Skip to content

PLUGINS-350 Opt-in OIDC auto-login (prompt=none) - #10

Merged
kathrynalpert merged 1 commit into
developfrom
feature/auto-login
Jul 28, 2026
Merged

PLUGINS-350 Opt-in OIDC auto-login (prompt=none)#10
kathrynalpert merged 1 commit into
developfrom
feature/auto-login

Conversation

@kathrynalpert

@kathrynalpert kathrynalpert commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Adds an opt-in openid.{provider}.autoLogin flag. With it on, an anonymous visitor who lands on the XNAT login page gets sent straight into the provider's OIDC flow with prompt=none instead of having to click "Sign in with …". In an SSO setup they're already logged into the provider for everything else, so they just end up in XNAT logged in. If they don't have a session at the provider, it comes back with login_required and we show the normal login page.

The login-screen extension issues the redirect, the PKCE provider forwards the prompt param, and the filter reads the callback.

Worth calling out:

  • Off by default, opt-in per provider. Only one provider may set it (first one wins, warning logged); any other providers keep their normal "Sign in with …" buttons.
  • Only fires from the login page (it's a login-screen extension) for a not-yet-logged-in visitor. A short-lived guard cookie stops an immediate re-attempt, so a signed-out visitor can't get caught in a redirect loop; once it expires a later visit tries again (which also picks up someone who has since signed in to the provider).
  • The loop guard and the "was this an auto-login attempt" signal both have to survive XNAT recreating the session between Login.vm and the callback — the Turbine login page and the Spring filter don't reliably see the same HttpSession. So the guard is a short-lived first-party cookie (OPENID_AUTOLOGIN_TRIED) rather than a session attribute, and the fallback decision keys off the OIDC error code alone (login_required and the other interaction-required codes only come back from a prompt=none request). The first cut leaned on a session flag for both and got it wrong — a failed attempt showed "OpenID Connect login failed" instead of the login page, because the flag was already gone by the time the callback came back. Best evidence points to the Turbine→Spring session handoff rather than pod-hopping (sticky sessions are configured) — inferred from the flow, not confirmed by capturing session IDs.
  • Requires a provider that supports prompt=none — it's standard OIDC, but not universal.
  • On an open (requireLogin=false) XNAT this doesn't touch normal anonymous browsing — that's served by Index.vm, not Login.vm, so the extension never runs there.

Testing

Built and deployed to dev04, which sits behind oauth2-proxy. The edge proxy authenticates everything before XNAT sees it, so to reach Login.vm anonymously I temporarily pulled the oauth2-proxy forward-auth middleware off the XNAT ingress (and put it back after).

  • Fresh incognito, no provider session → hit XNAT → bounce to the provider with prompt=nonelogin_required → land back on the normal login page, no error banner. This is the case the fix is about.
  • Incognito with a live provider session → straight into XNAT, no button click.
  • Refreshing the login page after a failed attempt doesn't loop — the guard cookie holds for its short TTL, and a later visit retries (so someone who signs into the provider in the meantime still gets picked up).

Unit tests cover the redirect decision matrix (guest vs authenticated, one-shot guard, response-already-committed, provider opt-in), the prompt forwarding, and the interaction-required error classification.

When a provider sets openid.<providerId>.autoLogin=true, an anonymous visitor to the
XNAT login page is sent into that provider's OIDC flow with prompt=none, skipping the
"Sign in with ..." button. If the visitor already has a session at the provider they are
logged straight into XNAT; otherwise the provider reports that interaction is required
and the user is returned to the normal login page.

- OpenIdLoginExtension: on Login.vm, redirect an anonymous guest to
  /openid-login?providerId=<p>&prompt=none. Guarded by a short-lived first-party cookie
  (AUTO_LOGIN_ATTEMPTED_COOKIE, ~2 min) so it can't loop — a later visit retries; only
  for guests, only when the response is not already committed, and never over a pending
  error message.
- PkceAuthorizationCodeAccessTokenProvider: forward a 'prompt' request parameter into the
  authorization request (absent on normal button-click logins).
- OpenIdConnectFilter: intercept an ?error= callback. An interaction-required error
  (login_required and friends) is the expected "no session to reuse" result and returns
  the user to the login page quietly; any other error is a genuine failure. The decision
  keys off the OIDC error code alone, and the loop guard is a first-party cookie, so
  neither depends on server-side session state surviving the Login.vm -> provider ->
  callback redirect chain, across which the XNAT session is not reliably continuous.
- OpenIdAuthPlugin.getAutoLoginProviderId(): resolves the single provider that opts in
  (warns if more than one). Other providers stay configured; their buttons still show.

Opt-in and off by default; documented in the README and sample properties. On an open
(requireLogin=false) XNAT this never touches normal anonymous browsing, which is served
by Index.vm, not Login.vm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kathrynalpert
kathrynalpert requested a review from johnflavin July 28, 2026 14:09
@kathrynalpert
kathrynalpert marked this pull request as ready for review July 28, 2026 14:09
@kathrynalpert
kathrynalpert requested a review from mohana-xw July 28, 2026 14:09
@kathrynalpert
kathrynalpert merged commit 932262e into develop Jul 28, 2026
1 check passed
@andylassiter andylassiter mentioned this pull request Aug 31, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants