Skip to content

Magic link v2 0.7#181

Open
kp-timo-beyel wants to merge 4 commits intop2-inc:mainfrom
kp-timo-beyel:magic-link-v2-0.7
Open

Magic link v2 0.7#181
kp-timo-beyel wants to merge 4 commits intop2-inc:mainfrom
kp-timo-beyel:magic-link-v2-0.7

Conversation

@kp-timo-beyel
Copy link

@kp-timo-beyel kp-timo-beyel commented Mar 13, 2026

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-v2
Instead 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-flow
Place as ALTERNATIVE before Cookie in the browser flow. When login_hint does not start with mlv2:, 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:

  • Default (confirm_user_switch: false) — silently expires the session cookies and redirects to a fresh auth flow; no screen is shown
  • confirm_user_switch: true — shows a confirmation page; the user can approve the logout or cancel (returns error=access_denied to the client)

Key parameters (/magic-link-v2)

Parameter Default Description
email / username Target user
client_id OIDC client
expiration_seconds 300 Token TTL
loa Force session LOA level
reusable false Allow multiple redemptions
confirm_user_switch false Show confirmation screen instead of auto-logout
additional_parameters Extra OIDC params (scope, 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:

Browser Flow
├── Magic Link Verifier  [ALTERNATIVE]  ← must be first
├── Cookie               [ALTERNATIVE]
└── Username/Password    [ALTERNATIVE]

Tests

3 new Cypress E2E tests (MagicLinkV2UserSwitchTest) covering:

  1. Auto-logout (default): User B's link silently logs out User A and returns a code
  2. confirm_user_switch=true + continue: confirmation form shown, "Sign out and continue" completes the flow
  3. confirm_user_switch=true + cancel: "Cancel" redirects to client with error=access_denied

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
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.

1 participant