Skip to content

Add Custom JWT role path and format support#3549

Open
Copilot wants to merge 7 commits into
mainfrom
copilot/add-custom-jwt-paths
Open

Add Custom JWT role path and format support#3549
Copilot wants to merge 7 commits into
mainfrom
copilot/add-custom-jwt-paths

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 13, 2026

Why make this change?

Custom JWT providers often place roles outside DAB’s expected top-level roles array. This adds configurable role extraction so providers like Keycloak, Auth0, Okta, Cognito, and custom issuers can work without token rewriting.

What is this change?

  • Configuration

    • Adds runtime.host.authentication.jwt.rolesPath
    • Adds runtime.host.authentication.jwt.rolesFormat
    • Runtime and schema defaults remain rolesPath: "roles" and rolesFormat: "array" for Custom JWT authentication.
    • Rejects blank or whitespace-only rolesPath values.
  • Custom JWT role extraction

    • Resolves roles from the raw JWT payload JSON.
    • Supports literal keys, URL keys, bracket literals, and dot-path traversal.
    • Supports array, string, space-delimited, and comma-delimited formats.
    • Normalizes roles by trimming, removing empty values, ordinal de-duping, and preserving casing.
    • Injects normalized roles as DAB "roles" claims.
  • Validation and runtime behavior

    • Rejects rolesPath / rolesFormat for non-Custom providers.
    • Rejects invalid bracket syntax, blank role paths, and invalid role formats at startup.
    • Applies the same provider/path/format validation when using dab configure before writing config changes.
    • Fails authentication for missing configured claims or unsupported claim value types.
    • Preserves existing X-MS-API-ROLE, User.IsInRole(), @claims.roles, and system Authenticated role behavior.
  • Schema and CLI

    • Updates JSON schema for the new JWT settings.
    • Adds conditional schema defaults for rolesPath and rolesFormat only when the provider is Custom.
    • Prevents schema-valid non-Custom JWT configs from using rolesPath / rolesFormat.
    • Adds CLI init/configure support for the new options.
"authentication": {
  "provider": "Custom",
  "jwt": {
    "issuer": "https://keycloak.example.com/realms/myrealm",
    "audience": "dab-api",
    "rolesPath": "realm_access.roles",
    "rolesFormat": "array"
  }
}

How was this tested?

  • Integration Tests
  • Unit Tests

Targeted authentication/configuration/CLI coverage was added for role path resolution, role formats, invalid inputs, startup validation, dab configure validation, X-MS-API-ROLE, User.IsInRole(), @claims.roles, schema-default behavior, exact logging for missing claims, and non-Custom provider default behavior.

Validation run:

  • dotnet format src/Azure.DataApiBuilder.sln
  • dotnet build src/Azure.DataApiBuilder.sln --no-restore
  • Targeted CLI JWT role settings tests
  • Targeted service authentication/configuration tests

Sample Request(s)

dab init \
  --database-type mssql \
  --connection-string "@env('connection_string')" \
  --auth.provider Custom \
  --auth.audience dab-api \
  --auth.issuer https://keycloak.example.com/realms/myrealm \
  --auth.roles-path realm_access.roles \
  --auth.roles-format array
dab configure \
  --runtime.host.authentication.provider Custom \
  --runtime.host.authentication.jwt.audience dab-api \
  --runtime.host.authentication.jwt.issuer https://keycloak.example.com/realms/myrealm \
  --runtime.host.authentication.jwt.rolesPath realm_access.roles \
  --runtime.host.authentication.jwt.rolesFormat array
curl \
  -H "Authorization: ******" \
  -H "X-MS-API-ROLE: editor" \
  https://localhost:5001/api/books

Copilot AI linked an issue May 13, 2026 that may be closed by this pull request
Copilot AI and others added 2 commits May 13, 2026 23:16
Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/46abd1d4-486d-40f9-a907-f91714efb3ad

Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/46abd1d4-486d-40f9-a907-f91714efb3ad

Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
Copilot AI changed the title [WIP] Add custom JWT paths for roles in authentication Add Custom JWT role path and format support May 13, 2026
Copilot AI requested a review from JerryNixon May 13, 2026 23:27
@JerryNixon JerryNixon marked this pull request as ready for review May 13, 2026 23:34
Copilot AI review requested due to automatic review settings May 13, 2026 23:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds configurable role extraction for Custom JWT authentication so DAB can map provider-specific role claims into its standard roles flow for REST/GraphQL authorization and claims policies.

Changes:

  • Adds rolesPath / rolesFormat config model, schema, CLI, and validation support.
  • Adds Custom JWT role extraction and normalization during bearer token validation.
  • Adds unit tests covering extraction formats, authorization behavior, and config validation.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Service/Startup.cs Registers the Custom JWT scheme and hot-reload JWT options.
src/Service/JwtBearerOptionsChangeTokenSource.cs Supports named JWT bearer option reload tokens.
src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs Adds validation tests for Custom JWT role settings.
src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs Adds authentication/authorization tests for extracted roles.
src/Service.Tests/Authentication/CustomJwtRoleClaimExtractorUnitTests.cs Adds unit coverage for role path and format parsing.
src/Core/Configurations/RuntimeConfigValidator.cs Validates role path/format configuration.
src/Core/AuthenticationHelpers/CustomJwtRoleClaimExtractor.cs Implements role extraction, parsing, normalization, and claim replacement.
src/Core/AuthenticationHelpers/ConfigureJwtBearerOptions.cs Wires extraction into dynamic JWT bearer configuration.
src/Config/ObjectModel/JwtOptions.cs Adds role path/format JWT config fields and defaults.
src/Config/ObjectModel/AuthenticationOptions.cs Adds Custom provider helper.
src/Cli/ConfigGenerator.cs Adds init/configure handling for new JWT role settings.
src/Cli/Commands/InitOptions.cs Adds init CLI flags for roles path/format.
src/Cli/Commands/ConfigureOptions.cs Adds configure CLI flags for roles path/format.
schemas/dab.draft.schema.json Adds schema entries for JWT role settings.
Comments suppressed due to low confidence (1)

src/Cli/ConfigGenerator.cs:1807

  • The configure path writes rolesFormat directly without validating that it is one of the supported formats or that the provider is Custom. This lets dab configure --runtime.host.authentication.jwt.rolesFormat ... create a config that will fail runtime validation later; validate these values before updating the file.
                // Runtime.Host.Authentication.Jwt.RolesFormat
                updatedValue = options?.RuntimeHostAuthenticationJwtRolesFormat;
                if (updatedValue != null)
                {
                    JwtOptions jwtOptions;
                    AuthenticationOptions AuthOptions;
                    if (updatedHostOptions?.Authentication == null || updatedHostOptions.Authentication?.Jwt == null)
                    {
                        jwtOptions = new(null, null, RolesFormat: (string)updatedValue);
                    }
                    else
                    {
                        jwtOptions = updatedHostOptions.Authentication.Jwt with { RolesFormat = (string)updatedValue };
                    }

Comment on lines +1766 to +1779
// Runtime.Host.Authentication.Jwt.RolesPath
updatedValue = options?.RuntimeHostAuthenticationJwtRolesPath;
if (updatedValue != null)
{
JwtOptions jwtOptions;
AuthenticationOptions AuthOptions;
if (updatedHostOptions?.Authentication == null || updatedHostOptions.Authentication?.Jwt == null)
{
jwtOptions = new(null, null, RolesPath: (string)updatedValue);
}
else
{
jwtOptions = updatedHostOptions.Authentication.Jwt with { RolesPath = (string)updatedValue };
}
public const string CUSTOM_JWT_ROLE_SETTINGS_PROVIDER_ERROR = "jwt.rolesPath and jwt.rolesFormat are only supported when authentication.provider is Custom.";

public static bool IsValidRolesPath(string rolesPath)
{
Comment thread schemas/dab.draft.schema.json Outdated
Comment on lines +461 to +475
"rolesPath": {
"type": "string",
"description": "Path to the roles claim in the raw JWT payload JSON. Supported for Custom authentication.",
"default": "roles"
},
"rolesFormat": {
"type": "string",
"description": "Format used to parse the roles claim. Supported for Custom authentication.",
"enum": [
"array",
"string",
"space-delimited",
"comma-delimited"
],
"default": "array"
@JerryNixon
Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Copilot AI and others added 2 commits May 14, 2026 15:40
Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/0569f33b-b1aa-4b03-b259-bd56b4753cec

Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/0569f33b-b1aa-4b03-b259-bd56b4753cec

Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 14, 2026

@copilot apply changes based on the comments in this thread

Applied the review feedback in f2b321e: dab configure now validates Custom JWT role settings before writing, blank rolesPath values are rejected, and the schema no longer advertises shared defaults for non-Custom providers.

@JerryNixon
Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Also Gaps / concerns

  1. JSON schema omits the spec’s defaults
    Issue [Enh]: Custom JWT paths for Claims #3078 explicitly specifies schema defaults for:

rolesPath: "roles"
rolesFormat: "array"
The schema adds both properties and the enum, but no default values are present: schemas/dab.draft.schema.json:461-474.

This may be intentional to satisfy the review-bot comment about not advertising shared defaults for non-Custom providers, but strictly against the issue spec, schema defaults are missing.

  1. Test coverage is good but not all 30 spec cases are explicitly covered
    Covered well:

Defaults, omitted path/format, nested path, URL, colon, bracket literal, array/string/space/comma, empty array/string, non-string array, object/number/boolean/null, missing claim, duplicates, trimming, empty normalized roles: src/Service.Tests/Authentication/CustomJwtRoleClaimExtractorUnitTests.cs:15-76
X-MS-API-ROLE selected/absent, IsInRole, @claims.roles, 401 on missing/wrong type: src/Service.Tests/Authentication/JwtTokenAuthenticationUnitTests.cs:116-228
Provider/validation failures: src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs:92-175
CLI configure validation: src/Cli.Tests/ConfigureOptionsTests.cs:747-788
Not explicitly covered from the 30/spec edge cases:

rolesFormat: "string" with a value containing spaces/commas to prove it does not split.
space-delimited with no whitespace and comma-delimited with no comma.
Bracket literal for URL key $['https://example.com/roles'].
Exact logging message/level assertions.
Explicit “header omitted and no extracted roles” behavior.
Explicit “existing default behavior unchanged when rolesPath/rolesFormat omitted” for non-Custom/AzureAD/EntraID.
3. Potential X-MS-API-ROLE spec edge: system role can still be granted
For authenticated requests, the middleware adds the system Authenticated role if the selected client role is a system role and the user is not already in it: src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs:142-157.

Authorization then accepts any header value for which User.IsInRole(...) is true: src/Core/Authorization/ClientRoleHeaderAuthorizationMiddleware.cs:63-66.

This means a Custom JWT request with no extracted token role can still use X-MS-API-ROLE: Authenticated. That is probably existing DAB behavior, but it is worth explicitly documenting/testing because the issue says the header “must not grant a role that is absent from the token.”

  1. CLI option naming differs between init and configure
    init uses the prompt/spec-style flags --auth.roles-path and --auth.roles-format: src/Cli/Commands/InitOptions.cs:112-116.

configure uses full config-path camelCase flags instead: --runtime.host.authentication.jwt.rolesPath and --runtime.host.authentication.jwt.rolesFormat: src/Cli/Commands/ConfigureOptions.cs:373-377.

This may match existing configure conventions, but if the intended user-facing requirement was literally dab configure --auth.roles-path, that is not implemented.

Copilot AI and others added 2 commits May 14, 2026 21:33
Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/0680c0c9-a2d8-4982-84e2-67d88eb52b18

Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/0680c0c9-a2d8-4982-84e2-67d88eb52b18

Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 14, 2026

@copilot apply changes based on the comments in this thread

Also Gaps / concerns
...

Applied the follow-up changes in a233d35: schema defaults are now restored conditionally for Custom JWT, additional role parsing/client-role/default-behavior tests were added, and the CLI validation message was generalized. dab configure keeps the existing full config-path option naming convention rather than adding init aliases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enh]: Custom JWT paths for Claims

3 participants