Skip to content

feat(oidc): manage the admin flag from a configurable id_token claim - #2576

Closed
btallman wants to merge 1 commit into
omnigent-ai:mainfrom
airbrx:upstream-oidc-admin-claim
Closed

feat(oidc): manage the admin flag from a configurable id_token claim#2576
btallman wants to merge 1 commit into
omnigent-ai:mainfrom
airbrx:upstream-oidc-admin-claim

Conversation

@btallman

@btallman btallman commented Jul 14, 2026

Copy link
Copy Markdown

Related issue

Closes #2577. Builds on #1846 (OIDC admin surface) and #1859 (email_verified opt-out); originally raised in #1489 (closed).

Summary

  • Manage admins from the OIDC/SSO IdP instead of only the box-local admin-list file. Set OMNIGENT_OIDC_ADMIN_CLAIM (+ optional OMNIGENT_OIDC_ADMIN_VALUE) and that id_token claim becomes authoritative for users.is_admin at every login — matching users are promoted, losing the claim demotes them.
  • Fail-safe: a token without the claim (or a null/undecidable shape) is a no-op, so an IdP that stops sending the claim can never mass-demote existing admins. The file-backed admin list still runs afterward as a break-glass, promote-only override.
  • Handles the common claim shapes: list (groups/roles — any element matches), scalar string (exact match), and boolean (the bool itself).

ELI5: put someone in your omnigent-admins IdP group and they become an Omnigent admin next login; remove them and they stop being one. A local admins file stays as a spare key so you can't lock yourself out.

flowchart LR
  A[id_token verified<br/>sig + iss + aud] --> B{ADMIN_CLAIM set?}
  B -- no --> D[promote_if_listed<br/>break-glass file]
  B -- yes --> C[evaluate claim]
  C -- match --> P[is_admin = true]
  C -- present, no match --> M[is_admin = false]
  C -- absent / null --> N[no-op]
  P --> D
  M --> D
  N --> D
Loading

The claim name and value are both env-driven (nothing IdP-specific is hardcoded): JumpCloud uses memberOf, Okta/Entra/Google typically groups, others roles.

Test Plan

  • pytest tests/server/test_admin_list.py tests/server/test_oidc_callback.py tests/server/test_oidc_open_redirect.py69 passed on a clean main checkout.
  • New coverage: 5 end-to-end callback tests driving real RS256 id_tokens through the production decode path (promote / demote / absent-claim no-op / admin-list break-glass / feature-off inert), plus a verdict-table + sync_admin_claim unit tests.
  • ruff format --check + ruff check clean on all touched files.
  • Verified in production against a JumpCloud-backed deployment: with the app emitting memberOf, a login carrying memberOf=['omnigent-admins', …] promoted a user who was not in the admin file (pure claim path); server logged admin claim 'memberOf' promoted <user> and the DB flag flipped.

Demo

N/A — no UI change (env-configured server behaviour).

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

Manually verified end-to-end against a live JumpCloud OIDC deployment (see Test Plan): confirmed the claim promotes a non-file user, and that memberOf must be emitted in the id_token (the server reads decoded id_token claims, not UserInfo) and only carries app-bound group names.

Changelog

Manage OIDC/SSO admins from an id_token group/role claim via OMNIGENT_OIDC_ADMIN_CLAIM

Admins under OIDC/SSO previously came only from the file-backed admin
list. Now the IdP can be the source of truth: when
OMNIGENT_OIDC_ADMIN_CLAIM is set, that id_token claim is authoritative
for users.is_admin at every login — a matching OMNIGENT_OIDC_ADMIN_VALUE
promotes, a present-but-unmatching claim demotes.

- Claim shapes: list (groups/roles — any element matches), scalar string
  (exact match), boolean (the bool itself; value optional).
- Fail-safe: a token without the claim (or a null/undecidable shape) is a
  no-op, so an IdP that stops emitting the claim can never mass-demote
  existing admins.
- The file-backed admin list (promote_if_listed) still runs after the sync
  as a break-glass, promote-only override.

To read the claims, the callback threads the validated id_token out of the
signature/iss/aud check (_resolve_oidc_email -> _resolve_oidc_identity), so
the sync only ever sees IdP-signed data. GitHub OAuth (no id_token) is
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

@btallman This PR is a Bug fix, Feature, or UI / frontend change but the Demo section is missing or only contains a placeholder.

These change types require a screenshot or screen recording so reviewers can see the new behaviour without checking out the branch. Please update the Demo section with:

  • A screenshot or screen recording of the change, or
  • A link to a hosted video or GIF showing the new behaviour.

Use N/A only when the change has no user-visible effect whatsoever (e.g. a pure refactor or test-only change). If that's the case, uncheck the relevant type box and check Refactor / chore or Test / CI instead.

@github-actions github-actions Bot added the needs-demo PR needs a demo screenshot or recording label Jul 14, 2026
@PattaraS

Copy link
Copy Markdown
Contributor

Thanks — the overall IdP-driven admin design looks good. Before we merge, could you please make the claim-shape handling strict?

Accepted shapes should be:

  • a boolean
  • a string
  • a list/tuple containing only strings

For any other shape — including objects, numbers, or lists containing non-string elements — please log a warning and return None (“no decision”), leaving the existing admin flag unchanged. In particular, avoid stringifying malformed values into an authoritative False verdict, since that could unexpectedly demote an existing admin.

Please add regression tests for malformed scalar values and mixed/non-string lists. Also rebase the PR onto the current master once this is addressed.

@github-actions

Copy link
Copy Markdown
Contributor

Closing this PR because it has been labeled waiting-on-author for 7 days without an author reply or new commit.

The label was last applied on 2026-07-17T05:14:22Z. If you are ready to continue, please reopen this PR or open a new one.

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

Labels

needs-demo PR needs a demo screenshot or recording size/L Pull request size: L waiting-on-author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Manage OIDC/SSO admins from an id_token claim (IdP group/role)

2 participants