Open
Conversation
Introduces a parallel Magic Link v2 implementation that participates in
the standard Keycloak browser flow, enabling acr_values / step-up auth
and LOA support without bypassing the flow.
Key design:
- POST /magic-link-v2 stores credentials in SingleUseObjectProvider
(Infinispan) under a random UUID; only "mlv2:{uuid}" (~42 chars) is
placed in login_hint to stay within Keycloak's 255-char limit
- MagicLinkBFAuthenticator (ext-magic-link-browser-flow) resolves the
UUID, validates expiry and client, enforces single-use atomically via
putIfAbsent, sets user/LOA/rememberMe and calls context.success()
- LOA is read from the stored credential or falls back to the sibling
Condition - Level of Authentication in the same parent sub-flow
- set_email_verified=true also removes the VERIFY_EMAIL required action
New files:
- MagicLinkV2Token, MagicLinkV2Request, MagicLinkV2Response
- MagicLinkV2Resource + Provider/Factory
- MagicLinkV2ApiTest (10 integration tests)
- MagicLinkV2GeneratedWithPostRequestTest (Cypress browser-flow test)
- pre-generated-magic-link-v2.cy.ts
- magic-link-v2-api-test-setup.json realm fixture
- CLAUDE.md project instructions
Documentation:
- README: updated "How it works" and Why v2 sections to reflect
Infinispan UUID design instead of signed JWT
- CLAUDE.md: documented token lifetime, key structure, security model
When a magic link is opened for a different user than the one currently logged in, the verifier now handles the session conflict automatically: - Default (confirm_user_switch: false): silently expires the identity and auth-session cookies and redirects to a fresh OIDC auth flow. The token is still valid, so the fresh flow authenticates the target user without any user interaction. - confirm_user_switch: true: shows a confirmation page informing the user that they are currently signed in and asking whether to continue. "Sign out and continue" performs the same redirect; "Cancel" returns error=access_denied to the client. Changes: - MagicLinkV2Token: add KEY_CONFIRM_USER_SWITCH constant - MagicLinkV2Request: add confirm_user_switch field (default false) - MagicLinkV2Resource: store the flag in the Infinispan notes map - MagicLinkBFAuthenticator: check the flag in handleTokenId(); extract shared redirectAfterLogout() method used by both auto-logout and the confirmation form's "logout" action - magic-link-user-switch.ftl: buttons side-by-side, username bold, user-friendly message without "magic link" terminology - messages_en/de.properties: updated and split user-switch messages - README: document confirm_user_switch parameter and user-switch section
Three scenarios are covered: 1. Auto-logout (default, confirm_user_switch=false): opening a magic link for User B while User A is logged in silently expires session cookies and returns an authorization code for User B without any user interaction. 2. confirm_user_switch=true — continue: confirmation form is shown; clicking "Sign out and continue" completes the flow and returns a code. 3. confirm_user_switch=true — cancel: clicking "Cancel" on the confirmation form redirects back to the client with error=access_denied. New files: - magic-link-v2-user-switch-test-setup.json: realm with two users (A and B) and the verifier placed BEFORE the Cookie authenticator (required for user-switch detection) - magic-link-v2-user-switch.cy.ts: Cypress spec covering all three scenarios - MagicLinkV2UserSwitchTest.java: Java test that generates four links (User A reusable, User B auto-logout, User B confirm-continue, User B confirm-cancel) and passes them to the Cypress container via environment variables
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Magic Link v2 — Browser-Flow Authenticator with User-Switch Handling
Adds a parallel Magic Link v2 implementation that coexists with v1 without breaking changes.
What's new
New endpoint:
POST /realms/{realm}/magic-link-v2Instead of an action-token URL, returns a standard OIDC authorization URL with the credential stored server-side in Infinispan under a UUID reference (
login_hint=mlv2:{uuid}). The full browser flow executes —acr_values,Condition – Level of Authentication, and step-up authenticators all work natively.New authenticator:
ext-magic-link-browser-flowPlace as ALTERNATIVE before Cookie in the browser flow. When
login_hintdoes not start withmlv2:, it passes through silently.User-switch handling
When a magic link is opened for User B while User A is already logged in on the device:
confirm_user_switch: false) — silently expires the session cookies and redirects to a fresh auth flow; no screen is shownconfirm_user_switch: true— shows a confirmation page; the user can approve the logout or cancel (returnserror=access_deniedto the client)Key parameters (
/magic-link-v2)email/usernameclient_idexpiration_secondsloareusableconfirm_user_switchadditional_parametersscope,state,nonce,code_challenge, …)Why UUID instead of JWT in
login_hint?Keycloak silently truncates OIDC parameters longer than 255 characters. A typical JWT is 500–700 characters and would be dropped. The UUID reference (
mlv2:{uuid}) is ~42 characters and provides equivalent security: 128-bit entropy + Infinispan TTL + atomic single-use tracking.Browser flow setup
Place the Magic Link Verifier (
ext-magic-link-browser-flow) as ALTERNATIVE before Cookie:Tests
3 new Cypress E2E tests (
MagicLinkV2UserSwitchTest) covering:confirm_user_switch=true+ continue: confirmation form shown, "Sign out and continue" completes the flowconfirm_user_switch=true+ cancel: "Cancel" redirects to client witherror=access_denied