Identity

On this page

IdP failure modes, MFA & debugger

A sign-in that works proves only the happy path. The mock identity providers can also send the responses a careful app must refuse, ask for a second factor, and validate their own answers in a built-in debugger:

  • Failure modes make the IdP send expired, misaddressed, incomplete or failing responses, so you can check that your app rejects them.
  • MFA and step-up add a simulated one-time-code step, with amr, acr, auth_time and max_age in OIDC and AuthnContextClassRef in SAML.
  • The debugger signs in the way a strict relying party would and lists every check it made.

These work on every plan. They apply to generic OIDC and SAML IdPs; the social login presets have their own failure modes.

Failure modes

A fault only changes ordinary fields (claims, times, audience, destination, status) before the normal signing step, so responses stay signed by the IdP's own key. The one exception is the SAML unsigned fault, which leaves out every signature. The IdP never forges or tampers with a signature.

A fault is chosen for each sign-in, in this order:

  1. The Sign in with select next to each user on the login screen (normal or a fault).
  2. ?fault=<id> on the authorize URL (OIDC) or the SSO URL (SAML), for automated tests. ?fault=none turns the default off for one sign-in. An unknown ID is an invalid_request error (OIDC) or an error page (SAML).
  3. The IdP's default failure mode, on the Setup tab or with PATCH /api/idps/:id and faults. A default that exists for only one protocol leaves the other protocol's sign-ins normal.

With auto-login there is no login screen, so only 2 and 3 apply. The fault travels with the authorization code and refresh token, so token-endpoint and userinfo faults hit the requests that follow the sign-in. Every activity-log event of a faulted sign-in has details.fault and a fault badge.

bash
# Every sign-in of this IdP gets an expired ID token until you switch it off
curl -X PATCH https://app.testlie.com/api/idps/$IDP_ID \
  -H "Authorization: Bearer $TESTLIE_API_KEY" -H "Content-Type: application/json" \
  -d '{ "faults": { "mode": "expired_id_token" } }'

# Back to normal
curl -X PATCH https://app.testlie.com/api/idps/$IDP_ID \
  -H "Authorization: Bearer $TESTLIE_API_KEY" -H "Content-Type: application/json" \
  -d '{ "faults": { "mode": null } }'

Two faults take a parameter, set with faults: skewSeconds (default 300, at most 86,400) is how far not_yet_valid moves times into the future, and delayMs (default 10,000, at most 30,000) is how long slow_token waits.

OIDC

Fault What the IdP does A correct app
expired_id_token ID token exp 5 minutes in the past Rejects the ID token
not_yet_valid iat and nbf skewSeconds in the future Rejects it once the skew exceeds its clock leeway
wrong_audience aud (and azp) is urn:testlie:wrong-audience Rejects it: aud does not contain its client ID
wrong_issuer iss has another host (https://wrong-issuer.invalid/...) Rejects it: iss is not the configured issuer
wrong_nonce nonce differs from the one sent Rejects it (replay protection)
missing_nonce No nonce claim Rejects it when it sent a nonce
state_mismatch Redirects back with a different state Rejects the callback before redeeming the code (CSRF)
missing_email No email or email_verified Does not create or link an account; shows an error
email_unverified email_verified: false Does not link to an existing account by email
token_error_invalid_grant The token endpoint answers 400 invalid_grant Shows a sign-in error and lets the user retry
token_error_server The token endpoint answers 500 server_error Handles the error gracefully
slow_token The token endpoint waits delayMs, then answers normally Times out instead of hanging
userinfo_error Userinfo answers 500 server_error Fails the sign-in or falls back to ID token claims, without crashing
userinfo_sub_mismatch Userinfo sub differs from the ID token's Rejects the userinfo response (OIDC Core 5.3.2)

SAML

Fault What the IdP does A correct app
expired_assertion Conditions and SubjectConfirmationData NotOnOrAfter 5 minutes in the past Rejects the assertion
not_yet_valid NotBefore and IssueInstant skewSeconds in the future Rejects it once the skew exceeds its clock leeway
wrong_audience Audience is urn:testlie:wrong-audience Rejects it: the audience is not its SP entity ID
wrong_destination Destination and Recipient are another URL (the response is still posted to your ACS) Rejects the response
wrong_in_response_to InResponseTo names a request that was never sent Rejects it (also for IdP-initiated responses, which must have none)
wrong_issuer The response and assertion Issuer have another host Rejects it: not the configured IdP entity ID
missing_name_id The Subject has no NameID Rejects the assertion
unsigned Neither the response nor the assertion is signed Rejects it: signatures are required
status_authn_failed Responder / AuthnFailed status, no assertion Shows a sign-in error and signs nobody in
status_responder Top-level Responder status, no assertion Shows a sign-in error and signs nobody in

In an end-to-end test

Set the IdP's default failure mode before the test, run your sign-in flow, assert that your app shows its error and created no session, then clear the default. If your test controls the authorize URL, add &fault=<id> to it instead, so tests with different faults can run in parallel.

js
const setFault = (mode) =>
  fetch(`${TESTLIE}/api/idps/${IDP_ID}`, {
    method: "PATCH",
    headers: { Authorization: `Bearer ${TESTLIE_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ faults: { mode } }),
  });

test("rejects an ID token for another audience", async () => {
  await setFault("wrong_audience");
  try {
    // ... sign in through your app and expect its error page ...
  } finally {
    await setFault(null);
  }
});

MFA and step-up

The Multi-factor step setting (Setup tab, or mfa in PATCH /api/idps/:id) decides when the IdP asks for a one-time code after the user is picked:

Setting When the code screen is shown
off (default) Never
always After every user pick
on_request When OIDC acr_values or the SAML RequestedAuthnContext asks for MFA

Values that ask for MFA: mfa, otp, phr, phrh, http://schemas.openid.net/pape/policies/2007/06/multi-factor, urn:okta:loa:2fa:any, urn:okta:loa:2fa:any:ifpossible, urn:oasis:names:tc:SAML:2.0:ac:classes:MultiFactor, urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract, urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken and http://schemas.microsoft.com/claims/multipleauthn.

On the code screen any six digits pass. 000000 simulates a failed challenge: OIDC returns error=access_denied with a description, and SAML posts a Responder / AuthnFailed status. Auto-login completes the step on its own and still reports MFA.

What the IdP reports:

Claim or element Value
amr ["pwd", "otp"] after MFA, ["pwd"] otherwise. Always present in ID tokens
acr After MFA, the first requested MFA value (or the first requested value if none asks for MFA). Without MFA, the first requested value that does not ask for MFA. With nothing requested, or nothing it can satisfy (for example MFA requested while the setting is off): mfa or pwd
SAML AuthnContextClassRef The same rule over the requested class refs. With nothing requested: urn:oasis:names:tc:SAML:2.0:ac:classes:MultiFactor after MFA, urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport without it
auth_time The moment the user was picked or auto-logged in

A correct app compares acr with what it asked for: when the setting is off, a request for mfa comes back as acr: "pwd", and the app must refuse the step-up.

max_age (any value) and prompt=login force a fresh sign-in, so the login screen is shown even with an auto-login user; SAML ForceAuthn="true" does the same. prompt=none with max_age=0 returns login_required.

The debugger

Test OIDC and Test SAML on the IdP's page (and Try in OIDC debugger and Try in SAML debugger in the Failure modes card) start a sign-in from https://app.testlie.com/idp/<slug>/debug/start. Like a real app, the debugger first stores state and nonce (OIDC) or the AuthnRequest ID (SAML) in a session tied to a cookie, then validates the answer completely and lists each check as PASS or FAIL. It allows 60 seconds of clock leeway.

Protocol Checks
OIDC state; the token response (HTTP 200 within 5 seconds, access_token, token_type, id_token); the ID token signature and kid, iss, aud (and azp with several audiences), exp, iat, nbf, sub, nonce, email and email_verified (when the email scope was requested), auth_time against max_age, acr against acr_values, and amr when MFA was requested; the userinfo status and sub
SAML Status, response and assertion Issuer, Destination, InResponseTo (response and SubjectConfirmationData), exactly one assertion, signed (response or assertion, each signature referencing its own element) and every signature valid, IssueInstant, NameID, bearer SubjectConfirmation with NotOnOrAfter and Recipient, Conditions NotBefore and NotOnOrAfter, Audience, AuthnStatement, and AuthnContextClassRef when MFA was requested

Start parameters:

Parameter Protocol Notes
protocol Both oidc (default) or saml
fault Both A failure mode for this sign-in
scope OIDC Default openid profile email groups offline_access
acr_values, max_age, prompt OIDC Passed on to the authorize request
mfa=1 SAML Adds a RequestedAuthnContext for MultiFactor
force=1 SAML Sets ForceAuthn="true"

The debugger is always an allowed redirect URI and ACS URL, so it needs no setup. Each check on the result page has a stable ID (data-check="<id>" and data-ok="true" or "false"), so a browser test can assert on it.

Use the debugger to see what a fault looks like before you test your app with it: a fault that makes a debugger check fail is a response your app must refuse too.