Best Practices for Securing OAuth in Web Applications

OAuth 2.0 has become the standard for API authentication, but implementing it securely remains challenging. With token theft, deprecated flows, and browser-specific vulnerabilities threatening web applications daily, developers need clear guidance on what actually works. This article covers the essential OAuth 2.0 security best practices you need in 2025, from RFC 9700 requirements to SPA-specific patterns.
Key Takeaways
- Never store OAuth tokens in localStorage or sessionStorage—use memory storage for SPAs
- Authorization Code Flow with PKCE is mandatory for all clients under OAuth 2.1
- Implement refresh token rotation and short-lived access tokens (15-30 minutes)
- Consider Backend for Frontend (BFF) pattern to keep tokens out of the browser entirely
Why OAuth Security Matters More Than Ever
OAuth protects your users’ data by eliminating password sharing between applications. Instead of giving third-party apps your credentials (like handing over your house keys), OAuth issues temporary, scoped tokens—more like giving a valet key that only starts the car.
But here’s the problem: tokens are valuable targets. A stolen access token grants immediate API access. Poor implementation choices—storing tokens in localStorage, using deprecated flows, or skipping PKCE—turn minor vulnerabilities into major breaches.
Core Security Risks and How to Stop Them
Token Theft and Storage
What not to do: Store tokens in localStorage or sessionStorage. Any XSS attack can read these values and exfiltrate them to attacker-controlled servers.
What to do: For SPAs, keep tokens in memory only. For server-side apps, store them in encrypted server-side sessions. Use short-lived access tokens (15-30 minutes) to limit damage from any theft.
Deprecated Flows to Avoid
The Implicit Flow is dead. RFC 9700 and OAuth 2.1 both prohibit it. Why? Because it sends tokens directly in URL fragments, exposing them to browser history, referrer headers, and any JavaScript on the page.
The Resource Owner Password Credentials flow should also be avoided—it defeats OAuth’s entire purpose by requiring apps to handle user passwords directly.
Always use: Authorization Code Flow with PKCE for all clients, including SPAs and mobile apps.
Discover how at OpenReplay.com.
Modern Standards: RFC 9700 and OAuth 2.1
PKCE Is Now Mandatory
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. Here’s how it works:
// Generate code verifier and challenge
const verifier = generateRandomString(128);
const challenge = base64url(sha256(verifier));
// Include challenge in authorization request
const authUrl = `https://auth.example.com/authorize?` +
`client_id=app123&` +
`code_challenge=${challenge}&` +
`code_challenge_method=S256`;
// Send verifier when exchanging code for token
const tokenResponse = await fetch('/token', {
method: 'POST',
body: JSON.stringify({
code: authorizationCode,
code_verifier: verifier
})
});
OAuth 2.1 makes PKCE mandatory for all authorization code flows, not just public clients.
Token Binding with mTLS or DPoP
Token binding ensures stolen tokens can’t be used by attackers. Two main approaches:
- mTLS (Mutual TLS): Binds tokens to client certificates
- DPoP (Demonstrating Proof of Possession): Uses cryptographic proof without certificates
Both prevent token replay attacks by cryptographically tying tokens to the legitimate client.
Refresh Token Rotation
Never reuse refresh tokens. Each refresh should issue a new refresh token and invalidate the old one. This limits the window for stolen refresh token abuse:
// Server-side refresh token handling
async function refreshAccessToken(oldRefreshToken) {
// Validate and revoke old refresh token
await revokeToken(oldRefreshToken);
// Issue new token pair
return {
access_token: generateAccessToken(),
refresh_token: generateRefreshToken(), // New refresh token
expires_in: 1800
};
}
SPA OAuth Security: Special Considerations
Single-page applications face unique challenges since all code runs in the browser. The browser is hostile territory—assume any data there can be compromised.
Backend for Frontend (BFF) Pattern
The most secure approach keeps tokens completely out of the browser. A lightweight backend proxy handles OAuth flows and maintains tokens server-side, using secure, httpOnly, sameSite cookies for the SPA session.
Token Handler Pattern
For teams wanting SPA benefits without backend complexity, the Token Handler pattern provides a middle ground. It uses a specialized proxy that:
- Handles OAuth flows
- Stores tokens securely
- Issues short-lived session cookies to the SPA
- Translates cookie-authenticated requests to token-authenticated API calls
If You Must Store Tokens in the Browser
When BFF isn’t feasible:
- Store tokens in memory only, never localStorage
- Use service workers to isolate token access
- Implement aggressive token expiration (5-15 minutes)
- Never store refresh tokens in the browser
Implementation Checklist
✓ Use Authorization Code Flow with PKCE for all clients
✓ Implement exact redirect URI matching
✓ Set token lifetimes to minimum viable durations
✓ Enable refresh token rotation for public clients
✓ Use state parameter for CSRF protection
✓ Implement token binding (mTLS/DPoP) for high-security applications
✓ For SPAs: Consider BFF or Token Handler patterns
✓ Monitor for OAuth 2.1 compliance requirements
Conclusion
Securing OAuth in web applications requires understanding both the protocol’s strengths and the specific threats facing your architecture. Start with RFC 9700’s requirements: mandatory PKCE, no implicit flow, and proper token handling. For SPAs, seriously consider keeping tokens out of the browser entirely through BFF or Token Handler patterns. These aren’t just best practices—they’re the minimum bar for OAuth security in 2025.
FAQs
No. Even apps without sensitive data shouldn't use localStorage for tokens. Stolen tokens can be used for account takeover, API abuse, and as stepping stones to more serious attacks. Always store tokens in memory or use server-side sessions.
Yes. OAuth 2.1 mandates PKCE for all authorization code flows, including confidential clients. PKCE adds defense in depth against authorization code interception attacks that client secrets alone cannot prevent, especially in scenarios involving compromised networks or malicious browser extensions.
Use the Backend for Frontend pattern where your backend manages refresh tokens and provides the SPA with short-lived access tokens via secure cookies. Alternatively, use silent authentication with prompt=none to get new tokens without user interaction when the access token expires.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.