OAuth Callback URLs: Redirect URIs, Authorization Code Flow, and Security
An OAuth callback URL (redirect_uri) is where the authorization server sends the user after they grant or deny access. The authorization code is appended as a query parameter. The client must exchange it for a token via a back-channel request — never expose tokens in the URL. Callback URLs must be pre-registered with the OAuth provider to prevent open redirect attacks.
The authorization code flow
1. Your app redirects the user to the auth provider with:
https://accounts.google.com/oauth2/auth
?client_id=your_client_id
&redirect_uri=https://yourapp.com/auth/callback
&response_type=code
&scope=email profile
2. User authenticates and grants permission on Google's page
3. Google redirects back to your callback URL:
https://yourapp.com/auth/callback?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7
4. Your server exchanges the code for tokens (back-channel, server-to-server):
POST https://oauth2.googleapis.com/token
client_id=...
client_secret=...
code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7
redirect_uri=https://yourapp.com/auth/callback
grant_type=authorization_code
5. Google returns:
{"access_token": "...", "id_token": "...", "expires_in": 3600}
The callback URL receives an authorization code — not the actual token. The code is short-lived (usually 1-10 minutes) and single-use. Exchanging it for a token requires the client secret, which only your server has.
Why two steps (code then token)?
The authorization code flows through the browser's URL bar, which means it appears in:
- Browser history
- Server access logs
- Referrer headers on subsequent requests
A code that can only be exchanged server-side (with your client_secret) is safe to expose briefly in a URL. An access token in a URL is a persistent credential — if it appears in a log, it's compromised for its entire lifetime.
redirect_uri must be registered with the OAuth provider — unregistered URIs enable open redirect attacks
GotchaOAuth / SecurityIf the authorization server didn't validate the redirect_uri, an attacker could send a user through OAuth with redirect_uri=https://attacker.com/steal and receive the authorization code (and potentially the token) at the attacker's server. OAuth 2.0 requires that redirect_uri be pre-registered and that the server verify it matches the registered value exactly. Some providers allow wildcards (https://yourapp.com/*), but exact matching is more secure — a subdomain takeover of any subdomain would otherwise be exploitable.
Prerequisites
- OAuth 2.0 flow
- Open redirect vulnerabilities
- PKCE
Key Points
- redirect_uri must be pre-registered with the auth provider — no wildcards unless the provider explicitly supports them.
- The server verifies redirect_uri on every authorization request and token exchange.
- PKCE (Proof Key for Code Exchange): for public clients (SPAs, mobile apps) without a client_secret, use PKCE to bind the code to the original requester.
- state parameter: a random value added to the auth request and verified on callback — prevents CSRF attacks on the OAuth flow.
Common callback URL patterns
// Express.js: handle the OAuth callback
app.get('/auth/callback', async (req, res) => {
const { code, state, error } = req.query;
// Verify state parameter (CSRF protection)
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state parameter');
}
if (error) {
return res.redirect(`/?error=${error}`); // user denied access
}
// Exchange code for token (back-channel)
const tokens = await exchangeCodeForToken(code, {
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
redirect_uri: 'https://yourapp.com/auth/callback',
grant_type: 'authorization_code',
});
req.session.accessToken = tokens.access_token;
res.redirect('/dashboard');
});
The callback handler must:
- Verify the
stateparameter matches what was sent in the initial request - Handle the
errorparameter (user denied, or other errors) - Exchange the code immediately (codes expire quickly)
- Store the token server-side, not in the URL
A single-page app (SPA) uses implicit flow: the auth server redirects with the access_token directly in the URL fragment (https://app.com/#access_token=...). What are the security implications?
mediumImplicit flow was designed for SPAs that couldn't keep a client_secret. The token appears in the URL fragment, which is never sent to servers.
AURL fragments are never sent to servers, so the token is completely safe
Incorrect.Fragments (#) aren't sent in HTTP requests to servers, which protects against server-side logging. However, the token can still be exposed through JavaScript access (window.location.hash), browser history, referrer headers in subsequent requests (some browsers include fragments), and postMessage if the page passes the URL to embedded iframes.BThe token in the URL fragment is visible to JavaScript on the page, stored in browser history, and potentially leaked via referrer headers — implicit flow is deprecated in OAuth 2.1 in favor of authorization code + PKCE
Correct!Implicit flow puts a long-lived access token in the URL, which is: stored in browser history (accessible to any script), potentially included in Referer headers when navigating to other pages, accessible to JavaScript on the same page and in iframes (XSS risk). OAuth 2.1 deprecates implicit flow. Modern SPAs use Authorization Code + PKCE: the SPA uses a code verifier/challenge instead of a client_secret, gets an authorization code (not a token) in the callback, and exchanges it for a token via a back-channel fetch(). The token is never in the URL.CAccess tokens in URL fragments always expire after 1 minute
Incorrect.Token expiry is set by the authorization server (typically 1 hour to 1 day). The expiry is independent of how the token was delivered.DImplicit flow is the recommended approach for SPAs because they can't store client secrets
Incorrect.This was historically true but is no longer the recommendation. Authorization Code + PKCE was designed specifically for public clients (SPAs, mobile apps) that can't store a client_secret securely.
Hint:What are all the places a URL fragment could be exposed? How does PKCE address this for SPAs?