feat(oidc): emit bunyip_role claim in at+jwt for resource servers (BUNYIP-66) #81

Merged
nrupard merged 2 commits from feat/bunyip-66-emit-bunyip-role-claim into main 2026-06-09 18:08:27 +02:00
Owner

Problem

Bunyip's at+jwt (RFC 9068) did not carry the authenticated user's Bunyip system role. AtClaims had no role claim and /oauth2/userinfo did not return it either, so resource servers (mokosh-server on its RS path) could not derive authorization from the verified token and fell back to a local default: a Bunyip admin was seen as a low-privilege user.

Change

  • Add pub bunyip_role: String to AtClaims, populated on every mint_access_token from the user's role (the DB column, already constrained to UserRole::as_str() values: subscriber | admin).
  • Identity-level: emitted regardless of requested scope, NOT gated behind an OAuth scope. Scopes carry per-client app permissions (allowed_scopes), not the user's system role; consumers map bunyip_role to their own taxonomy (PMS-172), Bunyip exposes only its own role.
  • Refactor the claim assembly out of mint_access_token into a pure AtClaims::build so the mapping is unit-testable without keys or a database.
  • Advertise bunyip_role in the discovery document's claims_supported.

Tests

crates/bunyip-oidc/src/services/oidc_provider.rs:

  • admin user mints bunyip_role = "admin"
  • subscriber mints bunyip_role = "subscriber"
  • claim is present with empty scope (identity-level)
  • claim serializes under the literal key bunyip_role the mokosh RS parses

Verification

just check-container green: fmt, clippy -D warnings, full workspace tests (4 new tests pass).

Coordination

The claim name bunyip_role matches what the mokosh consumer parses; mokosh-side translation is tracked in PMS-172 (linked depend).

Closes BUNYIP-66.

## Problem Bunyip's `at+jwt` (RFC 9068) did not carry the authenticated user's Bunyip system role. `AtClaims` had no role claim and `/oauth2/userinfo` did not return it either, so resource servers (mokosh-server on its RS path) could not derive authorization from the verified token and fell back to a local default: a Bunyip `admin` was seen as a low-privilege user. ## Change - Add `pub bunyip_role: String` to `AtClaims`, populated on every `mint_access_token` from the user's role (the DB column, already constrained to `UserRole::as_str()` values: `subscriber` | `admin`). - Identity-level: emitted regardless of requested scope, NOT gated behind an OAuth scope. Scopes carry per-client app permissions (`allowed_scopes`), not the user's system role; consumers map `bunyip_role` to their own taxonomy (PMS-172), Bunyip exposes only its own role. - Refactor the claim assembly out of `mint_access_token` into a pure `AtClaims::build` so the mapping is unit-testable without keys or a database. - Advertise `bunyip_role` in the discovery document's `claims_supported`. ## Tests `crates/bunyip-oidc/src/services/oidc_provider.rs`: - admin user mints `bunyip_role = "admin"` - subscriber mints `bunyip_role = "subscriber"` - claim is present with empty scope (identity-level) - claim serializes under the literal key `bunyip_role` the mokosh RS parses ## Verification `just check-container` green: fmt, clippy `-D warnings`, full workspace tests (4 new tests pass). ## Coordination The claim name `bunyip_role` matches what the mokosh consumer parses; mokosh-side translation is tracked in PMS-172 (linked depend). Closes BUNYIP-66.
feat(oidc): emit bunyip_role claim in at+jwt for resource servers (BUNYIP-66)
All checks were successful
Check / fmt / clippy / build / test (pull_request) Successful in 1m1s
9a703a32c6
Bunyip's RFC 9068 access token did not carry the authenticated user's Bunyip system role, so resource servers (mokosh-server on its RS path) could not derive authorization from the verified token and fell back to a local default, seeing a Bunyip admin as a low-privilege user.

Add a first-class bunyip_role claim to AtClaims, populated on every mint from the user's role (the DB column, already constrained to UserRole::as_str() values: subscriber | admin). The claim is identity-level, emitted regardless of requested scope, not gated behind an OAuth scope (scopes carry per-client app permissions, not the user's system role). Consumers map bunyip_role to their own taxonomy; Bunyip exposes only its own role.

Refactor the claim assembly out of mint_access_token into a pure AtClaims::build so the claim mapping is unit-testable without keys or a database, and advertise bunyip_role in the discovery document's claims_supported. Tests assert an admin mints bunyip_role = "admin", a subscriber mints "subscriber", the claim is present with empty scope, and it serializes under the literal key the mokosh RS parses (PMS-172).

#BUNYIP-66

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(oidc): default bunyip_role on deserialize for rolling-deploy compat (BUNYIP-66)
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 1m31s
bed0c4782a
AtClaims is also the deserialize target in verify_at_jwt_claims, so a required bunyip_role field with no serde default would reject every at+jwt minted by a pre-BUNYIP-66 build during a rolling deploy: decode fails with "missing field bunyip_role" -> OidcInvalidToken -> 401 until the old 600s-TTL tokens expire. Add #[serde(default)] so verifying a legacy token yields an empty role rather than a hard failure; mint always populates the claim, so emission is unchanged. Add a regression test deserializing a claim set with no bunyip_role.

#BUNYIP-66

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/bunyip-66-emit-bunyip-role-claim 2026-06-09 18:08:27 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!81
No description provided.