CodamAIDocs
Topicdone

Sign-in level and step-up

How strongly and how recently someone signed in: the three sign-in levels, the maximum age of the sign-in, where CIAS reads both from, and what the answer looks like when an operation asks for more.

Variants
level basic, mfa, phishing-resistantwith maximum ageno or unknown acr → basicno sign-in time → no maximum age satisfiedpresented instead of exchanged tokenuser switchservice accountpermission missing → 403sign-in not enough → 401 with step-up

What this is about

For most operations it is enough that someone is signed in. Some ask for more: whoever approves a template or grants roles should perhaps have signed in with a second factor, or recently. A second factor is something besides the password, such as a code from an authenticator app or a security key.

There are two settings for this:

  • The sign-in level says how strongly someone signed in.
  • The maximum age of the sign-in says how long ago the sign-in may have happened at most.

If the sign-in is not enough for an operation, the backend answers with 401 and says exactly what is missing. The user interface then sends the person to Keycloak again, and Keycloak asks only for the step that is missing. This is called step-up: raising the sign-in by a level without starting over.

The three levels

LevelWhat the person didExample
basicsigned in, by whatever means the realm allowsuser name and password
mfasigned in with a second factorpassword and one-time code (OTP)
phishing-resistantsigned in with a factor that is bound to the address of the sitepasskey, security key (WebAuthn)

Phishing-resistant means: a fake site under a different address cannot do anything with this factor. The key only answers the real address.

The levels are ordered. A stronger sign-in satisfies every weaker requirement: whoever signed in phishing-resistant also satisfies mfa and basic.

The three levels are fixed. An installation cannot add a level of its own or rename one. A module writes “this needs mfa” once in its code, and that should mean the same in every installation. The names describe what the person did, not how an identity provider numbers it. That way they stay valid when the identity provider changes.

The maximum age

A requirement consists of a level and, if wanted, a maximum age. The maximum age is a whole number of seconds, at least one. A user interface cannot be told anything more precise.

The time of the sign-in is in the token, in the claim auth_time. A claim is a named value in the token. A requirement is satisfied when

  • the person’s level satisfies the required level and
  • with a maximum age set, the sign-in is not longer ago than the maximum age. Exactly the maximum age still counts.

A few examples:

RequirementMeaning
basic, no maximum ageany sign-in. This is the normal case, and every signed-in person satisfies it
basic, 300 seconds“just signed in again”, by whatever method
mfa, no maximum agewith a second factor, no matter when
mfa, 300 secondswith a second factor, and at most five minutes ago

If the sign-in time is missing, no maximum age is satisfied. There is no tolerance for clocks that differ. If someone is refused by a narrow margin, they sign in again, get a fresh sign-in time and pass.

Where the level comes from

Keycloak calls the concept Level of Authentication. The level is in the token, in the claim acr. So that CIAS’s own names are there and not numbers, CIAS teaches them to Keycloak:

  • CIAS writes its three names into the realm’s ACR-to-LoA mapping, the attribute acr.loa.map: basic 1, mfa 2, phishing-resistant 3. This happens in every reconciliation. Names an operator added there stay.
  • Keycloak then writes exactly these names into acr: basic after a fresh sign-in, mfa after a step-up to mfa. A token refresh keeps the level.
  • Which factors Keycloak asks for is set by the operator in the realm’s browser flow. This is the sign-in sequence Keycloak works through at login. For every level above basic it needs a step with the condition “Condition – Level of Authentication” and the level’s number. CIAS never builds or changes this flow. It only reads which levels it can reach.

At login, the user interface asks for a level with acr_values. If the backend asks for a maximum age, it also sends prompt=login: Keycloak then signs in anew in any case, and the sign-in time is fresh. If the browser flow has no step for a level, Keycloak silently ignores acr_values and issues a token at the level the flow has. The backend then refuses again. So that a person is not the first to notice, the reconciliation compares the required levels with the reachable ones and reports every one that is missing. See Reconciliation with Keycloak and The Keycloak adapter.

basic when in doubt

CIAS reads the level so that a mistake never grants more. This is called fail-closed, see Reject when in doubt.

What the token says and which level it becomes
acr in the tokenauth_time in the tokenWhat CIAS reads
basic, mfa or phishing-resistant–exactly this level
missing or empty–basic
any other value, such as 0, 2 or a URL–basic. CIAS writes a warning to the log once per value, not on every request
–a numberthe sign-in time
–missing or not a numberno sign-in time. No maximum age is satisfied

Keycloak writes 0 into acr, for example, when a session was only restored from a cookie. CIAS reads that as basic too.

The presented token counts

On every request CIAS exchanges the token for one for the backend. CIAS reads level and sign-in time, however, from the token the caller presented, that is, before the exchange.

The reason: the presented token is the one the person signed in for. Its acr is the result of the sign-in sequence the user interface asked for. Keycloak’s token exchange does not keep the level: the exchanged token of a person who just stepped up to mfa reads basic again. If CIAS read the exchanged token, every step-up would be lost on the way into the backend.

In the RequestContext both values are authenticationLevel and authenticatedAt. The RequestContext is the store for the details of a request, which CDMS and the other modules read from.

User switch

With a user switch, a permitted person works on behalf of another. Level and sign-in time do not switch with it. They describe the person at the keyboard, not the person being acted as.

An administrator who switches into a person who just signed in with a second factor does not inherit that sign-in. If an operation requires mfa, the administrator must have signed in with a second factor.

Service accounts

A service account is the account a service signs in with by itself, without a person, see Sign in as a service. CIAS recognizes its token by azp (the client the token was issued for) and preferred_username in the form service-account-<client>, both from the presented token.

A service account always has the level basic and no sign-in time, because its token contains neither. It also cannot sign in again. It could never satisfy a requirement above basic or with a maximum age.

Whether a service account is exempt from a requirement is decided by each module:

WhereService accounts
the administration of CIASexempt, see Sign-in level for the administration
CDMSexempt, see Sign-in level per model and operation
final documents in CRMSexempt, see Sign-in level for documents
approvals in CRMSnot exempt. An approval is always given by a person, see Approval chains

The answer when the sign-in is not enough

sequenceDiagram
    participant O as User interface (BFF)
    participant B as Backend
    participant K as Keycloak
    O->>B: Request with token (acr = basic)
    B->>B: Permission present? yes
    B->>B: Sign-in enough? no, mfa is required
    B-->>O: 401, step-up-required, acr_values="mfa"
    O->>K: Login with acr_values=mfa
    K-->>O: asks only for the second factor, new token (acr = mfa)
    O->>B: the same request again
    B-->>O: 200

The backend here is CDMS, CRMS or CIAS itself. How the user interfaces react, with dialog, window and exactly one repeat, is described in Catching up on a stronger sign-in (step-up).

The answer has two parts: a header following the standard RFC 9470, which any client can understand, and a body in the platform’s error format, which the user interfaces translate.

Request
POST /cias/admin/role-assignments
Authorization: Bearer eyJ…   (acr = basic)
Response
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication", error_description="A stronger or more recent sign-in is required", acr_values="mfa", max_age=300

{ "messageKey": "cias.authentication.step-up-required|mfa|300", "code": "401", "layer": "api" }

The key carries its values after |:

cias.authentication.step-up-required|<level>
cias.authentication.step-up-required|<level>|<seconds>
  • The first value is the required level.
  • The second is the maximum age in seconds. Without a maximum age it is missing, and the header has no max_age.

Header and body always say the same, because CIAS reads the header from the key the module wrote. No module writes the header itself. That way the header states exactly the requirement that was checked.

Two kinds of 401

Two answers with 401
cias.authentication.step-up-required
  • The token is valid.
  • The sign-in is too weak or too old.
  • Header: error="insufficient_user_authentication" with acr_values and, where set, max_age.
  • Signing in again helps: with the required level, then repeat the request.
cias.authentication.token-rejected

A client tells the two answers apart by the value of error in the header.

Permission first, then the sign-in

Every module first checks whether the person may do the operation at all. Only then does it check whether the sign-in is enough.

In which order the checks run
Permission present?Sign-in enough?Answer
no–the module's 403, as without a requirement
yesno401 cias.authentication.step-up-required|…
yesyesthe operation runs

The other way round, CIAS would send a person to a stronger sign-in that would still not let them in afterwards.

Requirements are fixed

Where a requirement applies is stated by the module at the point where it decides on business grounds, never per URL:

Every module declares the levels it requires in its declaration. That way the reconciliation can check whether Keycloak reaches them.

Where an installation sets a requirement, the value is fixed at build or rollout time: as a setting that an environment variable can override. No requirement can be changed at runtime, neither through an endpoint nor through a user interface. Whoever could lower a requirement could switch the step-up off.

Pitfalls

Next

Search