Authorization Code Flow for Server-Rendered Apps
A traditional server-rendered web application is a confidential client: it can hold a client_secret safely on the server, so it uses the standard Authorization Code flow. The browser is redirected to the provider's /authorize endpoint with client_id, redirect_uri, scope, and state; after the user consents, the provider redirects back to a registered callback URL with a short-lived authorization code, which the server then exchanges directly, server-to-server, at the /token endpoint using its client_secret.
Cricket analogy: The redirect to /authorize is like a fan being sent from the stadium turnstile to a separate ticket-verification booth, and the callback with a code is like the booth handing back a stub that only the stadium's own back office can redeem for stand access.
Session Management and Token Storage
Because the code exchange happens server-side, the resulting access_token and refresh_token should stay on the server, held in a session store such as Redis or an encrypted database record, keyed by an opaque session identifier. The browser only ever receives an httpOnly, Secure, SameSite cookie containing that session ID, never the OAuth tokens themselves, which keeps the tokens out of reach of any JavaScript running on the page, including malicious injected scripts.
Cricket analogy: It's like the team management keeping player contracts locked in the franchise office (server-side session store), while each player only carries a numbered access card (session cookie) that means nothing outside the stadium's own systems.
// Express.js OAuth callback handler (server-rendered web app)
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state parameter');
}
const tokenResponse = await fetch('https://idp.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://app.example.com/callback',
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
}),
});
const tokens = await tokenResponse.json();
// Store tokens server-side; browser only ever gets a session cookie
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.tokenExpiry = Date.now() + tokens.expires_in * 1000;
res.redirect('/dashboard');
});State Parameter and CSRF Protection
The state parameter is a random, unguessable value the server generates and stores in the user's pre-authentication session before redirecting to /authorize; when the provider redirects back, the server compares the returned state against the stored value to confirm this callback corresponds to a request it actually initiated, blocking login CSRF attacks where an attacker tricks a victim into completing someone else's OAuth flow. Even for confidential server-side clients, OAuth 2.1 and current best practice recommend adding PKCE as a defense-in-depth measure against authorization code injection.
Cricket analogy: state is like a match referee issuing a unique toss-coin serial number before the game and checking it against the post-match report — without matching serials, you can't be sure the result being reported belongs to that specific toss.
Prefer a well-maintained OAuth/OIDC client library — such as openid-client, Passport.js strategies, or your framework's built-in OAuth middleware — over hand-rolling code exchange, state validation, and token refresh logic. These libraries correctly handle edge cases like clock skew, PKCE generation, and token validation that are easy to get subtly wrong by hand.
Refresh Token Rotation
Access tokens should be short-lived, often 5 to 15 minutes, to limit the damage if one leaks, while the longer-lived refresh_token is used to silently obtain new access tokens without re-prompting the user. Under refresh token rotation, the authorization server issues a brand-new refresh_token on every use and invalidates the old one; if an already-used refresh_token is presented again, the server treats it as a signal of theft and revokes the entire token family, forcing re-authentication.
Cricket analogy: It's like a bowler getting a fresh new ball every set number of overs rather than using one ball for the entire innings — and if the old ball mysteriously reappears in play, the umpires immediately suspect tampering and stop play.
Never store OAuth tokens in localStorage, sessionStorage, or a client-readable cookie in a server-rendered app — the whole point of keeping this a confidential, server-side flow is that tokens never reach the browser at all. If you find yourself passing an access_token into a client-side script, you've likely broken the security model this architecture is built on.
- Server-rendered apps are confidential clients and should use the standard Authorization Code flow with a client_secret.
- The authorization code is exchanged server-to-server at the /token endpoint, never in the browser.
- Tokens belong in server-side session storage (e.g., Redis); the browser should only hold an httpOnly session cookie.
- The state parameter must be validated on callback to prevent login CSRF attacks.
- Even confidential clients benefit from adding PKCE as defense-in-depth per OAuth 2.1 guidance.
- Refresh token rotation issues a new refresh_token on each use and revokes the token family if reuse is detected.
- Prefer established OAuth client libraries over hand-rolled implementations to avoid subtle security bugs.
Practice what you learned
1. Which OAuth client type is a traditional server-rendered web application?
2. Where should the access_token and refresh_token be stored in a server-rendered web app?
3. What does the state parameter protect against?
4. Under refresh token rotation, what happens if an already-used refresh_token is presented again?
5. Why is PKCE recommended even for confidential server-side clients under OAuth 2.1?
Was this page helpful?
You May Also Like
OAuth with OpenID Connect
Understand how OpenID Connect layers standardized authentication — ID tokens, claims, and the UserInfo endpoint — on top of OAuth 2.0's authorization framework.
OAuth for Single-Page Apps
Learn why browser-based SPAs must be treated as public OAuth clients, how PKCE secures the Authorization Code flow, and why the BFF pattern beats storing tokens in the browser.
OAuth with API Gateways
How API gateways centralize OAuth token validation, choose between JWT and introspection, enforce scope-based authorization, and apply per-client rate limits.
OAuth for Mobile Apps
How native iOS and Android apps should implement OAuth 2.0 per RFC 8252: external user-agents, PKCE, claimed redirect URIs, and hardware-backed token storage.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics