Repository navigation
Replies: 2 comments
|
This is a tricky one. Setting In Keycloak's current implementation, the So your approach should work idiomatically — set The main gotcha is that the session's LoA is stored at the session level, not per-client. So once a user steps up to "strong" for one client, all subsequent SSO logins in that session will be at "strong" level — there's no way to "step down" within the same session. That's usually fine but worth knowing. |
|
Thanks — that matches the design intent. Session-level The friction we hit is empirical and the repro at the end of the post is the load-bearing part. With Two small proposals, smallest-first:
Happy to file both with full reproducers and a draft PR for (1). Is that the preferred path, or should #28341 be reopened with the evidence + PR attached? |
Uh oh!
There was an error while loading. Please reload this page.
Context / requirements
A single realm with a mix of OIDC clients, some requiring strong authentication (LoA ≥ 2) and some accepting weak (LoA 1).
default.acr.values=strong+minimum.acr.value=strongon the client; realm hasacr.loa.map = {"strong": 2}.auth-otp-forminside an existingConditional 2FAsubflow)UsernamePasswordFormdispatching towebauthn-authenticator-passwordless)Identity Provider RedirectorWe're on Keycloak 26.5.
What we ended up building
We've iterated through about five flow shapes (one of which broke the realm 3×) and landed on this — but it feels like a lot of bespoke pieces for what we expected to be a stock primitive.
Flow shape (a copy of the built-in
browserflow with additions, provisioned on every backend startup)The four bespoke pieces
Custom
ConditionalAuthenticator(custom-conditional-loa). Same idea as the built-inconditional-level-of-authenticationbut with an explicitrequestedLoa >= configuredLoaguard — see "Why a custom conditional" below.A "set LoA" authenticator (
loa-promotion) that callsAcrStore.setLevelAuthenticated(2)and immediately succeeds. Used as the body of every per-credential CONDITIONAL subflow above.A LoA-MAP propagator — the same
loa-promotionauthenticator also implementsAuthenticationFlowCallback, and itsonTopFlowSuccesscopies the auth-sessionLOA_MAPauthNote to the user-session note (mirroring what the built-inConditionalLoaAuthenticator.onTopFlowSuccessdoes), so that cookie SSO into a new client picks up the LoA viaCookieAuthenticator.An
EventListenerthat, on aLOGINevent, writes the user-sessionLOA_MAPdirectly + writes a separateauth.methodsuser-session note (consumed by anoidc-usersessionmodel-note-mapperfor the token'samrclaim). OnLOGINevents that have nocredential_type/identity_providermarkers in details, we have to distinguish "fresh password re-auth" from "cookie-restored SSO" so we don't wipeLOA_STRONGfrom the map on the latter — we use the presence of the auth-sessionLOA_MAPauthNote (set byCookieAuthenticator.authenticateline ~56) as the discriminator.Why a custom conditional
The built-in
ConditionalLoaAuthenticator.matchCondition(seeConditionalLoaAuthenticator.java) has this early return:NO_LOA = -1 < MINIMUM_LOA = 0, so on every fresh login this returnstrueregardless of whether the request asked for any acr at all. A tail CONDITIONAL using the built-in therefore fires for every client — empirically reproduced: a tail-CONDITIONAL withdeny-accessbody locked out our weak-only client because the condition matched for it too.Our custom
matchCondition:That's the minimum we could find that yields the right behavior for both weak and strong clients on fresh login AND on cookie SSO.
Specific questions
Is there a simpler / canonical pattern we missed? Per-credential LoA promotion subflows + tail CONDITIONAL is a lot of plumbing for what feels like a standard requirement. Is there a built-in we should be using instead of a custom "set LoA" authenticator +
conditional-credential? Something that says "this credential type implies LoA X" at the realm or authenticator level?Is the bootstrap behavior of
ConditionalLoaAuthenticator.matchCondition(returningtrueunconditionally whencurrentAuthenticationLoa < MINIMUM_LOA) intentional? If yes, what's the recommended placement that doesn't cause it to fire for clients that didn't request a higher LoA? If no, would a PR moving therequestedLoa < configuredLoaguard above the early return be welcome?Per-credential LoA promotion as a first-class feature. Would the project accept a proposal for either (a) a realm-level credential-type → LoA map, or (b) an authenticator-config field "on success, set LoA to N"? Either would eliminate three of our four bespoke pieces.
minimum.acr.valueenforcement timing vs in-flow LoA restoration on cookie SSO. WhenCookieAuthenticatorrestoresLOA_MAPshowing level 2 was previously achieved, and a fresh authorize request comes in for a client withminimum.acr.value=strong, theminimum.acr.valuecheck appears to fire BEFORE in-flow conditionals get to restore LoA into the current request viasetLevelAuthenticatedToCurrentRequest. We observe re-prompts after step-up that we wouldn't expect given the user session has the level. Is this a known interaction, expected behavior, or worth a separate issue with a repro?Distinguishing cookie-restored LOGIN events from password re-auth. Both fire
EventType.LOGINwith essentially identicaldetails(nocredential_type, noidentity_provider). We worked around it by checkingauthSession.getAuthNote(Constants.LOA_MAP)— non-null implies cookie restoration. Would the team accept a feature request to add something likedetails.get("auth_source") ∈ {cookie, password, idp}toLOGINevents? Or is there an existing signal we missed?Repro
Minimal repro of question 2 (the bootstrap behavior):
rwithacr.loa.map = {"strong": 2}.weak-only(no acr attributes),strong-required(default.acr.values=strong).browsertobrowser-r. At the tail ofbrowser-r, add a CONDITIONAL subflow:conditional-level-of-authenticationREQUIRED, configloa-condition-level=2, loa-max-age=0.deny-accessREQUIRED.browser-ras the realm's browser flow.weak-onlyin fresh incognito — expect login form; observe "Access denied" instead.Versions: Keycloak 26.5.0, no preview features enabled beyond defaults.
What we'd love to confirm before filing issues
We've gone through this iteration cycle four times across a week and only landed on the current shape by reading Keycloak's source. If we missed something obvious, we'd love to know — that's why this is a Discussion and not an issue. If the answer is "yes you have to do all this", we'll happily file the bugs and RFEs with detailed repros or pull requests.
Thanks!
All reactions