See also: #3313
What
Add configurable custom role extraction for provider: "Custom" JWT authentication.
DAB currently expects custom roles in a fixed roles claim. This feature lets a developer configure where DAB reads custom roles from a JWT.
This supports common identity provider shapes such as:
{
"roles": [ "admin", "reader" ]
}
{
"realm_access": {
"roles": [ "admin", "reader" ]
}
}
{
"scope": "admin reader"
}
{
"https://schemas.example.com/roles": [ "admin", "reader" ]
}
Why
Different identity providers emit roles in different claim shapes.
DAB should not require users to rewrite tokens just to emit a top-level roles array.
The goal is to keep DAB authorization behavior unchanged while making custom JWT role extraction configurable.
Scope
This feature applies only when:
This feature does not apply to:
EntraID
AzureAD
StaticWebApps
AppService
Simulator
Unauthenticated
Non-goals
This feature does not change JWT validation.
This feature does not change issuer validation.
This feature does not change audience validation.
This feature does not change DAB permission evaluation.
This feature does not change X-MS-API-ROLE semantics.
This feature does not support multiple active roles per request.
This feature does not support array indexes in claim paths.
This feature does not add role aliasing.
This feature does not make role matching case-insensitive.
Configuration
Add three optional properties under:
runtime.host.authentication.jwt
Properties:
roles-path
roles-format
roles-delimiter
Example:
{
"runtime": {
"host": {
"authentication": {
"provider": "Custom",
"jwt": {
"issuer": "https://issuer.example.com",
"audience": "dab-api",
"roles-path": "realm_access.roles",
"roles-format": "array"
}
}
}
}
}
Delimited string example:
{
"runtime": {
"host": {
"authentication": {
"provider": "Custom",
"jwt": {
"issuer": "https://issuer.example.com",
"audience": "dab-api",
"roles-path": "scope",
"roles-format": "delimited-string",
"roles-delimiter": " "
}
}
}
}
}
Namespaced claim example:
{
"runtime": {
"host": {
"authentication": {
"provider": "Custom",
"jwt": {
"issuer": "https://issuer.example.com",
"audience": "dab-api",
"roles-path": "['https://schemas.example.com/roles']",
"roles-format": "array"
}
}
}
}
}
Mixed path example:
{
"runtime": {
"host": {
"authentication": {
"provider": "Custom",
"jwt": {
"issuer": "https://issuer.example.com",
"audience": "dab-api",
"roles-path": "resource_access['dab-api'].roles",
"roles-format": "array"
}
}
}
}
}
Defaults
Defaults must preserve current DAB behavior.
roles-path: roles
roles-format: array
roles-delimiter: " "
Default configuration behavior:
{
"roles": [ "admin", "reader" ]
}
Property rules
roles-path
roles-path identifies the JWT claim that contains custom roles.
It supports:
literal string
@env()
@akv()
Valid examples:
"roles-path": "realm_access.roles"
"roles-path": "['https://schemas.example.com/roles']"
"roles-path": "resource_access['dab-api'].roles"
"roles-path": "@env('JWT_ROLES_PATH')"
"roles-path": "@akv('jwt-roles-path')"
roles-format
roles-format defines how the resolved claim value is parsed.
It supports enum values only.
Valid values:
array
string
delimited-string
Valid examples:
"roles-format": "delimited-string"
Invalid examples:
"roles-format": "@env('JWT_ROLES_FORMAT')"
"roles-format": "@akv('jwt-roles-format')"
roles-delimiter
roles-delimiter is used only when:
"roles-format": "delimited-string"
It supports:
literal string
@env()
@akv()
Valid examples:
"roles-delimiter": "@env('JWT_ROLES_DELIMITER')"
"roles-delimiter": "@akv('jwt-roles-delimiter')"
roles-delimiter is valid only when roles-format is delimited-string.
Validation:
roles-format array + roles-delimiter present -> configuration error
roles-format string + roles-delimiter present -> configuration error
roles-format delimited-string + roles-delimiter missing -> valid, default to " "
roles-format delimited-string + roles-delimiter present -> valid
Path syntax
roles-path supports dot notation for nested objects.
It supports bracket notation for literal claim names.
['https://schemas.example.com/roles']
It supports mixed dot and bracket notation.
resource_access['dab-api'].roles
It does not support array indexing.
Valid:
roles
realm_access.roles
['https://schemas.example.com/roles']
resource_access['dab-api'].roles
Invalid:
realm_access..roles
resource_access['dab-api'.roles
groups[0]
resource_access.clients[1].roles
[]
Malformed paths fail dab validate and startup validation.
Extraction timing
DAB resolves roles only after normal JWT validation succeeds.
Order:
Validate JWT signature
Validate issuer
Validate audience
Validate lifetime
Authenticate identity
If X-MS-API-ROLE is present, resolve roles-path
Parse roles using roles-format
Compare requested role to extracted roles
Set active role or fail request
Custom role extraction runs only when X-MS-API-ROLE is present.
If X-MS-API-ROLE is not present, role extraction does not run and the active role is authenticated.
Active role behavior
A valid JWT establishes identity.
Without X-MS-API-ROLE, the active role is:
With X-MS-API-ROLE, DAB uses the header to select the active custom role.
The JWT roles prove the caller belongs to that role.
Both must agree.
Behavior:
valid JWT + no X-MS-API-ROLE -> active role is authenticated
valid JWT + X-MS-API-ROLE present -> extract roles from configured roles-path
valid JWT + X-MS-API-ROLE matches extracted role -> active role is requested custom role
valid JWT + X-MS-API-ROLE does not match extracted role -> 403
Role matching is case-sensitive.
Example:
X-MS-API-ROLE: admin
extracted roles: admin, reader
result: active role admin
X-MS-API-ROLE: admin
extracted roles: Admin, reader
result: 403
Format behavior
The resolved claim value must match roles-format exactly.
array -> JSON array of strings
string -> JSON string
delimited-string -> JSON string split by roles-delimiter
No convenience coercion.
Valid for array:
{
"roles": [ "admin", "reader" ]
}
Invalid for array:
Valid for string:
Invalid for string:
Valid for delimited-string:
{
"scope": "admin reader"
}
Invalid resolved values fail authentication with 401 and log an error:
object
number
boolean
null
array with non-string values
Role normalization
After extraction, DAB must:
trim each role
drop empty roles
deduplicate roles
preserve case
match case-sensitively
Example:
{
"roles": [ " admin ", "reader", "admin", "", "Admin" ]
}
Effective extracted roles:
Missing role claim behavior
If X-MS-API-ROLE is not present:
roles-path is not resolved
request uses authenticated role
If X-MS-API-ROLE is present and roles-path is missing from the token:
Reason:
The caller is authenticated, but the requested role cannot be proven from the token.
Error behavior
JWT validation failure returns the existing authentication failure behavior.
Role extraction format failure returns:
Examples:
roles-format array + value is string -> 401
roles-format string + value is array -> 401
roles-format delimited-string + value is array -> 401
Requested role mismatch returns:
Examples:
roles-path missing while X-MS-API-ROLE is present -> 403
roles extracted but requested role not found -> 403
case mismatch -> 403
Logging
Role extraction failures are logged as errors.
Log includes:
provider
roles-path
roles-format
failure reason
requested role
correlation ID
Log does not include:
JWT token
raw claim value
authorization header
secret values
Requested role mismatch should also be logged.
Log includes:
provider
roles-path
requested role
correlation ID
reason
Log does not include:
JWT token
raw claim value
authorization header
secret values
full extracted role list
Validation
dab validate and runtime startup must validate configuration.
Validation rules:
roles-path must be valid path syntax
roles-format must be array, string, or delimited-string
roles-delimiter is valid only when roles-format is delimited-string
roles-delimiter defaults to " " when roles-format is delimited-string
roles-path supports literal string, @env(), and @akv()
roles-delimiter supports literal string, @env(), and @akv()
roles-format does not support @env() or @akv()
this feature is valid only when provider is Custom
Malformed roles-path fails validation.
Invalid roles-format fails validation.
roles-delimiter with array or string fails validation.
Schema change
Add these properties under runtime.host.authentication.jwt.
"roles-path": {
"description": "Path to the JWT claim that contains custom roles. Supports dot notation and bracket notation. Defaults to the existing roles claim.",
"type": "string",
"default": "roles"
}
"roles-format": {
"description": "Format of the JWT claim that contains custom roles.",
"type": "string",
"enum": [ "array", "string", "delimited-string" ],
"default": "array"
}
"roles-delimiter": {
"description": "Delimiter used when roles-format is delimited-string.",
"type": "string",
"default": " "
}
For roles-path and roles-delimiter, use the existing string pattern that supports literal strings, @env(), and @akv() if available in the schema.
Documentation
Update custom JWT authentication docs.
Document:
default roles claim behavior
roles-path
roles-format
roles-delimiter
nested claim paths
namespaced claim paths
delimited role strings
X-MS-API-ROLE relationship to extracted roles
401 vs 403 behavior
security logging guidance
Include examples for:
default roles array
Keycloak realm_access.roles
Keycloak resource_access['client-id'].roles
Auth0 namespaced role claim
OAuth scope-style delimited string
comma-delimited custom role claim
Tests
Tests should cover:
default roles claim still works
roles-path defaults to roles
roles-format defaults to array
valid array claim
valid string claim
valid delimited-string claim
custom delimiter
nested dot path
bracket literal path
mixed dot and bracket path
namespaced claim key
malformed roles-path fails validation
unsupported array index fails validation
roles-format rejects @env()
roles-format rejects @akv()
roles-path supports @env()
roles-path supports @akv()
roles-delimiter supports @env()
roles-delimiter supports @akv()
roles-delimiter rejected when roles-format is array
roles-delimiter rejected when roles-format is string
roles-path missing with no X-MS-API-ROLE does not fail request
roles-path missing with X-MS-API-ROLE returns 403
format mismatch returns 401
requested role mismatch returns 403
case mismatch returns 403
roles are trimmed
empty roles are dropped
duplicate roles are deduplicated
valid JWT without X-MS-API-ROLE resolves to authenticated
role extraction runs only when X-MS-API-ROLE is present
feature is ignored or rejected for non-Custom providers
logs do not include JWT token
logs do not include raw claim value
logs do not include authorization header
Acceptance criteria
This feature applies only to provider: "Custom".
roles-path is added under runtime.host.authentication.jwt.
roles-format is added under runtime.host.authentication.jwt.
roles-delimiter is added under runtime.host.authentication.jwt.
roles-path defaults to current DAB behavior.
roles-format defaults to current DAB behavior.
roles-delimiter defaults to " ".
roles-path supports dot notation.
roles-path supports bracket notation.
roles-path supports mixed dot and bracket notation.
roles-path does not support array indexing.
Malformed roles-path fails dab validate.
Malformed roles-path fails runtime startup.
roles-format supports only array, string, and delimited-string.
roles-format is enum-only and does not support @env() or @akv().
roles-path supports literal string, @env(), and @akv().
roles-delimiter supports literal string, @env(), and @akv().
roles-delimiter is valid only when roles-format is delimited-string.
DAB resolves roles only after JWT validation succeeds.
DAB extracts custom roles only when X-MS-API-ROLE is present.
A valid JWT without X-MS-API-ROLE resolves to the authenticated role.
A valid JWT with X-MS-API-ROLE must match an extracted custom role.
Role matching is case-sensitive.
Extracted roles are trimmed.
Empty extracted roles are dropped.
Duplicate extracted roles are deduplicated.
Missing roles-path with X-MS-API-ROLE returns 403.
Requested role mismatch returns 403.
Resolved claim value format mismatch returns 401.
Role extraction failures are logged as errors.
Logs do not include JWT tokens, raw claim values, authorization headers, or secrets.
Existing authorization behavior remains unchanged.
See also: #3313
What
Add configurable custom role extraction for
provider: "Custom"JWT authentication.DAB currently expects custom roles in a fixed
rolesclaim. This feature lets a developer configure where DAB reads custom roles from a JWT.This supports common identity provider shapes such as:
{ "roles": [ "admin", "reader" ] }{ "realm_access": { "roles": [ "admin", "reader" ] } }{ "scope": "admin reader" }{ "https://schemas.example.com/roles": [ "admin", "reader" ] }Why
Different identity providers emit roles in different claim shapes.
DAB should not require users to rewrite tokens just to emit a top-level
rolesarray.The goal is to keep DAB authorization behavior unchanged while making custom JWT role extraction configurable.
Scope
This feature applies only when:
This feature does not apply to:
Non-goals
This feature does not change JWT validation.
This feature does not change issuer validation.
This feature does not change audience validation.
This feature does not change DAB permission evaluation.
This feature does not change
X-MS-API-ROLEsemantics.This feature does not support multiple active roles per request.
This feature does not support array indexes in claim paths.
This feature does not add role aliasing.
This feature does not make role matching case-insensitive.
Configuration
Add three optional properties under:
Properties:
Example:
{ "runtime": { "host": { "authentication": { "provider": "Custom", "jwt": { "issuer": "https://issuer.example.com", "audience": "dab-api", "roles-path": "realm_access.roles", "roles-format": "array" } } } } }Delimited string example:
{ "runtime": { "host": { "authentication": { "provider": "Custom", "jwt": { "issuer": "https://issuer.example.com", "audience": "dab-api", "roles-path": "scope", "roles-format": "delimited-string", "roles-delimiter": " " } } } } }Namespaced claim example:
{ "runtime": { "host": { "authentication": { "provider": "Custom", "jwt": { "issuer": "https://issuer.example.com", "audience": "dab-api", "roles-path": "['https://schemas.example.com/roles']", "roles-format": "array" } } } } }Mixed path example:
{ "runtime": { "host": { "authentication": { "provider": "Custom", "jwt": { "issuer": "https://issuer.example.com", "audience": "dab-api", "roles-path": "resource_access['dab-api'].roles", "roles-format": "array" } } } } }Defaults
Defaults must preserve current DAB behavior.
Default configuration behavior:
{ "roles": [ "admin", "reader" ] }Property rules
roles-path
roles-pathidentifies the JWT claim that contains custom roles.It supports:
Valid examples:
roles-format
roles-formatdefines how the resolved claim value is parsed.It supports enum values only.
Valid values:
Valid examples:
Invalid examples:
roles-delimiter
roles-delimiteris used only when:It supports:
Valid examples:
roles-delimiteris valid only whenroles-formatisdelimited-string.Validation:
Path syntax
roles-pathsupports dot notation for nested objects.It supports bracket notation for literal claim names.
It supports mixed dot and bracket notation.
It does not support array indexing.
Valid:
Invalid:
Malformed paths fail
dab validateand startup validation.Extraction timing
DAB resolves roles only after normal JWT validation succeeds.
Order:
Custom role extraction runs only when
X-MS-API-ROLEis present.If
X-MS-API-ROLEis not present, role extraction does not run and the active role isauthenticated.Active role behavior
A valid JWT establishes identity.
Without
X-MS-API-ROLE, the active role is:With
X-MS-API-ROLE, DAB uses the header to select the active custom role.The JWT roles prove the caller belongs to that role.
Both must agree.
Behavior:
Role matching is case-sensitive.
Example:
Format behavior
The resolved claim value must match
roles-formatexactly.No convenience coercion.
Valid for
array:{ "roles": [ "admin", "reader" ] }Invalid for
array:{ "roles": "admin" }Valid for
string:{ "role": "admin" }Invalid for
string:{ "role": [ "admin" ] }Valid for
delimited-string:{ "scope": "admin reader" }Invalid resolved values fail authentication with
401and log an error:Role normalization
After extraction, DAB must:
Example:
{ "roles": [ " admin ", "reader", "admin", "", "Admin" ] }Effective extracted roles:
Missing role claim behavior
If
X-MS-API-ROLEis not present:If
X-MS-API-ROLEis present androles-pathis missing from the token:Reason:
Error behavior
JWT validation failure returns the existing authentication failure behavior.
Role extraction format failure returns:
Examples:
Requested role mismatch returns:
Examples:
Logging
Role extraction failures are logged as errors.
Log includes:
Log does not include:
Requested role mismatch should also be logged.
Log includes:
Log does not include:
Validation
dab validateand runtime startup must validate configuration.Validation rules:
Malformed
roles-pathfails validation.Invalid
roles-formatfails validation.roles-delimiterwitharrayorstringfails validation.Schema change
Add these properties under
runtime.host.authentication.jwt.For
roles-pathandroles-delimiter, use the existing string pattern that supports literal strings,@env(), and@akv()if available in the schema.Documentation
Update custom JWT authentication docs.
Document:
Include examples for:
Tests
Tests should cover:
Acceptance criteria
This feature applies only to
provider: "Custom".roles-pathis added underruntime.host.authentication.jwt.roles-formatis added underruntime.host.authentication.jwt.roles-delimiteris added underruntime.host.authentication.jwt.roles-pathdefaults to current DAB behavior.roles-formatdefaults to current DAB behavior.roles-delimiterdefaults to" ".roles-pathsupports dot notation.roles-pathsupports bracket notation.roles-pathsupports mixed dot and bracket notation.roles-pathdoes not support array indexing.Malformed
roles-pathfailsdab validate.Malformed
roles-pathfails runtime startup.roles-formatsupports onlyarray,string, anddelimited-string.roles-formatis enum-only and does not support@env()or@akv().roles-pathsupports literal string,@env(), and@akv().roles-delimitersupports literal string,@env(), and@akv().roles-delimiteris valid only whenroles-formatisdelimited-string.DAB resolves roles only after JWT validation succeeds.
DAB extracts custom roles only when
X-MS-API-ROLEis present.A valid JWT without
X-MS-API-ROLEresolves to theauthenticatedrole.A valid JWT with
X-MS-API-ROLEmust match an extracted custom role.Role matching is case-sensitive.
Extracted roles are trimmed.
Empty extracted roles are dropped.
Duplicate extracted roles are deduplicated.
Missing
roles-pathwithX-MS-API-ROLEreturns403.Requested role mismatch returns
403.Resolved claim value format mismatch returns
401.Role extraction failures are logged as errors.
Logs do not include JWT tokens, raw claim values, authorization headers, or secrets.
Existing authorization behavior remains unchanged.