Imagine logging into your work laptop, opening five different apps, and not being asked for a password once. That is the promise of Single Sign-On (SSO), a mechanism allowing users to authenticate once to access multiple applications. Now imagine that same session getting hijacked because you never had to prove it was really you again. That is the risk.
The challenge for developers and security teams today isn't just building login screens. It is designing the logic that decides *when* to ask a user to prove their identity. This process, known as prompting, sits at the intersection of convenience and security. We need to balance the frictionless experience of SSO with the rigorous checks of Multi-Factor Authentication (MFA), a security method requiring two or more verification factors, all while using standard protocols like OAuth 2.0, an authorization framework granting limited access via tokens.
Understanding the Core Protocols
To control how users log in, we first need to understand the engine driving the process. Many people confuse OAuth 2.0 with a login system. It is not. OAuth 2.0 is an authorization framework. It allows an application (the client) to get limited access to a user's resources on another service without sharing passwords. Think of it as handing someone a key to your mailbox, not your house keys.
For actual login functionality, we use OpenID Connect (OIDC), an authentication layer built on top of OAuth 2.0. OIDC adds an ID token to the mix. This token contains claims about the user, such as their name, email, and crucially, when they last authenticated. This timestamp, known as the auth_time claim, becomes the heartbeat of our prompting strategy.
When implementing these flows, the choice of protocol matters. The Authorization Code Flow is currently the gold standard for most web applications. It exchanges a temporary code for tokens over a secure back-channel, keeping secrets safe. Older methods like the Implicit Flow are largely deprecated due to security vulnerabilities where tokens could be exposed in browser history or logs. For modern single-page applications (SPAs), the industry has shifted toward Authorization Code with PKCE (Proof Key for Code Exchange), which adds an extra layer of protection against interception attacks.
The Role of SSO and Session Management
SSO works by creating a shared session between an Identity Provider (IdP) and multiple Client Applications. When you log into the IdP, it sets a cookie in your browser. As long as that cookie is valid, you can navigate between connected apps without re-entering credentials. This reduces "password fatigue" and improves productivity.
However, this convenience creates a blind spot. If an attacker steals that SSO cookie, they inherit access to every app linked to that session. This is why relying solely on SSO is dangerous. You must pair it with robust session management. Access tokens issued during this process should be short-lived-typically between 15 and 60 minutes. Refresh tokens keep the session alive but should be rotated regularly. If a refresh token is stolen, rotation ensures the old one becomes useless immediately, limiting the window of opportunity for an attacker.
Controlling Prompts with OIDC Parameters
This is where the rubber meets the road. How do you tell the system, "Hey, even though there is an active SSO session, I want the user to log in again right now?" You use specific parameters defined in the OIDC specification.
The prompt parameter is your primary remote control. By default, if a user has a valid SSO session, the IdP will skip the login screen entirely. To override this, you send prompt=login in the authorization request. This forces the IdP to ignore existing cookies and require fresh credentials. Use this sparingly. You might trigger it before a high-risk action, like changing a payment method or updating security settings.
Another critical tool is max_age. This parameter specifies the maximum time since the last authentication. For example, sending max_age=1800 tells the IdP: "Only let me in if the user authenticated within the last 30 minutes." If the session is older than 1,800 seconds, the IdP interrupts the flow and asks for re-authentication. This allows for granular control based on risk rather than blanket policies.
| Parameter | Function | Use Case Example |
|---|---|---|
prompt=login |
Forces full re-authentication, ignoring SSO cookies | Before changing password or billing details |
max_age=1800 |
Requires auth within last N seconds | High-value financial transactions |
auth_time check |
Client-side validation of login freshness | Verifying step-up authentication requirements |
Integrating MFA Without Breaking UX
Adding MFA is non-negotiable for security, but doing it poorly destroys user trust. The biggest mistake teams make is triggering MFA too often. If a user has to enter a code every time they open a new tab, they will find ways to bypass it or complain loudly.
The solution lies in understanding the difference between initial authentication and step-up authentication. During the initial login, MFA should be part of the standard flow. Once completed, that assurance level should persist for a reasonable window. This is where checking event.authentication.methods in your IdP hooks becomes vital. In platforms like Auth0, you can inspect whether MFA was already used in the current session. If it was, skip the prompt. If not, enable it.
Consider a scenario where a user accesses a dashboard. They logged in two hours ago with MFA. Should they see the MFA prompt again? Probably not. But if they try to transfer money, yes. Here, you combine max_age with MFA. You might set a policy where general navigation allows sessions up to 8 hours, but any transaction requires an auth_time within the last 15 minutes, triggering a step-up MFA challenge only when necessary.
Avoiding Common Pitfalls
One major anti-pattern is the "silent authentication loop." This happens when an application calls for a new token silently, but the IdP interprets this as a new login request, triggering MFA again. The result is an infinite loop of prompts. To fix this, ensure your silent token requests do not include prompt=login unless absolutely required. Instead, rely on the presence of a valid SSO session and validate the auth_time claim on the client side to determine if additional steps are needed.
Another pitfall is neglecting client-side validation. The IdP enforces prompt and max_age, but the client application must also verify the resulting ID token. Always check the auth_time claim. An attacker might manipulate the query string to bypass server-side checks, so your application logic needs to independently confirm that the login is recent enough for the action being performed.
Best Practices for Implementation
- Use Short-Lived Tokens: Keep access tokens to 15-60 minutes. Rely on refresh tokens for continuity.
- Validate State and Nonce: Always include
stateto prevent CSRF andnonceto prevent replay attacks. - Contextual MFA: Trigger MFA based on risk (new device, unusual location, high-value action) rather than time alone.
- Respect User History: Check previous authentication methods to avoid redundant MFA prompts.
- Secure Transport: Ensure all endpoints use HTTPS. No exceptions.
By treating authentication as a dynamic conversation rather than a static gate, you create systems that are both secure and usable. The goal is not to stop the user, but to verify them intelligently. With the right combination of OAuth 2.0, OIDC parameters, and thoughtful MFA integration, you can achieve exactly that.
What is the difference between OAuth 2.0 and OpenID Connect?
OAuth 2.0 is an authorization framework that handles permissions and access tokens. OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0 that adds ID tokens to verify user identity. You use OAuth to say "this app can read my calendar" and OIDC to say "I am John Doe."
How does the max_age parameter work in OIDC?
The max_age parameter specifies the maximum elapsed time in seconds since the last authentication. If the user's session is older than this value, the Identity Provider forces re-authentication. For example, max_age=1800 requires the user to have logged in within the last 30 minutes.
Why should I use prompt=login?
Using prompt=login forces the Identity Provider to ignore any existing Single Sign-On (SSO) sessions and require the user to enter credentials again. This is useful for high-security actions like changing passwords or making large financial transactions.
How can I prevent MFA prompt loops?
Prevent MFA loops by checking the authentication history in your Identity Provider hooks. If the user has already completed MFA in the current session, skip the prompt for subsequent silent token requests. Only trigger MFA for initial logins or specific high-risk actions.
What is the recommended lifetime for access tokens?
Access tokens should be short-lived, typically between 15 and 60 minutes. This limits the damage if a token is stolen. Long-term sessions should be maintained using refresh tokens, which should be rotated regularly to invalidate old ones.