feat(auth): mount device grant under OIDC, forward reauth=1 to IdP (fixes #4476) - #5627
Conversation
Two bug fixes for OIDC deployments:
**Bug 1 — Device grant (RFC 8628) 404 under OIDC**
app.py gated create_device_auth_router on `_source == "accounts"` only.
Changed the mount condition to `_source in ("accounts", "oidc")` — both
providers own a server-minted session cookie so the consent flow works.
create_device_auth_router itself also hard-rejected OIDC; updated it to
use the existing _resolve_signing_config helper (which already handled
both modes) and extract base_url and session_cookie_name from whichever
config object is present.
**Bug 2 — /auth/login?reauth=1 ignores re-auth demand (anti-phishing bypass)**
The OIDC login handler never read the reauth=1 query param, so the
device-grant consent gate's stale-session bounce (reauth=1) was silently
swallowed. The IdP reused its own session, minted a fresh iat, and the
gate passed without the user re-authenticating.
login() now reads reauth (exact value "1", not applicable to GitHub
OAuth), and when set adds prompt=login + max_age=0 to the IdP
authorization URL (OIDC Core 3.1.2.1).
**Tests**
- test_oidc_login_reauth.py (new): fail→pass regression guard for Bug 2
- test_device_auth.py: updated test_router_factory_rejects_non_accounts_mode
→ test_router_factory_rejects_unsupported_mode[header] (header still
rejected); added test_router_factory_builds_for_oidc_mode (positive test)
Co-authored-by: omnigent <noreply@omnigent.ai>
|
UI Preview for this PR has been removed. |
|
… test create_device_auth_router now raises when provider_type=="github" to prevent mounting the device-grant flow for GitHub OAuth deployments, where the anti-phishing gate cannot be enforced (GitHub does not support prompt=login / max_age=0). Adds: - test_router_factory_rejects_github_oauth_oidc: verifies the rejection - test_github_provider_reauth_sends_no_prompt: verifies login handler skips prompt for GitHub (provider_type exclusion in login()) Co-authored-by: omnigent <noreply@omnigent.ai>
|
🤖 resolve-agent follow-up: Polly blocking fixPolly (round 2) found a real blocking issue on the current PR head (
Fix is committed locally as Changes (36 tests pass):
Patch (git apply this onto
|
app.py widened the device-grant mount to _source in ("accounts","oidc")
but did not exclude GitHub OAuth (provider_type=="github"). The router
constructor raises RuntimeError for GitHub (prompt=login unsupported) —
so any GitHub OIDC deployment with OMNIGENT_DEVICE_GRANT_ENABLED=1 crashed
at server startup after the prior commit.
Fix: add _is_github_oidc guard before calling create_device_auth_router
in app.py. GitHub OIDC silently falls through to the token-only router
(matching the graceful degradation already in login()) and logs a warning
so an operator knows the flag was set but the device flow is unavailable.
Also update the stale module docstring in device_auth.py (still said
"accounts-only") and add test_app_skips_device_grant_for_github_oidc_-
without_crashing to pin the boot-safety invariant.
Co-authored-by: omnigent <noreply@omnigent.ai>
|
|
✅ This PR no longer requires demo follow-up. |
Restore the two # noqa: BLE001 comments accidentally dropped on the opportunistic-purge and acp-seeding except blocks, and let ruff reformat the reauth guard expression onto one line. Co-authored-by: Isaac <no-reply@databricks.com>
|
/review |
|
The if not _is_github_oidc branch was dead (the assert above guarantees it is True), which the code-quality bot flagged as unreachable. Assert the factory would raise for GitHub, matching the reason app.py skips the mount, without the impossible conditional. Co-authored-by: Isaac <no-reply@databricks.com>
The device-consent anti-phishing gate demanded a fresh login via reauth=1, and /login forwarded prompt=login + max_age=0 to the IdP. But the callback trusted the IdP to honor it: it minted a fresh-iat session regardless, so a non-conformant IdP that silently reused its session still sailed past the consent freshness gate — the exact reflex-approve bypass, moved one layer down. Close it RP-side (OIDC Core 3.1.3.7): /login now stamps reauth_at into the signed state cookie, and /callback rejects the login unless the id_token's auth_time is at or after that demand (or is absent). Factor the id_token validation into a shared _validate_id_token helper so email and auth_time resolve from the same signature check. Co-authored-by: Isaac <no-reply@databricks.com>
|
/review |
|
|
🏷️ Doc impact: This is an internal security hardening of the opt-in, default-off device-grant/OIDC re-auth flow (prompt=login/max_age=0 auth_time verification), which doesn't change any documented user-facing behavior, config key, or integration surface. Auto-classified on merge. Set the label manually before merging to override. · run |
Related issue
Closes #4476
Summary
Two bug fixes for OIDC deployments (OMNI-2821):
POST /oauth/device/authorizereturned 404 under OIDC —app.pygatedcreate_device_auth_routeron_source == "accounts"only. Changed to_source in ("accounts", "oidc")— both providers own a server-minted session cookie, so the in-browser consent flow works for OIDC too. Updatedcreate_device_auth_routeritself (which also hard-rejected OIDC) to use the existing_resolve_signing_confighelper that already handled both modes, and to extractbase_url/session_cookie_namefrom whichever config object is present./auth/login?reauth=1ignored re-auth demand (anti-phishing gate bypass) — The OIDC login handler never readreauth=1, so the device-grant consent gate's stale-session bounce (→reauth=1) was swallowed: the IdP reused its session, minted a freshiat, and the gate passed without the user re-authenticating.login()now readsreauth(exact value"1", not applicable to GitHub OAuth) and addsprompt=login+max_age=0to the IdP authorization URL (OIDC Core §3.1.2.1).Test Plan
Fail→pass regression guard for Bug 2 (
tests/server/test_oidc_login_reauth.py, new):test_login_with_reauth_forces_prompt_login_at_idp— FAILS on unfixed main (assert None == ['login']), PASSES with fixtest_login_without_reauth_sends_no_prompt— passes both ways (baseline: plain login must not addprompt)Unit tests for Bug 1 (
tests/server/test_device_auth.py, updated):test_router_factory_rejects_unsupported_mode[header]— header mode still rejected (was[oidc, header])test_router_factory_builds_for_oidc_mode(new) — OIDC mode now accepted, router mounts cleanlyAll 33 device_auth + 113 OIDC tests green locally.
Demo
N/A — backend auth fix; no UI change. The regression guard
test_oidc_login_reauth.pyis the machine-verifiable before/after proof.Live validation (once deployed):
Type of change
Test coverage
test_oidc_login_reauth.py+ newtest_router_factory_builds_for_oidc_mode)test_device_auth.py)Coverage notes
Existing 204-test
servershard covers the surrounding OIDC and device-grant paths. The two new tests directly prove the fail→pass transition for each bug facet.Supersedes #5623 (closed) — that PR was based on a stale fork rebase that accidentally reverted unrelated merged features. This PR contains only the minimal surgical changes.
Builds on #4304 (@appletechie) — the underlying approach for extending
create_device_auth_routerto OIDC originated there.