fix(auth_n): try refresh oidc client on token exchange failure#1088
fix(auth_n): try refresh oidc client on token exchange failure#1088ayushjain17 wants to merge 2 commits into
Conversation
Changed Files |
WalkthroughOIDC authentication now uses refreshable clients and direct ID-token verification. Failed verification triggers one metadata/client refresh and retry. Token-based authentication is supported across authenticators, and middleware authentication control flow is consolidated around a single future. ChangesOIDC authentication refresh and verification
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant OidcLogin
participant OIDCAuthenticator
participant OidcProvider
Client->>OidcLogin: authorization code
OidcLogin->>OidcProvider: exchange code for tokens
OidcLogin->>OidcLogin: verify_id_token(current client)
alt verification fails
OidcLogin->>OIDCAuthenticator: refresh_client()
OIDCAuthenticator->>OidcProvider: fetch provider metadata
OIDCAuthenticator->>OIDCAuthenticator: build_client(metadata)
OidcLogin->>OidcLogin: verify_id_token(refreshed client)
OidcLogin-->>Client: authenticated result or Unauthorized sign-in failure
else verification succeeds
OidcLogin-->>Client: authenticated result
end
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR updates the OIDC authentication flow to better tolerate IdP metadata/JWKS changes by allowing the OIDC client (and, for SaaS, provider metadata) to be refreshed at runtime and retrying ID-token verification once after a refresh. It also changes the terminal failure behavior to return an explicit “Sign-in failed” response instead of redirecting again (avoiding redirect loops).
Changes:
- Add shared helpers to (re)discover provider metadata and rebuild a
CoreClient. - Store OIDC clients (and SaaS provider metadata) behind locks so they can be swapped on refresh.
- On ID-token verification failure, refresh provider metadata/client once and retry verification; on repeated failure, return an unauthorized error page.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| crates/service_utils/src/middlewares/auth_n/oidc/utils.rs | Adds provider metadata discovery and CoreClient construction helpers for reuse and refresh. |
| crates/service_utils/src/middlewares/auth_n/oidc/simple_authenticator.rs | Switches to a refreshable, lock-protected CoreClient and implements refresh_client(). |
| crates/service_utils/src/middlewares/auth_n/oidc/saas_authenticator.rs | Switches to refreshable, lock-protected CoreClient + provider metadata and implements refresh_client(). |
| crates/service_utils/src/middlewares/auth_n/oidc.rs | Extends the OIDC authenticator trait with refresh, retries ID-token verification after refresh, and adds a terminal failure response. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Verify the freshly-minted ID token. If verification fails, the most | ||
| // common cause is that the IdP has rotated its signing keys since we | ||
| // last fetched the JWKS, so the cached keys can't validate the new | ||
| // token's signature. Refresh the provider metadata once and retry | ||
| // before giving up — this self-heals key rotation without a restart. | ||
| let mut response = | ||
| verify_id_token(&data.get_client(), &token_response, &p_cookie.nonce); |
There was a problem hiding this comment.
if no id token is there, then shouldn't the person login to get the info
| fn get_client(&self) -> CoreClient { | ||
| self.client | ||
| .read() | ||
| .unwrap_or_else(|e| e.into_inner()) | ||
| .clone() | ||
| } |
| fn get_client(&self) -> CoreClient { | ||
| self.client | ||
| .read() | ||
| .unwrap_or_else(|e| e.into_inner()) | ||
| .clone() | ||
| } |
| // Verify the freshly-minted ID token. If verification fails, the most | ||
| // common cause is that the IdP has rotated its signing keys since we | ||
| // last fetched the JWKS, so the cached keys can't validate the new | ||
| // token's signature. Refresh the provider metadata once and retry | ||
| // before giving up — this self-heals key rotation without a restart. | ||
| let mut response = | ||
| verify_id_token(&data.get_client(), &token_response, &p_cookie.nonce); |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
crates/service_utils/src/middlewares/auth_n/oidc/saas_authenticator.rs (1)
232-253: 🚀 Performance & Scalability | 🔵 TrivialConsider de-duplicating concurrent refreshes to avoid a discovery-fetch storm during key rotation.
refresh_clientis invoked from the login handler on every ID-token verification failure. During a JWKS-rotation window, multiple in-flight logins will each fail verification and independently callrefresh_client, issuing N concurrent discovery fetches against the IdP for what is effectively the same refresh. A single-flight guard (collapse concurrent refreshes into one) or a short minimum interval between refreshes would bound the load on the IdP discovery endpoint while preserving the self-heal behavior.The two-step swap (metadata then client under separate write locks) is fine for the retry path, since both writes are in-memory and no lock is held across the
await.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/service_utils/src/middlewares/auth_n/oidc/saas_authenticator.rs` around lines 232 - 253, `refresh_client` in `SaasAuthenticator` is doing redundant concurrent discovery fetches when multiple login attempts fail at the same time during key rotation. Add a single-flight or other coalescing guard around `refresh_client` so only one refresh runs per issuer at a time, and have other callers reuse or await that in-flight refresh instead of calling `fetch_provider_metadata` repeatedly. Keep the existing two-step swap into `provider_metadata` and `client` as-is, but make the refresh entrypoint resilient to concurrent invocations from the login handler.crates/service_utils/src/middlewares/auth_n/oidc.rs (1)
208-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the redundant
.map_err(|e| e)
claims(...)already returnsClaimsVerificationError, so the identitymap_erradds no value and may triggerclippy::map_identityunder-D warnings.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/service_utils/src/middlewares/auth_n/oidc.rs` at line 208, The `claims(...)` call in `Oidc` token verification is wrapped with a redundant `.map_err(|e| e)` identity conversion. Remove that `map_err` from the `and_then` chain so the result from `client.id_token_verifier()` and `claims(...)` is passed through directly without triggering the identity-map warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/service_utils/src/middlewares/auth_n/oidc.rs`:
- Line 208: The `claims(...)` call in `Oidc` token verification is wrapped with
a redundant `.map_err(|e| e)` identity conversion. Remove that `map_err` from
the `and_then` chain so the result from `client.id_token_verifier()` and
`claims(...)` is passed through directly without triggering the identity-map
warning.
In `@crates/service_utils/src/middlewares/auth_n/oidc/saas_authenticator.rs`:
- Around line 232-253: `refresh_client` in `SaasAuthenticator` is doing
redundant concurrent discovery fetches when multiple login attempts fail at the
same time during key rotation. Add a single-flight or other coalescing guard
around `refresh_client` so only one refresh runs per issuer at a time, and have
other callers reuse or await that in-flight refresh instead of calling
`fetch_provider_metadata` repeatedly. Keep the existing two-step swap into
`provider_metadata` and `client` as-is, but make the refresh entrypoint
resilient to concurrent invocations from the login handler.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d2124887-1eaa-46d6-a65a-b70094563ab2
📒 Files selected for processing (4)
crates/service_utils/src/middlewares/auth_n/oidc.rscrates/service_utils/src/middlewares/auth_n/oidc/saas_authenticator.rscrates/service_utils/src/middlewares/auth_n/oidc/simple_authenticator.rscrates/service_utils/src/middlewares/auth_n/oidc/utils.rs
fdc2937 to
af596e7
Compare
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/service_utils/src/middlewares/auth_n/oidc.rs`:
- Around line 141-161: The retry in the ID token verification flow still uses
the pre-refresh client and stale JWKS. Update verify_id_token to borrow only
token_response from the token response while accepting the client independently,
then re-fetch the client via data.get_client() after a successful
data.refresh_client() before retrying verification.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dc09bdfe-6081-454f-8df0-aef64fed821e
📒 Files selected for processing (8)
crates/service_utils/src/middlewares/auth_n.rscrates/service_utils/src/middlewares/auth_n/authentication.rscrates/service_utils/src/middlewares/auth_n/no_auth.rscrates/service_utils/src/middlewares/auth_n/oidc.rscrates/service_utils/src/middlewares/auth_n/oidc/saas_authenticator.rscrates/service_utils/src/middlewares/auth_n/oidc/simple_authenticator.rscrates/service_utils/src/middlewares/auth_n/oidc/types.rscrates/service_utils/src/middlewares/auth_n/oidc/utils.rs
| let client = data.get_client(); | ||
|
|
||
| // Verify the freshly-minted ID token. If verification fails, the most | ||
| // common cause is that the IdP has rotated its signing keys since we | ||
| // last fetched the JWKS, so the cached keys can't validate the new | ||
| // token's signature. Refresh the provider metadata once and retry | ||
| // before giving up — this self-heals key rotation without a restart. | ||
| let mut response = verify_id_token(&client, &token_response, &p_cookie.nonce); | ||
| if let Err(e) = &response { | ||
| log::error!( | ||
| "OIDC: ID token verification failed; refreshing provider keys and retrying, error: {e:?}" | ||
| ); | ||
| match data.refresh_client().await { | ||
| Ok(()) => { | ||
| response = verify_id_token(&client, &token_response, &p_cookie.nonce); | ||
| } | ||
| Err(e) => { | ||
| log::error!("OIDC: failed to refresh provider metadata: {e}") | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file around the relevant section
sed -n '1,260p' crates/service_utils/src/middlewares/auth_n/oidc.rs
echo '--- search get_client/refresh_client definitions ---'
rg -n "fn get_client|fn refresh_client|struct .*CoreClient|verify_id_token\\(" crates/service_utils/src -SRepository: juspay/superposition
Length of output: 9310
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for f in \
crates/service_utils/src/middlewares/auth_n/oidc/simple_authenticator.rs \
crates/service_utils/src/middlewares/auth_n/oidc/saas_authenticator.rs
do
echo "===== $f ====="
sed -n '1,320p' "$f"
doneRepository: juspay/superposition
Length of output: 18843
Re-read the client before retrying
crates/service_utils/src/middlewares/auth_n/oidc.rs:141-161 — let client = data.get_client(); captures the pre-refresh CoreClient, so the retry still validates against stale JWKS and won’t recover after key rotation. Re-fetch the client after refresh_client(), and relax verify_id_token so only token_response is tied to the returned token reference.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/service_utils/src/middlewares/auth_n/oidc.rs` around lines 141 - 161,
The retry in the ID token verification flow still uses the pre-refresh client
and stale JWKS. Update verify_id_token to borrow only token_response from the
token response while accepting the client independently, then re-fetch the
client via data.get_client() after a successful data.refresh_client() before
retrying verification.
Change log
Note: The refresh would keep happening on every fail token exchange if a successful refresh was not achieved. token exchange re-try is only done once, re-try failure will lead to a terminal state in the user login experience avoiding the infinite redirects.
auth cookieto only contain onlyid_tokenid_tokenasBearer tokenvalueSummary by CodeRabbit
Summary by CodeRabbit