fix(auth): let an Auth0 RBAC admin role reach the token - #385
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes Auth0-admin authorization failures by adding support for Auth0 RBAC permissions[] as a role carrier in JWT validation, and by optionally plumbing a deploy-time Auth0 scope (DECPM_AUTH0_SCOPE) from CLI/config → backend /auth-config → frontend Auth0 SDK initialization. It also updates the deployment guide to document the three supported Auth0 “admin role” carriers.
Changes:
- Extend JWT role extraction to read Auth0 RBAC
permissions(in addition to existingroles,realm_access.roles, andscopecarriers), with tests. - Add
DECPM_AUTH0_SCOPEthrough config +/auth-config, and update the SPA to append it while preserving Auth0’s defaultopenid profile email. - Update deployment documentation to explain the three carrier options and troubleshooting guidance.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/DEPLOYMENT_GUIDE.md | Documents Auth0 admin-role carrier options (permissions, scope, custom claim) and adds DECPM_AUTH0_SCOPE. |
| crates/decman/src/server/handlers/auth.rs | Extends /auth-config response with auth0_scope and adds handler tests. |
| crates/decman/src/main.rs | Wires CLI/env DECPM_AUTH0_SCOPE into Auth0Config and warns when scope is set without Auth0 config. |
| crates/decman/src/config.rs | Adds scope: Option<String> to Auth0Config with documentation and updates config tests. |
| crates/decman/src/cli.rs | Introduces hidden CLI/env option DECPM_AUTH0_SCOPE. |
| crates/decman/src/auth/validators/jwt.rs | Reads Auth0 permissions[] into role collection and adds targeted JWT validator tests. |
| crates/decman/frontend/src/main.tsx | If auth0_scope is present, requests openid profile email plus the configured scopes (avoiding auth0-spa-js default override). |
| crates/common/src/types.rs | Adds auth0_scope to AuthConfigResponse (omitted when None). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/decman/src/auth/validators/jwt.rs:268
- Restrict this carrier to the configured Auth0 API.
find_trustedcurrently records onlyclient_id/discovery data and JWT validation disables audience checking, so this block promotespermissionsfor every trusted issuer. The new test even uses a Keycloak validator. Consequently, a Keycloak token—or an Auth0 token for another API in the same tenant and SPA—with a matching permission can satisfyDECPM_ADMIN_ROLE. Preserve the matched provider and expected Auth0 audience, enforce that audience, and only mergepermissionsfor that Auth0 match.
if let Some(value) = claims.additional.get("permissions")
&& let Ok(permissions) = serde_json::from_value::<Vec<String>>(value.clone())
{
flat_roles.extend(permissions);
Auth0 returns a permission in `scope` only if the client requested it, so an RBAC-assigned admin role never reached `require_admin`. The `permissions` array that `access_token_authz` emits is not filtered that way, so reading it lets the role arrive with no post-login Action. Read from the flattened claim map rather than a typed field: a typed `Option<Vec<String>>` would fail the whole token decode on a provider that emits a different shape under that name, locking out every user of an otherwise healthy node.
The SPA sent no `scope`, so an admin role granted as a resource-server scope could not reach the token whatever the tenant was configured to do. DECPM_AUTH0_SCOPE now travels to the browser over /auth-config. auth0-spa-js shallow-merges authorizationParams over its own defaults, so a supplied `scope` replaces "openid profile email" outright. The frontend re-states the default; without that, setting the variable would strip the `email` claim `Principal` depends on. Warn when the variable is set without DECPM_AUTH0_DOMAIN/CLIENT_ID, mirroring DECPM_KEYCLOAK_INTERNAL_URL — a knob that is silently ignored is how this class of problem costs operators an afternoon.
The guide said an Action was mandatory. RBAC permissions need none, so lead with that and keep the scope and namespaced-claim paths as alternatives. Point the 403 entry at the token's actual carriers.
Reading `permissions` for every trusted issuer let any provider that emits a top-level `permissions` array satisfy DECPM_ADMIN_ROLE through it, including a Keycloak realm with a custom mapper. `find_trusted` now reports which provider matched and only the Auth0 arms enable the carrier. The carrier moves into a pure `auth0_permission_roles` so it can be tested directly: `auth0_issuer_of` hardcodes https, so the Auth0 arm of `find_trusted` is not reachable through the wiremock harness.
65de142 to
0f0ffea
Compare
Summary
With
DECPM_ADMIN_ROLEset on an Auth0 node, no user could passrequire_admin, soPOST /network-config,/kickand post-bootstrapPUT /party-configwere unreachable. Auth0 returns a permission inscopeonly when the client requested it, and the SPA requested nothing beyond the defaultopenid profile email.Two carriers, so an operator can pick per tenant:
permissions— read the array Auth0 emits underaccess_token_authz. Auth0 does not filter it by requested scope, so an RBAC-assigned role arrives with no post-login Action. This is the path that needs no Auth0 Action at all.DECPM_AUTH0_SCOPE— extra scopes plumbedcli.rs→Auth0Config→/auth-config→authorizationParams.scope, for tenants that grant the role as a resource-server scope.DECPM_JWT_ROLE_CLAIM(#378) already covers the namespaced-custom-claim path. The three are complementary; the docs now present all three and lead with the one that needs no Action.Related issues
Closes #383
Type of change
Checklist
cargo fmt -- --checkpassescargo clippy --all-targets --all-features -- -D warningsis cleancargo testpasses<type>(<scope>): <subject>conventionNotes for reviewers
Two decisions worth a look.
permissionsis read from the flattened claim map, not a typed field. A typedOption<Vec<String>>would fail the wholedecode::<Claims>on any provider that emits a different shape under that name, turning a cosmetic mismatch into a total lockout of a healthy node.malformed_permissions_claim_still_verifiespins that.The frontend re-states
openid profile emailwhen a scope is set. auth0-spa-js 2.19.2 builds options asObject.assign({}, defaultOptions.authorizationParams, options.authorizationParams), a shallow merge, so a suppliedscopereplaces the default outright — its own typings say so. A plain passthrough would have silently stripped theemailclaimPrincipalreads.cargo testis unchecked because I did not run the suite locally; clippy--all-targetscompiles the new tests, and CI will execute them. Five added: permissions grants the role, malformed permissions still verifies, permissions/scope dedupe, and/auth-configsurfacing vs omitting the scope.The reporter on #383 offered to test whichever shape we picked, so worth a devnet image for them.