Repository: mrparkers/terraform-provider-keycloak
Pull request #309 (2020-05-29 14:18:01)

add user session note protocol mapper

dlechevalier: Adding user session note protocol mapper.
Waiting for your feedback.

Patch:
diff --git a/docs/resources/keycloak_openid_user_session_note_protocol_mapper.md b/docs/resources/keycloak_openid_user_session_note_protocol_mapper.md
new file mode 100644
index 0000000..201ff64
--- /dev/null
+++ b/docs/resources/keycloak_openid_user_session_note_protocol_mapper.md
@@ -0,0 +1,88 @@
+# keycloak_openid_user_session_note_protocol_mapper
+
+Allows for creating and managing user session note protocol mappers within
+Keycloak.
+
+User session note protocol mappers map a custom user session note to a token claim.
+Protocol mappers can be defined for a single client, or they can
+be defined for a client scope which can be shared between multiple different
+clients.
+
+### Example Usage (Client)
+
+```hcl
+resource "keycloak_realm" "realm" {
+    realm   = "my-realm"
+    enabled = true
+}
+resource "keycloak_openid_client" "openid_client" {
+    realm_id            = "${keycloak_realm.realm.id}"
+    client_id           = "test-client"
+    name                = "test client"
+    enabled             = true
+    access_type         = "CONFIDENTIAL"
+    valid_redirect_uris = [
+        "http://localhost:8080/openid-callback"
+    ]
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_client" {
+	name               = "tf-test-open-id-user-session-note-protocol-mapper-client"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_id          = "${keycloak_openid_client.openid_client.id}"
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "bar"
+    add_to_id_token     = true
+    add_to_access_token = false
+}
+```
+
+### Example Usage (Client Scope)
+
+```hcl
+resource "keycloak_realm" "realm" {
+    realm   = "my-realm"
+    enabled = true
+}
+resource "keycloak_openid_client_scope" "client_scope" {
+    realm_id = "${keycloak_realm.realm.id}"
+    name     = "test-client-scope"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_client_scope" {
+	name               = "tf-test-open-id-user-session-note-protocol-mapper-client-scope"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_scope_id    = "${keycloak_openid_client_scope.client_scope.id}"
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "bar"
+    add_to_id_token     = true
+    add_to_access_token = false
+}
+```
+
+### Argument Reference
+
+The following arguments are supported:
+
+- `realm_id` - (Required) The realm this protocol mapper exists within.
+- `client_id` - (Required if `client_scope_id` is not specified) The client this protocol mapper is attached to.
+- `client_scope_id` - (Required if `client_id` is not specified) The client scope this protocol mapper is attached to.
+- `name` - (Required) The display name of this protocol mapper in the GUI.
+- `claim_name` - (Required) The name of the claim to insert into a token.
+- `claim_value_type` - (Optional) The claim type used when serializing JSON tokens. Can be one of `String`, `JSON`, `long`, `int`, or `boolean`. Defaults to `String`.
+- `session_note_label` - (Optional) String value being the name of stored user session note within the UserSessionModel.note map.
+- `add_to_id_token` - (Optional) Indicates if the property should be added as a claim to the id token. Defaults to `true`.
+- `add_to_access_token` - (Optional) Indicates if the property should be added as a claim to the access token. Defaults to `true`.
+
+### Import
+
+Protocol mappers can be imported using one of the following formats:
+- Client: `{{realm_id}}/client/{{client_keycloak_id}}/{{protocol_mapper_id}}`
+- Client Scope: `{{realm_id}}/client-scope/{{client_scope_keycloak_id}}/{{protocol_mapper_id}}`
+
+Example:
+
+```bash
+$ terraform import keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4
+$ terraform import keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4
+```
\ No newline at end of file
diff --git a/example/main.tf b/example/main.tf
index 6e12c7f..ca66c7a 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -429,6 +429,32 @@ resource "keycloak_openid_user_client_role_protocol_mapper" "user_client_role_cl
   add_to_user_info    = false
 }
 
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_client" {
+	name               = "tf-test-open-id-user-session-note-protocol-mapper-client"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_id          = "${keycloak_openid_client.openid_client.id}"
+
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "bar"
+
+  add_to_id_token     = true
+  add_to_access_token = false
+}
+
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_client_scope" {
+	name               = "tf-test-open-id-user-session-note-protocol-mapper-client-scope"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_scope_id    = "${keycloak_openid_client_scope.test_default_client_scope.id}"
+
+	claim_name         = "foo2"
+	claim_value_type   = "String"
+	session_note_label = "bar2"
+
+  add_to_id_token     = true
+  add_to_access_token = false
+}
+
 resource "keycloak_openid_client" "bearer_only_client" {
   client_id   = "test-bearer-only-client"
   name        = "test-bearer-only-client"
diff --git a/keycloak/openid_user_session_note_protocol_mapper.go b/keycloak/openid_user_session_note_protocol_mapper.go
new file mode 100644
index 0000000..925c390
--- /dev/null
+++ b/keycloak/openid_user_session_note_protocol_mapper.go
@@ -0,0 +1,117 @@
+package keycloak
+
+import (
+	"fmt"
+	"strconv"
+)
+
+type OpenIdUserSessionNoteProtocolMapper struct {
+	Id            string
+	Name          string
+	RealmId       string
+	ClientId      string
+	ClientScopeId string
+
+	AddToIdToken     bool
+	AddToAccessToken bool
+
+	ClaimName            string
+	ClaimValueType       string
+	UserSessionNoteLabel string
+}
+
+func (mapper *OpenIdUserSessionNoteProtocolMapper) convertToGenericProtocolMapper() *protocolMapper {
+	return &protocolMapper{
+		Id:             mapper.Id,
+		Name:           mapper.Name,
+		Protocol:       "openid-connect",
+		ProtocolMapper: "oidc-usersessionmodel-note-mapper",
+		Config: map[string]string{
+			addToIdTokenField:              strconv.FormatBool(mapper.AddToIdToken),
+			addToAccessTokenField:          strconv.FormatBool(mapper.AddToAccessToken),
+			claimNameField:                 mapper.ClaimName,
+			claimValueTypeField:            mapper.ClaimValueType,
+			userSessionModelNoteLabelField: mapper.UserSessionNoteLabel,
+		},
+	}
+}
+
+func (protocolMapper *protocolMapper) convertToOpenIdUserSessionNoteProtocolMapper(realmId, clientId, clientScopeId string) (*OpenIdUserSessionNoteProtocolMapper, error) {
+	addToIdToken, err := strconv.ParseBool(protocolMapper.Config[addToIdTokenField])
+	if err != nil {
+		return nil, err
+	}
+
+	addToAccessToken, err := strconv.ParseBool(protocolMapper.Config[addToAccessTokenField])
+	if err != nil {
+		return nil, err
+	}
+
+	return &OpenIdUserSessionNoteProtocolMapper{
+		Id:            protocolMapper.Id,
+		Name:          protocolMapper.Name,
+		RealmId:       realmId,
+		ClientId:      clientId,
+		ClientScopeId: clientScopeId,
+
+		AddToIdToken:     addToIdToken,
+		AddToAccessToken: addToAccessToken,
+
+		ClaimName:            protocolMapper.Config[claimNameField],
+		ClaimValueType:       protocolMapper.Config[claimValueTypeField],
+		UserSessionNoteLabel: protocolMapper.Config[userSessionModelNoteLabelField],
+	}, nil
+}
+
+func (keycloakClient *KeycloakClient) GetOpenIdUserSessionNoteProtocolMapper(realmId, clientId, clientScopeId, mapperId string) (*OpenIdUserSessionNoteProtocolMapper, error) {
+	var protocolMapper *protocolMapper
+
+	err := keycloakClient.get(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protocolMapper, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return protocolMapper.convertToOpenIdUserSessionNoteProtocolMapper(realmId, clientId, clientScopeId)
+}
+
+func (keycloakClient *KeycloakClient) DeleteOpenIdUserSessionNoteProtocolMapper(realmId, clientId, clientScopeId, mapperId string) error {
+	return keycloakClient.delete(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), nil)
+}
+
+func (keycloakClient *KeycloakClient) NewOpenIdUserSessionNoteProtocolMapper(mapper *OpenIdUserSessionNoteProtocolMapper) error {
+	path := protocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId)
+
+	_, location, err := keycloakClient.post(path, mapper.convertToGenericProtocolMapper())
+	if err != nil {
+		return err
+	}
+
+	mapper.Id = getIdFromLocationHeader(location)
+
+	return nil
+}
+
+func (keycloakClient *KeycloakClient) UpdateOpenIdUserSessionNoteProtocolMapper(mapper *OpenIdUserSessionNoteProtocolMapper) error {
+	path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id)
+
+	return keycloakClient.put(path, mapper.convertToGenericProtocolMapper())
+}
+
+func (keycloakClient *KeycloakClient) ValidateOpenIdUserSessionNoteProtocolMapper(mapper *OpenIdUserSessionNoteProtocolMapper) error {
+	if mapper.ClientId == "" && mapper.ClientScopeId == "" {
+		return fmt.Errorf("validation error: one of ClientId or ClientScopeId must be set")
+	}
+
+	protocolMappers, err := keycloakClient.listGenericProtocolMappers(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId)
+	if err != nil {
+		return err
+	}
+
+	for _, protocolMapper := range protocolMappers {
+		if protocolMapper.Name == mapper.Name && protocolMapper.Id != mapper.Id {
+			return fmt.Errorf("validation error: a protocol mapper with name %s already exists for this client", mapper.Name)
+		}
+	}
+
+	return nil
+}
diff --git a/keycloak/protocol_mapper.go b/keycloak/protocol_mapper.go
index 0a08579..c94edf5 100644
--- a/keycloak/protocol_mapper.go
+++ b/keycloak/protocol_mapper.go
@@ -30,6 +30,7 @@ var (
 	userRealmRoleMappingRolePrefixField  = "usermodel.realmRoleMapping.rolePrefix"
 	userClientRoleMappingClientIdField   = "usermodel.clientRoleMapping.clientId"
 	userClientRoleMappingRolePrefixField = "usermodel.clientRoleMapping.rolePrefix"
+	userSessionModelNoteLabelField       = "userSession.modelNote.label"
 	aggregateAttributeValuesField        = "aggregate.attrs"
 )
 
diff --git a/mkdocs.yml b/mkdocs.yml
index d0c1b1d..dd1074b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -32,6 +32,7 @@ nav:
   - keycloak_openid_hardcoded_role_protocol_mapper: resources/keycloak_openid_hardcoded_role_protocol_mapper.md
   - keycloak_openid_user_realm_role_protocol_mapper: resources/keycloak_openid_user_realm_role_protocol_mapper.md
   - keycloak_openid_user_client_role_protocol_mapper: resources/keycloak_openid_user_client_role_protocol_mapper.md
+  - keycloak_openid_user_session_note_protocol_mapper: resources/keycloak_openid_user_session_note_protocol_mapper.md
   - keycloak_saml_client: resources/keycloak_saml_client.md
   - keycloak_saml_user_attribute_protocol_mapper: resources/keycloak_saml_user_attribute_protocol_mapper.md
   - keycloak_saml_user_property_protocol_mapper: resources/keycloak_saml_user_property_protocol_mapper.md
diff --git a/provider/provider.go b/provider/provider.go
index aed886d..de1020f 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -47,6 +47,7 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_openid_hardcoded_role_protocol_mapper":           resourceKeycloakOpenIdHardcodedRoleProtocolMapper(),
 			"keycloak_openid_user_realm_role_protocol_mapper":          resourceKeycloakOpenIdUserRealmRoleProtocolMapper(),
 			"keycloak_openid_user_client_role_protocol_mapper":         resourceKeycloakOpenIdUserClientRoleProtocolMapper(),
+			"keycloak_openid_user_session_note_protocol_mapper":        resourceKeycloakOpenIdUserSessionNoteProtocolMapper(),
 			"keycloak_openid_client_default_scopes":                    resourceKeycloakOpenidClientDefaultScopes(),
 			"keycloak_openid_client_optional_scopes":                   resourceKeycloakOpenidClientOptionalScopes(),
 			"keycloak_saml_client":                                     resourceKeycloakSamlClient(),
diff --git a/provider/resource_keycloak_openid_user_session_note_protocol_mapper.go b/provider/resource_keycloak_openid_user_session_note_protocol_mapper.go
new file mode 100644
index 0000000..488638c
--- /dev/null
+++ b/provider/resource_keycloak_openid_user_session_note_protocol_mapper.go
@@ -0,0 +1,176 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func resourceKeycloakOpenIdUserSessionNoteProtocolMapper() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceKeycloakOpenIdUserSessionNoteProtocolMapperCreate,
+		Read:   resourceKeycloakOpenIdUserSessionNoteProtocolMapperRead,
+		Update: resourceKeycloakOpenIdUserSessionNoteProtocolMapperUpdate,
+		Delete: resourceKeycloakOpenIdUserSessionNoteProtocolMapperDelete,
+		Importer: &schema.ResourceImporter{
+			// import a mapper tied to a client:
+			// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
+			// or a client scope:
+			// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
+			State: genericProtocolMapperImport,
+		},
+		Schema: map[string]*schema.Schema{
+			"name": {
+				Type:        schema.TypeString,
+				Required:    true,
+				Description: "A human-friendly name that will appear in the Keycloak console.",
+			},
+			"realm_id": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The realm id where the associated client or client scope exists.",
+			},
+			"client_id": {
+				Type:          schema.TypeString,
+				Optional:      true,
+				ForceNew:      true,
+				Description:   "The mapper's associated client. Cannot be used at the same time as client_scope_id.",
+				ConflictsWith: []string{"client_scope_id"},
+			},
+			"client_scope_id": {
+				Type:          schema.TypeString,
+				Optional:      true,
+				ForceNew:      true,
+				Description:   "The mapper's associated client scope. Cannot be used at the same time as client_id.",
+				ConflictsWith: []string{"client_id"},
+			},
+			"add_to_id_token": {
+				Type:        schema.TypeBool,
+				Optional:    true,
+				Default:     true,
+				Description: "Indicates if the attribute should be a claim in the id token.",
+			},
+			"add_to_access_token": {
+				Type:        schema.TypeBool,
+				Optional:    true,
+				Default:     true,
+				Description: "Indicates if the attribute should be a claim in the access token.",
+			},
+			"claim_name": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"claim_value_type": {
+				Type:         schema.TypeString,
+				Optional:     true,
+				Description:  "Claim type used when serializing tokens.",
+				Default:      "String",
+				ValidateFunc: validation.StringInSlice([]string{"JSON", "String", "long", "int", "boolean"}, true),
+			},
+			"session_note_label": {
+				Type:        schema.TypeString,
+				Optional:    true,
+				Description: "String value being the name of stored user session note within the UserSessionModel.note map.",
+			},
+		},
+	}
+}
+
+func mapFromDataToOpenIdUserSessionNoteProtocolMapper(data *schema.ResourceData) *keycloak.OpenIdUserSessionNoteProtocolMapper {
+	return &keycloak.OpenIdUserSessionNoteProtocolMapper{
+		Id:               data.Id(),
+		Name:             data.Get("name").(string),
+		RealmId:          data.Get("realm_id").(string),
+		ClientId:         data.Get("client_id").(string),
+		ClientScopeId:    data.Get("client_scope_id").(string),
+		AddToIdToken:     data.Get("add_to_id_token").(bool),
+		AddToAccessToken: data.Get("add_to_access_token").(bool),
+
+		ClaimName:            data.Get("claim_name").(string),
+		ClaimValueType:       data.Get("claim_value_type").(string),
+		UserSessionNoteLabel: data.Get("session_note_label").(string),
+	}
+}
+
+func mapFromOpenIdUserSessionNoteMapperToData(mapper *keycloak.OpenIdUserSessionNoteProtocolMapper, data *schema.ResourceData) {
+	data.SetId(mapper.Id)
+	data.Set("name", mapper.Name)
+	data.Set("realm_id", mapper.RealmId)
+
+	if mapper.ClientId != "" {
+		data.Set("client_id", mapper.ClientId)
+	} else {
+		data.Set("client_scope_id", mapper.ClientScopeId)
+	}
+
+	data.Set("add_to_id_token", mapper.AddToIdToken)
+	data.Set("add_to_access_token", mapper.AddToAccessToken)
+	data.Set("claim_name", mapper.ClaimName)
+	data.Set("claim_value_type", mapper.ClaimValueType)
+	data.Set("session_note_label", mapper.UserSessionNoteLabel)
+}
+
+func resourceKeycloakOpenIdUserSessionNoteProtocolMapperCreate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	openIdUserSessionNoteMapper := mapFromDataToOpenIdUserSessionNoteProtocolMapper(data)
+
+	err := keycloakClient.ValidateOpenIdUserSessionNoteProtocolMapper(openIdUserSessionNoteMapper)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.NewOpenIdUserSessionNoteProtocolMapper(openIdUserSessionNoteMapper)
+	if err != nil {
+		return err
+	}
+
+	mapFromOpenIdUserSessionNoteMapperToData(openIdUserSessionNoteMapper, data)
+
+	return resourceKeycloakOpenIdUserSessionNoteProtocolMapperRead(data, meta)
+}
+
+func resourceKeycloakOpenIdUserSessionNoteProtocolMapperRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+	realmId := data.Get("realm_id").(string)
+	clientId := data.Get("client_id").(string)
+	clientScopeId := data.Get("client_scope_id").(string)
+
+	openIdUserSessionNoteMapper, err := keycloakClient.GetOpenIdUserSessionNoteProtocolMapper(realmId, clientId, clientScopeId, data.Id())
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+
+	mapFromOpenIdUserSessionNoteMapperToData(openIdUserSessionNoteMapper, data)
+
+	return nil
+}
+
+func resourceKeycloakOpenIdUserSessionNoteProtocolMapperUpdate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	openIdUserSessionNoteMapper := mapFromDataToOpenIdUserSessionNoteProtocolMapper(data)
+
+	err := keycloakClient.ValidateOpenIdUserSessionNoteProtocolMapper(openIdUserSessionNoteMapper)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.UpdateOpenIdUserSessionNoteProtocolMapper(openIdUserSessionNoteMapper)
+	if err != nil {
+		return err
+	}
+
+	return resourceKeycloakOpenIdUserSessionNoteProtocolMapperRead(data, meta)
+}
+
+func resourceKeycloakOpenIdUserSessionNoteProtocolMapperDelete(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmId := data.Get("realm_id").(string)
+	clientId := data.Get("client_id").(string)
+	clientScopeId := data.Get("client_scope_id").(string)
+
+	return keycloakClient.DeleteOpenIdUserSessionNoteProtocolMapper(realmId, clientId, clientScopeId, data.Id())
+}


Test patch:
diff --git a/provider/generic_protocol_mapper_validation_test.go b/provider/generic_protocol_mapper_validation_test.go
index 3fb23fc..8156940 100644
--- a/provider/generic_protocol_mapper_validation_test.go
+++ b/provider/generic_protocol_mapper_validation_test.go
@@ -305,6 +305,54 @@ func TestAccKeycloakOpenIdUserClientRoleProtocolMapper_clientScopeDuplicateNameV
 	})
 }
 
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_clientDuplicateNameValidation(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-protocol-mapper-" + acctest.RandString(5)
+
+	userRealmRoleProtocolMapperResourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testGenericProtocolMapperValidation_clientRealmRoleMapper(realmName, clientId, mapperName),
+				Check:  testKeycloakOpenIdUserRealmRoleProtocolMapperExists(userRealmRoleProtocolMapperResourceName),
+			},
+			{
+				Config:      testGenericProtocolMapperValidation_clientUserSessionNoteAndRealmRoleMapper(realmName, clientId, mapperName),
+				ExpectError: regexp.MustCompile("validation error: a protocol mapper with name .+ already exists for this client"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_clientScopeDuplicateNameValidation(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-protocol-mapper-" + acctest.RandString(5)
+
+	userRealmRoleProtocolMapperResourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client_scope"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testGenericProtocolMapperValidation_clientScopeUserRealmRoleMapper(realmName, clientId, mapperName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(userRealmRoleProtocolMapperResourceName),
+			},
+			{
+				Config:      testGenericProtocolMapperValidation_clientScopeUserSessionNoteAndRealmRoleMapper(realmName, clientId, mapperName),
+				ExpectError: regexp.MustCompile("validation error: a protocol mapper with name .+ already exists for this client"),
+			},
+		},
+	})
+}
+
 /*
  * Protocol mappers must be attached to either a client or client scope.  The following tests assert that errors are raised
  * if neither are specified.
@@ -422,6 +470,22 @@ func TestAccKeycloakOpenIdUserClientRoleProtocolMapper_validateClientOrClientSco
 	})
 }
 
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_validateClientOrClientScopeSet(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers: testAccProviders,
+		PreCheck:  func() { testAccPreCheck(t) },
+		Steps: []resource.TestStep{
+			{
+				Config:      testKeycloakOpenIdUserSessionNoteProtocolMapper_parentResourceValidation(realmName, mapperName),
+				ExpectError: regexp.MustCompile("validation error: one of ClientId or ClientScopeId must be set"),
+			},
+		},
+	})
+}
+
 func testGenericProtocolMapperValidation_clientGroupMembershipMapper(realmName, clientId, mapperName string) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {
@@ -693,6 +757,49 @@ resource "keycloak_openid_user_client_role_protocol_mapper" "user_client_role_ma
 }`, realmName, clientId, mapperName)
 }
 
+func testGenericProtocolMapperValidation_clientUserSessionNoteAndRealmRoleMapper(realmName, clientId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client" "openid_client" {
+	realm_id    = "${keycloak_realm.realm.id}"
+	client_id   = "%s"
+	access_type = "BEARER-ONLY"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_validation" {
+	name             = "%s"
+	realm_id         = "${keycloak_realm.realm.id}"
+	client_id        = "${keycloak_openid_client.openid_client.id}"
+	claim_name       = "foo"
+	claim_value_type = "String"
+}
+resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client" {
+	name             = "%s"
+	realm_id         = "${keycloak_realm.realm.id}"
+	client_id        = "${keycloak_openid_client.openid_client.id}"
+	claim_name       = "foo"
+}`, realmName, clientId, mapperName, mapperName)
+}
+
+func testGenericProtocolMapperValidation_clientRealmRoleMapper(realmName, clientId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client" "openid_client" {
+	realm_id    = "${keycloak_realm.realm.id}"
+	client_id   = "%s"
+	access_type = "BEARER-ONLY"
+}
+resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client" {
+	name             = "%s"
+	realm_id         = "${keycloak_realm.realm.id}"
+	client_id        = "${keycloak_openid_client.openid_client.id}"
+	claim_name       = "foo"
+}`, realmName, clientId, mapperName)
+}
+
 func testGenericProtocolMapperValidation_clientScopeFullNameMapper(realmName, clientId, mapperName string) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {
@@ -789,6 +896,24 @@ resource "keycloak_openid_user_client_role_protocol_mapper" "user_client_role_ma
 
 }
 
+func testGenericProtocolMapperValidation_clientScopeUserSessionNoteMapper(realmName, clientId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client_scope" "client_scope" {
+	name     = "%s"
+	realm_id = "${keycloak_realm.realm.id}"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_validation" {
+	name            = "%s"
+	realm_id        = "${keycloak_realm.realm.id}"
+	client_scope_id = "${keycloak_openid_client_scope.client_scope.id}"
+	claim_name      = "bar-property"
+}`, realmName, clientId, mapperName)
+
+}
+
 func testGenericProtocolMapperValidation_clientScopeFullNameAndGroupMembershipMapper(realmName, clientScopeId, mapperName string) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {
@@ -953,6 +1078,30 @@ resource "keycloak_openid_hardcoded_claim_protocol_mapper" "hardcoded_claim_mapp
 }`, realmName, clientId, mapperName, mapperName)
 }
 
+func testGenericProtocolMapperValidation_clientScopeUserSessionNoteAndRealmRoleMapper(realmName, clientId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client_scope" "client_scope" {
+	name     = "%s"
+	realm_id = "${keycloak_realm.realm.id}"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_client_scope" {
+	name            = "%s"
+	realm_id        = "${keycloak_realm.realm.id}"
+	client_scope_id = "${keycloak_openid_client_scope.client_scope.id}"
+	claim_name       = "foo"
+	claim_value_type = "String"
+}
+resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client_scope" {
+	name            = "%s"
+	realm_id        = "${keycloak_realm.realm.id}"
+	client_scope_id = "${keycloak_openid_client_scope.client_scope.id}"
+	claim_name      = "foo"
+}`, realmName, clientId, mapperName, mapperName)
+}
+
 func testKeycloakOpenIdFullNameProtocolMapper_parentResourceValidation(realmName, mapperName string) string {
 	return fmt.Sprintf(`
 resource "keycloak_realm" "realm" {
@@ -1049,3 +1198,16 @@ resource "keycloak_openid_user_client_role_protocol_mapper" "user_client_role_ma
 	claim_value_type = "String"
 }`, realmName, mapperName)
 }
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapper_parentResourceValidation(realmName, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_client_scope" {
+	name             = "%s"
+	realm_id         = "${keycloak_realm.realm.id}"
+	claim_name       = "foo"
+	claim_value_type = "String"
+}`, realmName, mapperName)
+}
diff --git a/provider/resource_keycloak_openid_user_session_note_protocol_mapper_test.go b/provider/resource_keycloak_openid_user_session_note_protocol_mapper_test.go
new file mode 100644
index 0000000..1c2bdb1
--- /dev/null
+++ b/provider/resource_keycloak_openid_user_session_note_protocol_mapper_test.go
@@ -0,0 +1,462 @@
+package provider
+
+import (
+	"fmt"
+	"regexp"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/terraform"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_basicClient(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper_client"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_basicClientScope(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientScopeId := "terraform-client-scope-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper_client_scope"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_clientScope(realmName, clientScopeId, mapperName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_import(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-openid-client-" + acctest.RandString(10)
+	clientScopeId := "terraform-client-scope-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	clientResourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper_client"
+	clientScopeResourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper_client_scope"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_import(realmName, clientId, clientScopeId, mapperName),
+				Check: resource.ComposeTestCheckFunc(
+					testKeycloakOpenIdUserSessionNoteProtocolMapperExists(clientResourceName),
+					testKeycloakOpenIdUserSessionNoteProtocolMapperExists(clientScopeResourceName),
+				),
+			},
+			{
+				ResourceName:      clientResourceName,
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getGenericProtocolMapperIdForClient(clientResourceName),
+			},
+			{
+				ResourceName:      clientScopeResourceName,
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getGenericProtocolMapperIdForClientScope(clientScopeResourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_updateClaim(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	claimName := "claim-name-" + acctest.RandString(10)
+	updatedClaimName := "claim-name-update-" + acctest.RandString(10)
+
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_claim(realmName, clientId, mapperName, claimName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_claim(realmName, clientId, mapperName, updatedClaimName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_updateLabel(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	labelName := "session-note-label-" + acctest.RandString(10)
+	updatedLabelName := "session-note-label-update-" + acctest.RandString(10)
+
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_label(realmName, clientId, mapperName, labelName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_label(realmName, clientId, mapperName, updatedLabelName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_createAfterManualDestroy(t *testing.T) {
+	var mapper = &keycloak.OpenIdUserSessionNoteProtocolMapper{}
+
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper_client"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperFetch(resourceName, mapper),
+			},
+			{
+				PreConfig: func() {
+					keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+					err := keycloakClient.DeleteOpenIdUserSessionNoteProtocolMapper(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id)
+					if err != nil {
+						t.Error(err)
+					}
+				},
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_client(realmName, clientId, mapperName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_validateClaimValueType(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(10)
+	invalidClaimValueType := acctest.RandString(5)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config:      testKeycloakOpenIdUserSessionNoteProtocolMapper_validateClaimValueType(realmName, mapperName, invalidClaimValueType),
+				ExpectError: regexp.MustCompile("expected claim_value_type to be one of .+ got " + invalidClaimValueType),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_updateClientIdForceNew(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	updatedClientId := "terraform-client-update-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	claimName := "claim-name-" + acctest.RandString(10)
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_claim(realmName, clientId, mapperName, claimName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_claim(realmName, updatedClientId, mapperName, claimName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_updateClientScopeForceNew(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+	clientScopeId := "terraform-client-" + acctest.RandString(10)
+	newClientScopeId := "terraform-client-scope-" + acctest.RandString(10)
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper_client_scope"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_clientScope(realmName, clientScopeId, mapperName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_clientScope(realmName, newClientScopeId, mapperName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOpenIdUserSessionNoteProtocolMapper_updateRealmIdForceNew(t *testing.T) {
+	realmName := "terraform-realm-" + acctest.RandString(10)
+	newRealmName := "terraform-realm-" + acctest.RandString(10)
+	clientId := "terraform-client-" + acctest.RandString(10)
+	mapperName := "terraform-openid-connect-user-session-note-mapper-" + acctest.RandString(5)
+
+	claimName := "claim-name-" + acctest.RandString(10)
+	resourceName := "keycloak_openid_user_session_note_protocol_mapper.user_session_note_mapper"
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_claim(realmName, clientId, mapperName, claimName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+			{
+				Config: testKeycloakOpenIdUserSessionNoteProtocolMapper_claim(newRealmName, clientId, mapperName, claimName),
+				Check:  testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName),
+			},
+		},
+	})
+}
+
+func testAccKeycloakOpenIdUserSessionNoteProtocolMapperDestroy() resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		for resourceName, rs := range state.RootModule().Resources {
+			if rs.Type != "keycloak_openid_user_session_note_protocol_mapper" {
+				continue
+			}
+
+			mapper, _ := getUserSessionNoteMapperUsingState(state, resourceName)
+
+			if mapper != nil {
+				return fmt.Errorf("openid user attribute protocol mapper with id %s still exists", rs.Primary.ID)
+			}
+		}
+
+		return nil
+	}
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapperExists(resourceName string) resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		_, err := getUserSessionNoteMapperUsingState(state, resourceName)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapperFetch(resourceName string, mapper *keycloak.OpenIdUserSessionNoteProtocolMapper) resource.TestCheckFunc {
+	return func(state *terraform.State) error {
+		fetchedMapper, err := getUserSessionNoteMapperUsingState(state, resourceName)
+		if err != nil {
+			return err
+		}
+
+		mapper.Id = fetchedMapper.Id
+		mapper.ClientId = fetchedMapper.ClientId
+		mapper.ClientScopeId = fetchedMapper.ClientScopeId
+		mapper.RealmId = fetchedMapper.RealmId
+
+		return nil
+	}
+}
+
+func getUserSessionNoteMapperUsingState(state *terraform.State, resourceName string) (*keycloak.OpenIdUserSessionNoteProtocolMapper, error) {
+	rs, ok := state.RootModule().Resources[resourceName]
+	if !ok {
+		return nil, fmt.Errorf("resource not found in TF state: %s ", resourceName)
+	}
+
+	id := rs.Primary.ID
+	realm := rs.Primary.Attributes["realm_id"]
+	clientId := rs.Primary.Attributes["client_id"]
+	clientScopeId := rs.Primary.Attributes["client_scope_id"]
+
+	keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+	return keycloakClient.GetOpenIdUserSessionNoteProtocolMapper(realm, clientId, clientScopeId, id)
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_client(realmName, clientId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client" "openid_client" {
+	realm_id  = "${keycloak_realm.realm.id}"
+	client_id = "%s"
+	access_type = "BEARER-ONLY"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_client" {
+	name               = "%s"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_id          = "${keycloak_openid_client.openid_client.id}"
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "bar"
+}`, realmName, clientId, mapperName)
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapper_basic_clientScope(realmName, clientScopeId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client_scope" "client_scope" {
+	name     = "%s"
+	realm_id = "${keycloak_realm.realm.id}"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_client_scope" {
+	name               = "%s"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_scope_id    = "${keycloak_openid_client_scope.client_scope.id}"
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "bar"
+}`, realmName, clientScopeId, mapperName)
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapper_claim(realmName, clientId, mapperName, claimName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client" "openid_client" {
+	realm_id  = "${keycloak_realm.realm.id}"
+	client_id = "%s"
+	access_type = "BEARER-ONLY"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper" {
+	name             = "%s"
+	realm_id         = "${keycloak_realm.realm.id}"
+	client_id        = "${keycloak_openid_client.openid_client.id}"
+	claim_name       = "%s"
+	claim_value_type = "String"
+}`, realmName, clientId, mapperName, claimName)
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapper_label(realmName, clientId, mapperName, labelName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client" "openid_client" {
+	realm_id  = "${keycloak_realm.realm.id}"
+	client_id = "%s"
+	access_type = "BEARER-ONLY"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper" {
+	name               = "%s"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_id          = "${keycloak_openid_client.openid_client.id}"
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "%s"
+}`, realmName, clientId, mapperName, labelName)
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapper_import(realmName, clientId, clientScopeId, mapperName string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client" "openid_client" {
+	realm_id    = "${keycloak_realm.realm.id}"
+	client_id   = "%s"
+	access_type = "BEARER-ONLY"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_client" {
+	name               = "%s"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_id          = "${keycloak_openid_client.openid_client.id}"
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "bar"
+}
+resource "keycloak_openid_client_scope" "client_scope" {
+	name     = "%s"
+	realm_id = "${keycloak_realm.realm.id}"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_client_scope" {
+	name               = "%s"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_scope_id    = "${keycloak_openid_client_scope.client_scope.id}"
+	claim_name         = "foo"
+	claim_value_type   = "String"
+	session_note_label = "bar"
+}`, realmName, clientId, mapperName, clientScopeId, mapperName)
+}
+
+func testKeycloakOpenIdUserSessionNoteProtocolMapper_validateClaimValueType(realmName, mapperName, claimValueType string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+resource "keycloak_openid_client" "openid_client" {
+	realm_id  = "${keycloak_realm.realm.id}"
+	client_id = "openid-client"
+	access_type = "BEARER-ONLY"
+}
+resource "keycloak_openid_user_session_note_protocol_mapper" "user_session_note_mapper_validation" {
+	name               = "%s"
+	realm_id           = "${keycloak_realm.realm.id}"
+	client_id          = "${keycloak_openid_client.openid_client.id}"
+	claim_name         = "foo"
+	claim_value_type   = "%s"
+	session_note_label = "bar"
+}`, realmName, mapperName, claimValueType)
+}