fix(auth): terminal error page on rejected OIDC login (LINKS-11) #46

Merged
David merged 1 commit from fix/oidc-rejected-login-terminal-page-LINKS-11 into main 2026-05-28 04:07:19 +02:00
Owner

Summary

Fixes the infinite login redirect loop in rusty-links-saas when the OIDC IdP rejects an authenticated user (e.g. the email-verified gate fails). Resolves LINKS-11.

Previously the /oauth2/callback handler redirected a rejected login to /login?error=access_denied&error_description=..., but the saas /login page then unconditionally redirected the browser to /oauth2/login. That restarted the authorize round-trip against the IdP's still-valid SSO session, which failed again immediately, looping forever between /login, /oauth2/login, the IdP authorize endpoint, and /oauth2/callback. The user never saw the rejection reason.

Change

The saas Login component now inspects the query string on mount. When an error parameter is present it renders a terminal error card showing error_description and does NOT auto-redirect, breaking the loop. With no error parameter the page behaves exactly as before and starts the OIDC flow. The detection signal stays None on SSR and the initial WASM render so hydration reconciles identically before the effect runs. Adds the UrlSearchParams and Location web-sys features for query parsing.

Retrying requires an explicit user action: a "Try again" link to /oauth2/login, or "Sign in with a different account" which routes through /oauth2/logout to clear the local session and terminate the reused IdP SSO session that fuels the loop.

The IdP-side account gate is tracked separately in A8N-63; this PR is the client-robustness fix that holds regardless of account state.

Acceptance criteria

  • An OIDC callback returning access_denied (or an unverified/rejected identity) renders a terminal page showing error_description, with no automatic redirect back to the IdP authorize endpoint.
  • /login does not auto-redirect to /oauth2/login when an error parameter is present; the /login <-> /oauth2/login cycle is broken.
  • Retrying login requires an explicit user action.
  • Stable error page instead of a loop for the rejected staging account (verified by control-flow: the error-param branch never calls set_href).

Testing

  • cargo check --no-default-features --features web,saas --target wasm32-unknown-unknown (clean)
  • cargo check --no-default-features --features saas,server (lib compiles; the only error is the dx-generated assets/tailwind.css being absent in a bare checkout, unrelated to this change)
  • cargo fmt --check, cargo clippy (no new findings)

🤖 Generated with Claude Code

## Summary Fixes the infinite login redirect loop in `rusty-links-saas` when the OIDC IdP rejects an authenticated user (e.g. the email-verified gate fails). Resolves LINKS-11. Previously the `/oauth2/callback` handler redirected a rejected login to `/login?error=access_denied&error_description=...`, but the saas `/login` page then unconditionally redirected the browser to `/oauth2/login`. That restarted the authorize round-trip against the IdP's still-valid SSO session, which failed again immediately, looping forever between `/login`, `/oauth2/login`, the IdP authorize endpoint, and `/oauth2/callback`. The user never saw the rejection reason. ## Change The saas `Login` component now inspects the query string on mount. When an `error` parameter is present it renders a terminal error card showing `error_description` and does NOT auto-redirect, breaking the loop. With no `error` parameter the page behaves exactly as before and starts the OIDC flow. The detection signal stays `None` on SSR and the initial WASM render so hydration reconciles identically before the effect runs. Adds the `UrlSearchParams` and `Location` web-sys features for query parsing. Retrying requires an explicit user action: a "Try again" link to `/oauth2/login`, or "Sign in with a different account" which routes through `/oauth2/logout` to clear the local session and terminate the reused IdP SSO session that fuels the loop. The IdP-side account gate is tracked separately in A8N-63; this PR is the client-robustness fix that holds regardless of account state. ## Acceptance criteria - [x] An OIDC callback returning `access_denied` (or an unverified/rejected identity) renders a terminal page showing `error_description`, with no automatic redirect back to the IdP authorize endpoint. - [x] `/login` does not auto-redirect to `/oauth2/login` when an `error` parameter is present; the `/login` <-> `/oauth2/login` cycle is broken. - [x] Retrying login requires an explicit user action. - [x] Stable error page instead of a loop for the rejected staging account (verified by control-flow: the `error`-param branch never calls `set_href`). ## Testing - `cargo check --no-default-features --features web,saas --target wasm32-unknown-unknown` (clean) - `cargo check --no-default-features --features saas,server` (lib compiles; the only error is the `dx`-generated `assets/tailwind.css` being absent in a bare checkout, unrelated to this change) - `cargo fmt --check`, `cargo clippy` (no new findings) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(auth): show terminal error page on rejected OIDC login (LINKS-11)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 51s
Create release / Create release from merged PR (pull_request) Has been skipped
4a3fbc6242
When the OIDC callback rejects a user (e.g. the email-verified gate fails) it redirects to `/login?error=access_denied&error_description=...`. The saas `/login` page unconditionally redirected the browser to `/oauth2/login`, which restarted the authorize round-trip against the IdP's still-valid SSO session. That immediately failed again and produced an infinite redirect loop between `/login`, `/oauth2/login`, the IdP authorize endpoint, and `/oauth2/callback`; the user never saw the rejection reason.

The saas `Login` component now reads the query string on mount. When an `error` parameter is present it renders a terminal card showing `error_description` and does NOT auto-redirect, breaking the loop. Retrying requires an explicit user action: a "Try again" link to `/oauth2/login`, or "Sign in with a different account" which routes through `/oauth2/logout` to clear the local session and terminate the reused IdP SSO session. With no `error` parameter the page behaves as before and starts the OIDC flow.

The signal stays `None` on SSR and the initial WASM render so hydration reconciles identically before the effect runs. Adds the `UrlSearchParams` and `Location` web-sys features for query parsing.

#LINKS-11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
David merged commit 1c51ae1037 into main 2026-05-28 04:07:19 +02:00
David deleted branch fix/oidc-rejected-login-terminal-page-LINKS-11 2026-05-28 04:07:19 +02:00
Sign in to join this conversation.
No reviewers
No labels
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
a8n-tools/rusty-links!46
No description provided.