
OIDC Token Management: Access, ID, and Refresh Token Strategies
Examines strategies for managing OpenID Connect access tokens, ID tokens, and refresh tokens including rotation, caching, and revocation.
The Mechanics of OIDC Token Lifecycle
In the architecture of OpenID Connect (OIDC), tokens are not merely data strings; they are the physical manifestation of state and trust. To manage them securely, we must stop thinking of them as "credentials" in the traditional sense and start viewing them as transient, bounded capabilities. The confusion often arises when developers treat the Access Token, ID Token, and Refresh Token as interchangeable or apply uniform security policies to all three. This fails because each token serves a distinct mechanism within the protocol: one grants temporary access to resources, one asserts identity at a specific moment, and one acts as a long-lived key to generate new short-lived keys.
This article is Part 4 of the OpenID Connect Advanced Series.
The Three-Layer Architecture
Consider a user, Alice, logging into a banking application via an Identity Provider (IdP) named "SecureAuth". When Alice authenticates, SecureAuth issues three distinct artifacts.
The ID Token is a JSON Web Token (JWT) signed by SecureAuth. Its sole mechanism is to assert who Alice is to the Client Application (the banking app). It contains claims like sub (subject identifier), name, and email. Crucially, the ID Token is intended for the Client only. If the Client stores this token and sends it to a backend API, the API should reject it. The mechanism here is "identity assertion," not "access control." The ID Token is a snapshot of Alice's state at the moment of login. If her password changes or her profile updates after login, the ID Token remains stale until a new one is issued. This is why the ID Token has a short exp (expiration) claim, typically minutes, to limit the window of stale identity data.
The Access Token is the mechanism for authorization. It is opaque to the Client (often a random string) or a JWT, depending on the IdP configuration. Its sole purpose is to be presented to a Resource Server (the banking API) to prove Alice has permission to read her account balance. The Resource Server does not care who Alice is; it cares if the token is valid and if it has the scope accounts:read. The Access Token is the "key" to the door. Because it is the active credential, it has the shortest lifespan, usually minutes. If an attacker steals an Access Token, they have a very short window to exploit it before it expires naturally.
The Refresh Token is the mechanism for renewal. It is a long-lived credential (hours or days) that allows the Client to obtain a new Access Token without re-prompting Alice for her password. Unlike the Access Token, which is sent to the Resource Server, the Refresh Token is sent only to the Authorization Server. This separation of duties is critical. The Refresh Token is the "master key" to the vault of access tokens. If an attacker steals a Refresh Token, they can generate infinite new Access Tokens until the Refresh Token itself is revoked or expires. Therefore, the security requirements for the Refresh Token are exponentially higher than for the Access Token.
Rotation: Breaking the Replay Chain
The most common misconception in token management is assuming that a Refresh Token can be reused indefinitely. In a naive implementation, a client receives a Refresh Token, uses it to get a new Access Token, and then reuses the same Refresh Token. This creates a vulnerability known as "token replay." If an attacker intercepts the Refresh Token once, they can use it forever.
The mechanism to defeat this is Refresh Token Rotation (RTR).
When Alice's client requests a new Access Token using a Refresh Token, the Authorization Server performs two actions simultaneously:
- It issues a new Access Token.
- It issues a new Refresh Token and invalidates the old one.
This creates a "one-time pad" dynamic for the refresh cycle. Let's trace the data flow with named actors.
- Actor A: Alice's Browser (Client).
- Actor B: SecureAuth (Authorization Server).
- Actor C: Attacker (Intercepting the network).
Scenario 1: Normal Operation
- Alice's Browser sends
refresh_token=RT_Ato SecureAuth. - SecureAuth validates
RT_A, generatesaccess_token=AT_1andrefresh_token=RT_B. - SecureAuth marks
RT_Aas "used" in its database. - The Browser stores
RT_Band discardsRT_A.
Scenario 2: Attack Attempt
- Attacker intercepts the response containing
RT_A(or captures it from a previous log). - Attacker sends
refresh_token=RT_Ato SecureAuth. - SecureAuth checks its database, sees
RT_Awas already consumed in Scenario 1. - SecureAuth returns an
invalid_tokenerror.
This mechanism ensures that even if a Refresh Token is stolen, it is valid only once. The tradeoff is that the Authorization Server must maintain state (a database of used tokens) rather than relying solely on cryptographic verification. For high-scale systems, this state management can be optimized using distributed caches like Redis with TTLs, but the mechanism of invalidation remains the same.
Opinion: While stateless Refresh Tokens (where the token itself contains the validity state) are theoretically possible, they are generally discouraged for production OIDC implementations. RFC 8693 (Refresh Token Rotation) and RFC 8628 outline this as a recommended best practice rather than a core OIDC mandate. Stateful rotation on the server is widely adopted because it prevents the client from validating the token's internal signature and expiration, thereby reducing the attack surface on the client side.
Caching: The Storage Problem
Where these tokens live matters as much as how they rotate. The mechanism of storage determines the vector for theft.
If a developer stores the Access Token or Refresh Token in localStorage or sessionStorage within the browser, they expose the tokens to any JavaScript running on the page. This is the classic Cross-Site Scripting (XSS) vector. If an attacker injects malicious script, they can simply call localStorage.getItem('access_token') and exfiltrate the data.
The httpOnly Cookie Mechanism
To mitigate this, modern OIDC implementations often store the Refresh Token (and sometimes the Access Token) in an httpOnly cookie.
- Mechanism: The browser sets a cookie with the
HttpOnlyflag. - Effect: JavaScript cannot read the cookie.
document.cookiewill not return the value. - Data Flow: The browser automatically attaches the cookie to requests to the same domain.
This effectively blocks XSS from stealing the Refresh Token directly. However, it introduces a different risk: Cross-Site Request Forgery (CSRF). If the browser automatically sends the cookie, an attacker could trick Alice into visiting a malicious site that submits a form to the banking API, causing the browser to send the stolen Refresh Token. To counter this, the server must enforce the SameSite attribute on cookies (ideally Strict or Lax) and use CSRF tokens or the Origin header validation.
Server-Side Caching For Single Page Applications (SPAs) where the client is purely a view layer, the most secure pattern is Server-Side Session Storage.
- Flow: The client sends credentials to the backend. The backend validates them and creates a session ID.
- Storage: The client stores only the session ID (in an
httpOnlycookie). The actual tokens are stored in the backend's memory or database. - Benefit: The client never holds the sensitive tokens. If the client is compromised, the attacker only gets the session ID, which can be rotated or invalidated by the server more granularly.
Opinion: For public clients (like SPAs), using httpOnly cookies for the Refresh Token is the current best practice. Storing tokens in JS variables or local storage is an anti-pattern that should be avoided in any security-conscious application.
Common Pitfalls
Implementing OIDC token management correctly requires avoiding several common architectural traps:
- XSS Exposure via Local Storage: Storing tokens in
localStorageorsessionStorageis the most frequent vulnerability. Any JavaScript injection can read these values. Even if the server implements rotation, the client-side storage remains the weakest link. Always preferhttpOnlycookies or server-side sessions. - CSRF Risks with Cookies: While
httpOnlycookies protect against XSS, they introduce CSRF risks. If theSameSiteattribute is not set strictly, or if CSRF tokens are omitted, an attacker can force the user's browser to submit requests that rotate tokens or access resources. - Stale ID Tokens: Developers often assume the ID Token is always fresh. However, if a user's permissions change or their profile updates on the server, the ID Token (which is a snapshot) remains stale until a new one is issued. Relying on a cached ID Token for long periods can lead to authorization errors or incorrect user data display.
Practical Takeaways
To manage OIDC tokens effectively, adopt these mental models:
- The Disposable Key Model: Treat the Access Token as a disposable key. It is meant to be used once or for a very short duration and then discarded. Do not attempt to persist it longer than necessary.
- The Master Key Constraint: View the Refresh Token as the master key. If you lose the master key, you lose the ability to generate new keys. This dictates that Refresh Tokens must be stored with the highest level of security (e.g.,
httpOnlycookies) and rotated immediately upon use. - State is Security: Accept that stateful management (databases for rotation and revocation) is a feature, not a bug. Stateless tokens shift the burden of security validation to the client, which is inherently less secure. Trust the server to manage the state of validity.
FAQ
Q: Can I store refresh tokens in localStorage?
A: No. Storing refresh tokens in localStorage exposes them to any JavaScript running on the page, making them vulnerable to XSS attacks. If an attacker can run JavaScript, they can steal the refresh token and generate new access tokens indefinitely. Use httpOnly cookies instead.
Q: What happens if the Identity Provider (IdP) goes down? A: If the IdP is unavailable, the client cannot validate tokens via introspection, cannot rotate refresh tokens, and cannot revoke tokens. Applications should implement graceful degradation, such as allowing users to continue with cached short-lived access tokens until they expire, while preventing new logins or token rotations until the IdP is restored.
Q: Do I need to implement a revocation endpoint if my tokens are short-lived? A: Yes. While short-lived access tokens minimize the window of opportunity for attackers, they do not prevent the misuse of a stolen refresh token. Immediate revocation of refresh tokens upon logout or suspicious activity is essential to cut off the source of new tokens, regardless of the access token lifespan.
Revocation: The Hard Stop
Eventually, a token must die. This happens when Alice logs out, when her account is compromised, or when the Refresh Token reaches its maximum lifetime. The mechanism for this is the Token Revocation Endpoint, defined in RFC 7009 (OAuth 2.0 Token Revocation), which OIDC implementations typically adopt. It is important to note that while OIDC relies on OAuth 2.0 for this functionality, the core OIDC specification itself does not mandate a specific revocation endpoint.
When Alice clicks "Log Out" in the banking app, the app must not just clear its local storage. It must actively tell the Authorization Server to kill the tokens.
The Revocation Protocol
- The Client sends a POST request to the
/revokeendpoint of the Authorization Server. - The request includes the token (usually the Refresh Token or Access Token) and a client authentication credential (client ID and secret, or a private key).
- The Authorization Server verifies the request.
- The Authorization Server updates its database to mark the token as
revoked. - The Authorization Server returns a
200 OKresponse.
Once revoked, any subsequent attempt to use that token will result in an invalid_token error. However, the Resource Server (RS) cannot know a token is revoked automatically without additional logic. The RS must perform Token Introspection (calling the Authorization Server to check the token's status) or rely on a synchronized revocation list shared across the distributed system to detect the revocation status. Without this introspection or synchronization, a revoked token might still be accepted by a Resource Server node that hasn't updated its cache yet.
The Challenge of State This mechanism requires the Authorization Server to have a persistent store of revoked tokens. In a distributed system, this means the revocation list must be replicated across all nodes. If the revocation list is not synchronized, a token revoked on Node A might still work on Node B for a few seconds. To handle this, many systems use a "soft revoke" strategy where the token is marked for immediate invalidation in the cache layer, or they rely on short lifespans (e.g., 5-minute Access Tokens) to make the window of exposure negligible.
Immediate vs. Delayed Revocation
There is a tradeoff here. If you rely solely on the token's exp claim (expiration time), you are trusting the clock. If the clock is skewed or the token is stolen 1 second before expiration, the attacker has 1 second. If you rely on revocation, you are trusting the database. The most robust strategy combines both: short-lived Access Tokens (to minimize the window) and immediate revocation of Refresh Tokens upon logout or suspicious activity (to cut the source).
Conclusion
Managing OIDC tokens requires balancing usability with cryptographic hygiene.
- Identity: Use the ID Token only for the Client to know who the user is. Never send it to APIs.
- Access: Treat the Access Token as a disposable key. Keep it short-lived.
- Renewal: Implement Refresh Token Rotation. Never reuse a Refresh Token. This is the single most effective defense against token theft.
- Storage: Avoid
localStoragefor sensitive tokens. PreferhttpOnlycookies or server-side sessions. - Cleanup: Always call the revocation endpoint on logout. Do not assume clearing local storage is enough.
By adhering to these mechanisms, you ensure that the flow of trust remains intact, and the cost of compromise for an attacker rises to a level that makes the attack unviable.
Related posts
OpenID Connect Guide: Extending OAuth 2.0 for Identity Verification
An examination of OpenID Connect (OIDC) and how it extends OAuth 2.0 to handle identity verification using ID tokens and discovery protocols.
Angular OAuth2/OIDC: loadDiscoveryDocumentAndTryLogin
Learn how to use loadDiscoveryDocumentAndTryLogin and strict discovery document validation in Angular for secure OAuth2/OIDC authentication.
The AuthConfig Reference: Every Property That Matters
A complete reference for Angular-OAuth2-OIDC AuthConfig properties, covering requireHttps, remoteOnly, and nonceStateSeparator for secure Angular authentication.