Add Custom JWT role path and format support#3549
Conversation
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>
There was a problem hiding this comment.
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/rolesFormatconfig 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 };
}
| // 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) | ||
| { |
| "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" |
|
@copilot apply changes based on the comments in this thread |
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>
Applied the review feedback in f2b321e: |
|
@copilot apply changes based on the comments in this thread Also Gaps / concerns
rolesPath: "roles" 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.
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 rolesFormat: "string" with a value containing spaces/commas to prove it does not split. 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.”
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. |
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>
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. |
Why make this change?
Custom JWT providers often place roles outside DAB’s expected top-level
rolesarray. 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
runtime.host.authentication.jwt.rolesPathruntime.host.authentication.jwt.rolesFormatrolesPath: "roles"androlesFormat: "array"forCustomJWT authentication.rolesPathvalues.Custom JWT role extraction
array,string,space-delimited, andcomma-delimitedformats."roles"claims.Validation and runtime behavior
rolesPath/rolesFormatfor non-Customproviders.dab configurebefore writing config changes.X-MS-API-ROLE,User.IsInRole(),@claims.roles, and systemAuthenticatedrole behavior.Schema and CLI
rolesPathandrolesFormatonly when the provider isCustom.CustomJWT configs from usingrolesPath/rolesFormat.How was this tested?
Targeted authentication/configuration/CLI coverage was added for role path resolution, role formats, invalid inputs, startup validation,
dab configurevalidation,X-MS-API-ROLE,User.IsInRole(),@claims.roles, schema-default behavior, exact logging for missing claims, and non-Customprovider default behavior.Validation run:
dotnet format src/Azure.DataApiBuilder.slndotnet build src/Azure.DataApiBuilder.sln --no-restoreSample 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