backend / authentication / sso / 09_mfa_step_up.md

MFA and Step-Up Authentication

6 interview angles 7 min read source

MFA and Step-Up Authentication

MFA (Multi-Factor Authentication) requires a second proof of identity beyond a password. With SSO, MFA is usually enforced at the IdP — your app inherits it. But your app may need to demand additional authentication for sensitive operations (step-up).

The factors

Factor Examples
Something you know password, PIN, security question
Something you have TOTP app, hardware key (YubiKey), passkey, SMS code
Something you are fingerprint, face, voice

MFA = ≥ 2 factors from different categories. Password + SMS = MFA. Password + security question ≠ MFA (both “know”).

SSO + MFA — who enforces it?

In SSO, MFA enforcement is usually at the IdP. Your app trusts the IdP did its job and reports the result in the assertion.

Two claims in OIDC carry the info:

Claim Meaning
amr Authentication Methods References — array like ["pwd", "mfa", "otp"]
acr Authentication Context Class Reference — a level like urn:mace:incommon:iap:silver

SAML has the equivalent: <saml:AuthnContextClassRef>.

Your app checks amr / acr to enforce policies:

def require_mfa(claims):
    amr = claims.get("amr", [])
    if "mfa" not in amr and "otp" not in amr:
        raise NotAuthorized("MFA required")

Standard amr values (RFC 8176)

Value Meaning
pwd password
otp one-time password (TOTP, HOTP, SMS)
hwk hardware key (FIDO, YubiKey)
face face recognition
fpt fingerprint
mfa multi-factor authentication (composite)
mca multi-channel authentication
sc smart card
swk software key (proof-of-possession of a software-stored key)
pin PIN

Not all IdPs include amr. Read your IdP’s docs; Okta, Auth0, Azure AD all have somewhat different conventions.

ACR — strength tiers

ACR groups authentication methods by strength. Common references:

ACR Means
0 “I authenticated somehow but make no claim about strength”
1 password
urn:mace:incommon:iap:bronze weak
urn:mace:incommon:iap:silver password + something
urn:mace:incommon:iap:gold strong + biometric
custom URIs vendor / app-specific

Your app can request a minimum ACR:

GET /authorize?response_type=code
    &acr_values=urn:mace:incommon:iap:silver
    &scope=openid

The IdP enforces it (re-prompts for MFA if needed) and returns acr in the ID token.

For step-up: send the user back through /authorize with a higher acr_values.

Step-up authentication

User is logged in with password only. They click “Transfer $10,000.” Your app demands MFA before letting that go through.

@app.post("/transfer")
def transfer(amount: Decimal, request: Request):
    user = get_current_user(request)
    last_mfa = request.session.get("last_mfa_at")

    if amount > Decimal("1000") and (not last_mfa or last_mfa < time.time() - 300):
        # require fresh MFA within 5 minutes
        return RedirectResponse(f"/auth/step-up?next={request.url}")

    do_transfer(user, amount)

The step-up flow:

1. App → IdP            /authorize?...&acr_values=mfa&prompt=login
2. IdP                  forces MFA challenge
3. IdP → App            new tokens with elevated ACR
4. App                  records step-up timestamp; allows the sensitive operation

Key params:

  • prompt=login forces re-authentication even with active session.
  • max_age=0 similar effect (require fresh login).
  • acr_values=... requests specific strength.

Approaches without OIDC step-up

For apps that don’t go back to the IdP for every step-up:

Approach How
Re-enter password prompt for password again, validate against the IdP via password-grant (deprecated) or your own DB
Out-of-band push send a “approve this transaction?” notification to the user’s phone
TOTP at the app level additionally enroll users in app-specific MFA, prompt within app
WebAuthn passkey or hardware key challenge in-browser

App-level MFA layered on top of SSO MFA is increasingly common for high-stakes apps (banks, admin consoles).

Recent-auth checks

A simpler model: check that the user authenticated within a recent window.

last_login = session["auth_time"]
if time.time() - last_login > 900:        # 15 minutes
    redirect_to_login(prompt="login")

auth_time is an OIDC claim — when the user actually authenticated (may be earlier than iat).

Passkeys / WebAuthn

The modern replacement for passwords + TOTP. A device-bound or synced credential (typically backed by platform authenticators — Face ID, Touch ID, Windows Hello).

In SSO context: many IdPs (Google, Microsoft, Okta) now support passkeys. Your app benefits without changes — passkey login at the IdP shows up as a strong amr value.

For app-specific WebAuthn (passkey enrollment in your app on top of SSO): use a library like py_webauthn or webauthn-rp and enroll passkeys during onboarding. Use them for step-up later.

TOTP basics (when implementing app-level MFA)

TOTP (RFC 6238) = HMAC-based one-time password tied to time:

  1. App generates a secret per user (e.g. 160 bits).
  2. User scans a QR code in their authenticator app (Google Authenticator, 1Password, Authy).
  3. App and user’s device both compute the current 6-digit code from HMAC-SHA1(secret, current_30s_window).
  4. User enters code; app verifies.

Python:

import pyotp
secret = pyotp.random_base32()         # store this per user
totp = pyotp.TOTP(secret)
print(totp.now())                       # current 6-digit code
print(totp.verify("123456"))            # validate user input

Important:

  • Allow ±1 time window (default in pyotp’s verify) for clock skew.
  • Rate-limit attempts (an attacker has 1M codes to try; without limits, brute force is feasible).
  • Store the secret encrypted.
  • Provide recovery codes (10 single-use codes printed at enrollment) for lost devices.

SMS as a factor — increasingly discouraged

SMS is the lowest-quality second factor:

  • SIM swap attacks.
  • SS7 vulnerabilities.
  • SMS interception.

NIST 800-63B (since 2017) discourages SMS for high-security contexts. Many IdPs still offer it; many enterprises disable it.

Prefer: TOTP, push notifications, WebAuthn/passkeys.

MFA fatigue / push bombing

Attackers spam push notifications until the user (mistakenly) approves one. Lapsus$ used this pattern.

Defenses:

  • Number matching (“approve only if the code shows 27”) — user must enter the displayed number.
  • Rate limiting on push prompts.
  • Cancel pending prompts on new login attempts.
  • Alerting after N declined prompts.

Most major IdPs now do number matching by default.

Adaptive / risk-based authentication

Don’t always demand MFA — only when something looks off.

Risk signals:

  • New device fingerprint.
  • New geolocation.
  • IP reputation.
  • Impossible travel (logged in from US 10 min ago, now from China).
  • Time of day outside the user’s normal pattern.
  • Threat intel (compromised credential databases).

The IdP computes a risk score; high-risk → require MFA, very high → block. Combine with acr_values so your app gets the right level.

Common pitfalls

  • Trusting amr/acr without checking the IdP includes them — many don’t. Implement gracefully (treat missing as lowest tier).
  • Caching step-up MFA forever in the session — defeats step-up. Record a timestamp and re-prompt after 5–15 minutes.
  • Allowing SMS as the only factor — vulnerable; backup factor should be at minimum TOTP.
  • No rate limiting on TOTP — code space is 1M, brute force succeeds within seconds without limits.
  • Hardcoding the MFA requirement bypass for admins — admins are the highest-value targets. They need MFA the most.

Common interview confusions

  • “Password + security question is MFA.” — no, both are “something you know.” MFA needs factors from different categories.
  • “SSO removes the need for MFA.” — SSO centralizes MFA at the IdP. The IdP still enforces it; your app just trusts the result. MFA may be required by the IdP regardless of SSO.
  • “Step-up means re-entering password.” — broader: requires fresh / stronger authentication for sensitive operations. Could be re-password, MFA, biometric, depending on the app.

Interview angle

  • “Where is MFA enforced in an SSO architecture?” — at the IdP. The IdP runs the second factor; your app reads amr/acr claims to know what happened, and decides if it’s sufficient.
  • “What’s step-up authentication?” — demanding fresh/stronger auth for sensitive operations within an existing session. E.g. user is logged in, but transferring money requires re-authenticating with MFA. Implement via /authorize?prompt=login&acr_values=mfa.
  • “What’s the OIDC acr claim?” — Authentication Context Class Reference; a value indicating the strength/type of authentication. Apps can require minimum strength via acr_values in the authorize request.
  • “Why is SMS MFA discouraged?” — vulnerable to SIM swap, SS7 attacks, SMS interception. NIST 800-63B has discouraged it for high-security since 2017. Prefer TOTP, push (with number matching), or passkeys.
  • “What’s MFA fatigue and how do you defend against it?” — attackers spam push prompts hoping the user approves one to make them stop. Defenses: number matching (user enters displayed code, not just “approve”), rate limiting, alerting on multiple denied prompts.
  • “How do you implement TOTP correctly?” — generate per-user secret, QR enrollment, verify with ±1 window for clock skew, rate limit attempts, provide recovery codes, encrypt secrets at rest.