Skip to content
TDACorpIDENTITYSign in

Step-up and SSO enforcement

How a resource server asks for a fresher or stronger sign-in (RFC 9470), how a client answers it, and how an organization that enforces single sign-on uses the same challenge at the token endpoint.

On this page

Status: Available

for the resource-server check and the client step-up, which need no flag. Organization-level single sign-on enforcement is behind the sso_enforcement flag, which defaults OFF. Off, no token request is refused for want of single sign-on and the sign-in step never offers an organization's connections for it.

A valid token proves who called, and when and how they signed in. Some routes need more: a payment needs a sign-in from the last five minutes, an admin route needs aal2. RFC 9470 gives a resource server one way to say so, and a client one way to answer. This page follows that exchange from end to end. For what a step-up looks like to a person, see Step-up.

The exchange#

client  --access token--> your APIyour API --401 WWW-Authenticate: ... error="insufficient_user_authentication", acr_values, max_age--> clientclient  --rp.stepUpUrl(challenge, tenant)--> browser --> /authorize (max_age, acr_values)client  <--code-- identity  --> /token --> a new token that meets the requirementclient  --new access token--> your API

1. Your API refuses#

Pass requireAuthentication: { acrValues?, maxAgeS? } to createResourceServer, or to a decision-client adapter. The token is verified first; only then is its authentication compared.

  • acrValues: the token's acr must be one of them, or, for aal1 to aal3, reach the lowest one listed. A token with no acr fails.
  • maxAgeS: the token's auth_time must be at most that many seconds old. A token with no auth_time fails.

The checks are in authenticationShortfall. The options are documented in Resource-server verification.

The refusal is a 401. The challenge names the requirement:

WWW-Authenticate: DPoP error="insufficient_user_authentication", error_description="a stronger or more recent authentication is required", acr_values="aal2", max_age="300"

createResourceServer picks the scheme of the token (Bearer for an unbound one, DPoP for a bound one) and its body is {"error":"insufficient_user_authentication"}. The adapters answer an application/problem+json body and list both schemes in the header. acr_values is space separated, and each parameter appears only if you set it.

Where your own code is not the SDK, send the same header. Do not use it for a missing scope (that is a 403 with insufficient_scope) or for a bad token (invalid_token).

2. Your client answers#

rp.stepUpUrl(challenge, tenant, input?) takes the parsed challenge or the raw WWW-Authenticate value and the organization id, and starts an ordinary authorization. It asks for the challenge's max_age, and for its acr_values only where they are aal1, aal2 or aal3, because those are the only values the server accepts. It sets tdaid_tenant to the organization, and returns { url, transaction } exactly as startAuthorization does, so you store the transaction and send the browser to the URL as for any sign-in. See Relying-party module.

Identity then does what max_age and acr_values always ask: the person signs in again if the last sign-in is older, or confirms with a stronger method. See Authorize. The new access token carries the fresh auth_time and acr. Retry the request with it.

Single sign-on enforcement#

An organization can require that its members entered through one of its single sign-on connections, and how recently (sso.enforce and sso.max_age_s in its settings; max_age_s defaults to 86,400 seconds). With sso_enforcement ON, two places apply that rule.

At the authorization endpoint. A person who has not entered through one of the organization's connections, or whose connection sign-in is older than max_age_s, is sent to the sign-in step, which offers those connections. Under prompt=none they are redirected back with interaction_required. The answer is the same for a member and a non-member, and it comes after the installation half of the entry rule and before any membership read, so it reveals nothing. Members who are guests are exempt unless the organization sets sso.guests_exempt to false.

At the token endpoint. A code redemption, a refresh and a token exchange for an enforcing organization re-check the session behind the grant. If its single sign-on is too old the endpoint answers 400:

{ "error": "insufficient_user_authentication", "error_description": "the tenant requires a recent single sign-on authentication", "acr_values": "aal1", "max_age": 86400 }

max_age is the organization's sso.max_age_s. The check is skipped for a grant with no organization and for one with no session, such as client_credentials. It is the same challenge as above, so one handler can serve both: call rp.stepUpUrl with { acrValues: ['aal1'], maxAge } and the organization. aal1 is the lowest level, so any session meets it. RFC 9470 cannot say "through the organization's connection", so the requirement is carried by max_age and the connection itself is demanded by the authorization endpoint. The error row is also in Token endpoint.

What identity's decision API does not do#

The decision API authenticates your service, not the person, so it never answers with this challenge. The check in your API (step 1) is yours to place, before you ask for a decision.

Verified against commit c5ef357