Skip to content

feat(organizations): carry organization roles and permissions in the access token - #577

Merged
Bccorb merged 4 commits into
mainfrom
feat/organization-roles-in-token
Nov 6, 2026
Merged

Bccorb merged 4 commits into
mainfrom
feat/organization-roles-in-token

Conversation

@Bccorb

@Bccorb Bccorb commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Closes #416. Stacked on the #415 branch, so this diff shows only the organization work. Merge that first; this then retargets to main. Design in docs/adr/0004-permissions-and-organization-roles.md.

What

  • Token claims. org_roles and org_permissions beside org_id, for the active organization only. Organization roles never appear in roles, so a guard on admin does not pass for an organization admin. Absent with no active organization, and on recovery tokens.
  • Typed roles. New organization_roles catalogue, managed at /admin/organization-roles, with PUT /admin/roles/organization/:roleName/permissions for mappings. The migration seeds the built-in owner, admin and member and every value memberships already hold, spelled as they are, and rewrites no membership. Built-ins and roles still held cannot be deleted.
  • Live reads. GET /users/me and GET /users/me/permissions report the same organization data, checked against the membership on each call.
  • Overflow. org_roles counts against the 1 KiB budget, and permissions_omitted replaces all three arrays. 50 long role names could overflow the cookie by themselves.
  • Docs. docs/admin-operations.md and docs/api-contract.md.

Security fixes, each reproduced on the code before the fix

  • Removal did not reach the token. Refresh copied org_id from the session, so a removed member kept it until the session ended, and with this PR would have kept the roles and permissions too. The mint now reads the membership, drops the claims and clears the session's organization. The test failed with the organization id where it expected null.
  • An organization admin could grant owner. The member PATCH had no step-up and no audit. The test got 200 where it expected 403. Now only an owner or an instance admin:write can grant or remove owner, or remove an owner from the organization, and any other manager can only grant a role whose permissions they hold.

Behavior changes for the release notes

  • Assigning an organization role outside the catalogue answers 400. Existing legacy roles on a member are kept.
  • PATCH /organizations/:id/members/:userId needs step-up, including for a change to scopes alone.
  • Add, update and remove are audited as organization_member_added, organization_member_updated and organization_member_removed, and a refusal as organization_member_role_grant_refused.

For review

  • Removing an owner goes slightly beyond the agreed rule, which named granting and removing the role. Removal takes the role too, so the same check applies.
  • Four cases in tests/postgres/sessionExchange.spec.ts now step up first, because they assumed the member PATCH had no step-up. The device-session gate they test is unchanged.
  • An owner is exempt from the "hold the permissions" rule, because what an owner holds depends on how the role happens to be mapped.
  • Organization managers have no route to list assignable roles, since GET /admin/roles is admin-only. Left out as not yet needed.
  • Membership scopes stay out of the token. Provider tokens are not covered here; seamless-auth-oidc-provider#47 takes that up in Wave 4.

Testing

Run locally on this branch with @seamless-auth/types 0.38.0:

  • Unit and Postgres projects on postgres:18-alpine: 3,264 passed, 1 todo. The Postgres spec runs the migration against a legacy membership and mints a real token before and after removal
  • Unit coverage: 99.05% statements, 95.58% branches
  • Typecheck, lint, format and build pass

Not run: the conformance harness. Adapter guards arrive with step 3 of Wave 3, and they wait on review of this claim shape.

Release

Part of the held API v0.23.0 release. #566 stays open. Minor changeset included. New migration: 20261105130000-add-organization-roles.

@Bccorb Bccorb added this to the v0.23 milestone Nov 6, 2026
@Bccorb
Bccorb added this pull request to stack #578 November 6, 2026 06:00
Base automatically changed from feat/permissions-mapped-to-roles to main November 6, 2026 06:04
…access token

Adds org_roles and org_permissions beside org_id for the active organization,
and a catalogue of organization roles seeded from what memberships already
hold. Assigning a role outside the catalogue answers 400.

Role changes need step-up and are audited. Only an owner or an instance
admin:write can grant or remove owner, and other managers can only grant a
role whose permissions they hold, so an organization admin can no longer make
themselves owner.

Fixes removal from an organization not reaching the token: refresh copied
org_id from the session until it ended. The mint now reads the membership.

Closes #416.
@Bccorb
Bccorb force-pushed the feat/organization-roles-in-token branch from d503ea3 to 2978157 Compare November 6, 2026 06:04
@Bccorb
Bccorb merged commit bed4994 into main Nov 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: organization-scoped roles and permissions in the access token

1 participant