diff --git a/MIGRATION.md b/MIGRATION.md index af2ef21..cb77406 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -227,6 +227,8 @@ Affected endpoints (all require `user.webauthn.enabled=true` except where noted) **Passkey enrollment is gated when step-up is enabled.** `POST /webauthn/register` now requires an authentication factor issued within `user.security.stepUp.enrollmentTtlSeconds` (default `600`). Without it the feature protected nothing: an attacker holding a session cookie could enroll their own passkey and assert with it to satisfy every other gate. Any factor counts, not the configured `factors` list, so a first passkey can still be registered after an ordinary password or social login. +> **5.3.3 → 5.3.4 note — enrollment denial response.** Through 5.3.3 the stale/factorless-session denial on `POST /webauthn/register` was a bare `HTTP 403` (the filter-chain authorization rule's default, with no interpretable body). As of 5.3.4 it is `HTTP 401` with a JSON body `{"message": ..., "error": "step-up-required"}`, matching the passkey delete/rename endpoints so a client can recognize it and prompt for a fresh sign-in instead of a passkey retry. If you wrote a client that branched on the `403` for this endpoint, update it to also treat a `401` with error code `step-up-required` as the stale-session case. Re-running the passkey ceremony cannot satisfy enrollment (it accepts any factor and the account may have no passkey yet), so the remedy is a fresh login. + To make that possible, login flows that previously left no factor now stamp one: OIDC logins (`OidcAuthorizationCodeAuthenticationProvider` stamps none, unlike the password and plain-OAuth2 providers), the email-verification link (`FACTOR_OTT`), and post-registration auto-login (`FACTOR_PASSWORD`). Dev login still stamps nothing, so passkey enrollment is unavailable under `user.dev.auto-login-enabled` while step-up is on. `UserService.authWithoutPassword(User)` is unchanged for existing callers; a new overload takes the factor. **`StepUpService` gained a default method.** `canSatisfyStepUp(User)` reports whether a user could satisfy step-up at all, as opposed to whether they have. It defaults to `true`, so existing implementations compile and behave exactly as before. Override it when your mechanism depends on a credential some accounts lack: an OAuth2/OIDC account with no passkey can never produce a WebAuthn factor, and callers treat `false` as "step-up does not apply" and fall back to their configured default rather than rejecting an operation the user could never unlock. The built-in service overrides it, so with `user.security.stepUp.enabled=true` a social-login account with no passkey keeps the `allowInitialPasswordSetWithoutStepUp` behavior instead of receiving a permanent `HTTP 401` on `POST /user/setPassword`. The same applies to passkey delete and rename, which fall back to their pre-feature behavior for accounts that cannot satisfy the configured factors.