feat(auth): surface OIDC callback errors with distinct codes + log cause #100

Closed
opened 2026-06-18 03:45:20 +00:00 by james · 0 comments
Owner

Problem

The OAuth callback handler's catch block (added in #85) collapses every error other than NoVerifiedEmailError into a generic 400 with the message "Failed to fetch profile" and no log line:

} catch (e) {
  if (e instanceof NoVerifiedEmailError) {
    return failRedirect("no_verified_email");
  }
  return failBadRequest("Failed to fetch profile.");
}

For OIDC, extractProfile() can throw IdTokenSignatureError, IdTokenClaimError (covering iss/aud/exp/nbf/email/missing id_token), or NonceMismatchError — each indicating a different root cause that needs a different fix. The current code makes them indistinguishable from the browser AND swallows the underlying error so it doesn't reach container logs either. Hit while deploying #85 against Authentik on carol.int.wynning.tech — the only signal was "Failed to fetch profile," with no way to tell whether the signature, a claim, the nonce, or something else failed.

Scope

  • In app/api/auth/oauth/callback/[provider]/route.ts catch block: log the underlying error to console.error with the provider id so the cause shows up in container logs.
  • Map each OIDC error class to a distinct ?error= code on the /login redirect:
    • IdTokenSignatureErroroidc_signature
    • IdTokenClaimErroroidc_claim (iss / aud / exp / email / missing id_token)
    • NonceMismatchErroroidc_nonce_mismatch
    • NoVerifiedEmailErrorno_verified_email (unchanged)
    • other → oauth_profile
  • Update tests/api/oauth.test.ts: the three existing scenarios that asserted 400 (signature, email_verified=false, nonce mismatch) now assert 302 with the matching error code.

Out of scope

  • /login page copy for the new error codes. Lands separately.
  • Toggling between 302 + log (chosen here, friendlier for normal users + self-hoster debug) vs 400 + log (cleaner for security review). Open to swapping if a reviewer disagrees.

Acceptance

A self-hoster who hits a misconfigured Authentik / Keycloak / etc. gets a redirect URL ending in ?error=oidc_signature (or _claim / _nonce_mismatch) AND a [oauth callback <name>] line in container logs naming the underlying error class. Either signal alone is enough to diagnose.

## Problem The OAuth callback handler's catch block (added in #85) collapses every error other than `NoVerifiedEmailError` into a generic 400 with the message "Failed to fetch profile" and no log line: ```ts } catch (e) { if (e instanceof NoVerifiedEmailError) { return failRedirect("no_verified_email"); } return failBadRequest("Failed to fetch profile."); } ``` For OIDC, `extractProfile()` can throw `IdTokenSignatureError`, `IdTokenClaimError` (covering iss/aud/exp/nbf/email/missing id_token), or `NonceMismatchError` — each indicating a different root cause that needs a different fix. The current code makes them indistinguishable from the browser AND swallows the underlying error so it doesn't reach container logs either. Hit while deploying #85 against Authentik on `carol.int.wynning.tech` — the only signal was "Failed to fetch profile," with no way to tell whether the signature, a claim, the nonce, or something else failed. ## Scope - [ ] In `app/api/auth/oauth/callback/[provider]/route.ts` catch block: log the underlying error to `console.error` with the provider id so the cause shows up in container logs. - [ ] Map each OIDC error class to a distinct `?error=` code on the `/login` redirect: - `IdTokenSignatureError` → `oidc_signature` - `IdTokenClaimError` → `oidc_claim` (iss / aud / exp / email / missing id_token) - `NonceMismatchError` → `oidc_nonce_mismatch` - `NoVerifiedEmailError` → `no_verified_email` (unchanged) - other → `oauth_profile` - [ ] Update tests/api/oauth.test.ts: the three existing scenarios that asserted 400 (signature, email_verified=false, nonce mismatch) now assert 302 with the matching error code. ## Out of scope - `/login` page copy for the new error codes. Lands separately. - Toggling between 302 + log (chosen here, friendlier for normal users + self-hoster debug) vs 400 + log (cleaner for security review). Open to swapping if a reviewer disagrees. ## Acceptance A self-hoster who hits a misconfigured Authentik / Keycloak / etc. gets a redirect URL ending in `?error=oidc_signature` (or `_claim` / `_nonce_mismatch`) AND a `[oauth callback <name>]` line in container logs naming the underlying error class. Either signal alone is enough to diagnose.
james closed this issue 2026-06-18 03:51:02 +00:00
Sign in to join this conversation.
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
james/carol#100
No description provided.