FerrisKeyDocs

Protocol mappers

Protocol mappers are the rules that turn user, group, organization and client data into JWT claims. Each mapper belongs to a client scope and runs during token generation.

A mapper is identified by a mapper_type string and carries a JSON config. FerrisKey keeps the Keycloak names for both, so configuration written for Keycloak is recognizable here: keys are dotted, and their values are strings, including the booleans. FerrisKey runs eleven mapper types, listed below.

Keys are dotted, not snake_case

The config keys are claim.name, access.token.claim and user.attribute, not claim_name, add_to_access_token or attribute. The config field is free-form JSON, so the API accepts an unknown key without complaining and the mapper then ignores it. A mapper that silently does nothing, or writes to a claim you did not name, is almost always a key spelled the wrong way.

The same goes for mapper_type. The API stores any non-empty string, and at token time FerrisKey skips a type it does not run and logs a warning. The console’s mapper catalog lists oidc-address-mapper, oidc-hardcoded-role-mapper and oidc-role-name-mapper, and its forms offer Keycloak fields such as userinfo.token.claim, introspection.token.claim, multivalued, claim.value.type and included.custom.audience. FerrisKey runs none of those types and reads none of those keys.

Keys every mapper reads

Config keyDefaultDescription
claim.nameper mapperTarget claim. Dotted paths nest, so realm_access.roles produces {"realm_access": {"roles": [...]}} rather than a flat key with a dot in it
access.token.claimtrueInclude the claim in the access token. A boolean, or the string true. Any other string counts as false
id.token.claimtrueInclude the claim in the ID token. Same values

Both token flags default to true when the key is absent, so a mapper with no flags writes to both tokens. The client role mapper, the full-name mapper and the audience mapper read the two flags but not claim.name.

token.claim.name is accepted as a legacy alias for claim.name by the property, attribute, realm role and hardcoded claim mappers, because some earlier consoles stored mappers that way. The group and organization mappers read only claim.name. Prefer claim.name in anything new.

A claim name is a dotted path of at most 8 non-empty segments and at most 255 characters. The API rejects a longer claim.name or token.claim.name with a 422 invalid_claim_name. At token time a name outside that shape is dropped silently, with only a warning in the server log.

Mapper types

oidc-usermodel-property-mapper

Maps a built-in user field to a claim.

Config keyDescription
user.attributeField to read: username, email, firstName, lastName, or emailVerified. The spellings firstname, lastname and email_verified are also accepted
claim.nameTarget claim. Defaults to the value of user.attribute
jsonType.labelCast the value: String (default), Boolean, or Integer. Long, int, long and boolean are accepted too. A value that cannot be cast stays as it is
{
  "user.attribute": "username",
  "claim.name": "preferred_username",
  "jsonType.label": "String",
  "access.token.claim": "true",
  "id.token.claim": "true"
}

oidc-full-name-mapper

Writes the user’s first and last name, trimmed and joined with a space, into the name claim.

The claim name is fixed: this mapper does not read claim.name. It emits nothing when both names are empty. Its only keys are the two token flags.

{
  "access.token.claim": "false",
  "id.token.claim": "true"
}

The seeded profile scope carries given_name and family_name but no name, so add this mapper to a scope if a client expects it.

oidc-usermodel-attribute-mapper

Maps a custom user attribute to a claim. Use it for business data: department, plan tier, feature flags.

Config keyDescription
user.attributeAttribute key on the user. The mapper emits nothing when the user has no such attribute
claim.nameTarget claim. Defaults to the value of user.attribute

The value is always a string. This mapper does not read jsonType.label, so a numeric attribute reaches the token as text.

{
  "user.attribute": "department",
  "claim.name": "department",
  "access.token.claim": "true",
  "id.token.claim": "true"
}

oidc-usermodel-realm-role-mapper

Writes the user’s realm role names into the token. Roles the user inherits from groups are included.

Config keyDescription
claim.nameTarget claim. Defaults to realm_access.roles
{
  "claim.name": "realm_access.roles",
  "access.token.claim": "true",
  "id.token.claim": "true"
}
{
  "realm_access": {
    "roles": ["admin", "user-manager", "viewer"]
  }
}

oidc-usermodel-client-role-mapper

Writes client-scoped roles under resource_access.<client_id>.roles. The placement is fixed: this mapper does not take a claim.name.

Config keyDescription
client.idThe client_id string of the one client whose roles to include. Left empty or absent, roles from every client are included
{
  "resource_access": {
    "backend": { "roles": ["read", "write"] }
  }
}

oidc-group-membership-mapper

Writes the groups the user belongs to.

Config keyDefaultDescription
claim.namegroupsTarget claim
membershipeffectiveeffective includes every group plus all its ancestors, so a member of /novotel/EN/leaf gets all three paths. direct includes only groups the user is a direct member of, which matches Keycloak, keeps tokens smaller, and discloses less of the org structure
full.pathtrueEmit full paths (/parent/child). Set to false for bare group names
prefix.orgfalsePrepend the group’s organization alias (/acme/finance)

prefix.org exists because a FerrisKey token can carry groups from several organizations at once, and paths are relative to their org, so /finance from two organizations would collide. It only applies to full paths.

oidc-organization-membership-mapper

Writes the organizations the user belongs to, as a JSON object keyed by alias. Keying by alias means a consumer can look up one organization without scanning an array.

Config keyDefaultDescription
claim.nameorganizationsTarget claim
include.domainfalseAdd each organization’s domain, null when it has none
include.attributesfalseAdd each organization’s attributes

id, name and alias are always present. Domain and attributes are opt-in so tokens stay compact by default. A user with no organization gets an empty object.

oidc-organization-detail-mapper

Writes detail about a single organization.

Config keyDefaultDescription
claim.nameorganizationTarget claim
organization.aliasunsetOnly emit a claim if the user belongs to this organization. Omitted, the first organization in the user’s membership list is used
include.attributesfalseAdd the organization’s attributes

id, name, alias and domain are always present, and domain is null when the organization has none. If the user belongs to no matching organization, the mapper emits nothing.

oidc-organization-role-mapper

Writes the roles a user holds within the scope of each organization, keyed by organization alias. Realm roles land under roles, client roles under clients.<client_id>.roles. An organization where the user holds no scoped role is left out.

Config keyDefaultDescription
claim.nameorganizationsTarget claim

Org-scoped roles come from two sources, merged during token assembly: roles assigned directly to the membership, and roles inherited from the user’s groups in that organization.

This mapper shares a claim with the membership mapper

Both default to the organizations claim, and their objects deep-merge when both are enabled: the membership mapper contributes id, name and alias, this one contributes roles and clients. Match on the membership mapper’s id field rather than the alias, which can be renamed.

oidc-audience-mapper

Adds an entry to the audience (aud) claim of the access token. Use it when a token has to be accepted by more than one resource server.

Config keyDescription
included.client.audienceAudience value to add. Any string works, not only a client_id. Empty or absent, the mapper emits nothing
{
  "included.client.audience": "another-client",
  "access.token.claim": "true",
  "id.token.claim": "false"
}

The access token’s aud starts as ["<realm_name>-realm", "account"] and the mapper appends to it. A token request with a resource parameter replaces the whole list with that resource. The ID token’s aud is the client’s client_id and no mapper changes it.

oidc-hardcoded-claim-mapper

Injects a static value. Useful for environment indicators or tenant identifiers that do not come from user data.

Config keyDescription
claim.nameTarget claim. The mapper emits nothing without one
claim.valueStatic value to inject
jsonType.labelCast the value: String (default), Boolean, Integer, Long or JSON. The lowercase boolean, int and long work too. JSON parses the value as JSON. A number or JSON that cannot be parsed stays a string, and a Boolean is true only for the exact string true
{
  "claim.name": "environment",
  "claim.value": "production",
  "jsonType.label": "String",
  "access.token.claim": "true",
  "id.token.claim": "false"
}

Reserved claim names

Ten claim names are removed after every mapper has run, so a mapper cannot overwrite them:

sub  iat  jti  iss  typ  azp  aud  scope  exp  client_id

Target one of these and the mapper appears to be configured correctly yet changes nothing. aud is on the list, which is exactly why adding an audience needs oidc-audience-mapper, which contributes audiences through a separate channel, and not a hardcoded mapper pointed at aud.

Execution order

Mappers within a scope run in the order they were created. Default scopes run before the optional scopes the request named.

Claim collisions

If two mappers write the same claim name, the later one overwrites the earlier one, unless both values are JSON objects: objects merge key by key. That is how features.new_dashboard and features.beta_export end up in one features object, and how the two organization mappers share the organizations claim. Be intentional about claim names across scopes.

Token generation pipeline

  1. Resolve scopes. The client’s default scopes, plus any optional scopes named in the request’s scope parameter.
  2. Gather mappers. Every protocol mapper on the active scopes.
  3. Execute mappers. Each one runs against the authenticated user and the client context, once for the access token and once for the ID token. The ID-token pass only happens when scope includes openid.
  4. Filter by token type. access.token.claim and id.token.claim decide which token each claim lands in.
  5. Strip reserved claims. The ten names above are removed.
  6. Build and sign. Mapper output is merged with the standard claims and the JWT is signed with the realm’s signing key.

Adding a mapper

Create or select a scope

Open Client Scopes in the admin console. Create a scope, or select the one that should carry the claim.

Add a protocol mapper

On the scope’s Protocol Mappers tab, choose Add mapper. Start from a quick-start template, or pick a mapper_type and fill the config keys documented above.

Assign the scope to a client

On the client’s Client Scopes tab, add the scope as Default or Optional. Default scopes apply to every request; an optional scope only applies when the request asks for it by name.

Check the result

Use Evaluate on the client’s Client Scopes tab, or call POST /realms/{realm_name}/clients/{client_id}/evaluate-scopes with a user_id and an optional scope. It returns the claims that user would receive without a full login. If a claim is missing, check the mapper_type and the config keys before anything else.