Why OAuth2 for SPAs Is Harder Than the Tutorials Make It Look
You’ve probably been there: building a Single Page Application (SPA), needing authentication, and thinking “OAuth2 is a standard, this should be straightforward.” Then you dive into the implementation and realize the tutorials all conveniently skip the hard parts.
Three weeks later, you’re debugging redirect loops at midnight, questioning your life choices, and wondering why something that’s supposed to be “standard” feels so fragile.
After years of working on identity solutions and helping teams implement OAuth2 in production SPAs, I can tell you that this frustration is universal. Here’s what those tutorials don’t tell you about OAuth2 in SPAs.
Over the next few sections, we’ll walk through the specific ways OAuth2 implementations break down in production: the grant type confusion that leads teams astray, the impossible token storage decisions, the refresh token nightmares that cause random logouts, and why even popular SDKs don’t actually solve these fundamental problems. Then we’ll explore a different architectural approach that sidesteps these issues entirely.
The Grant Type Maze That No One Explains
Every OAuth2 tutorial starts the same way: “First, choose your grant type!” followed by a neat little comparison table. What they don’t mention is that the “right” answer has changed multiple times, and half the information on the internet is outdated.
In my experience working with identity systems, I’ve seen the same grant type mistakes over and over, often discovered only during security audits or production incidents.
The Implicit Grant Legacy Problem
Most tutorials from 2018 to 2020 recommended the Implicit grant for SPAs, but the OAuth2 Security Best Current Practices RFC now considers it harmful. As of OAuth 2.1, the Implicit grant has been completely removed from the specification due to security vulnerabilities with tokens exposed in URL fragments. Despite this, outdated blogs and Stack Overflow answers still point developers down this dangerous path.
The Client Credentials Confusion
Here’s a common developer thought process: “My API is protected and requires a token to invoke it. Let me just use Client Credentials to get a token.” This seems logical at first glance, especially when you’re frustrated with OAuth2 complexity and just want something that works.
This is fundamentally wrong for SPAs, and I see it more often than you’d expect. Client Credentials is designed for server-to-server communication where you can securely store secrets. In a SPA:
- The client secret is exposed in the JavaScript bundle
- Anyone can extract it and impersonate your application
- You can’t differentiate between users, everyone has the same permissions
I’ve encountered production applications doing exactly this during security reviews. The conversation usually starts with “why is your client secret visible in your JavaScript bundle?” and ends with emergency remediation work.
Authorization Code + PKCE: The “Current” Recommendation
So you switch to the Authorization Code flow with PKCE because that’s what the security best practices recommend. But here’s the thing: these recommendations keep evolving, and what’s “secure” today might be deprecated tomorrow.
The Moving Target Problem: OAuth 2.0 originally recommended Implicit flow for SPAs. Tutorials, Stack Overflow answers, and official documentation all said it was the right choice for browser-based applications. Then security researchers found issues, and suddenly everyone’s telling you to use PKCE instead. Now OAuth 2.1 mandates PKCE for all clients and removes the Implicit flow entirely.
What’s next? Will there be OAuth 2.2 with new mandatory requirements? Will PKCE be replaced by something else? You’re not just implementing authentication, you’re constantly chasing evolving security standards, refactoring working code, and explaining to stakeholders why the “best practice” from six months ago is now considered harmful.
From an identity provider’s perspective, I’ve watched teams spend more time maintaining auth code than building the features their users actually want.
Token Storage: The Problem No One Wants to Talk About
The tutorials make token storage look simple: “Just store the token!” But each browser storage option comes with security trade-offs and implementation headaches.
Cookies vs. HTML5 Storage: Pick Your Security Risk
Cookies are familiar but vulnerable to both XSS and CSRF attacks. Regular cookies can be accessed by any JavaScript on your page. HttpOnly cookies solve the XSS problem but create a new one: your SPA can’t access them via JavaScript, so you need to build and maintain your own backend proxy for every API call.
localStorage and sessionStorage seem like obvious choices for SPAs. They’re easy to use and don’t get sent with every request. But they can be accessed via JavaScript, making them vulnerable to XSS attacks. Any script running on your page can read localStorage.getItem(‘access_token’). This includes third-party analytics scripts, compromised CDN resources, malicious browser extensions, and any XSS vulnerability in your application.
Browser Memory vs. Web Workers: UX vs. Complexity
Storing tokens only in JavaScript memory sounds secure: no persistence, no cross-tab access, harder for attackers to reach. But the user experience is terrible. You lose your session every time you refresh the page, navigate to another site and come back, or experience any JavaScript error that reloads the page.
Web Workers run in a separate context, making tokens inaccessible to the main thread. But they create debugging complexity, multi-tab synchronization issues, and still don’t solve persistence problems.
Every storage option forces you to choose between exposing tokens to XSS attacks, losing user sessions on page refresh, or building complex authentication infrastructure that you’ll spend months debugging.
Token Refresh: Where Everything Falls Apart
Token refresh is where most OAuth2 implementations completely break down in production. The tutorials show the happy path, “just use the refresh token to get a new access token”, but production is full of edge cases.
The Multi-tab Synchronization Nightmare
Modern OAuth security best practices require refresh token rotation, issuing a new refresh token each time an access token is refreshed and immediately invalidating the previous one. Each browser tab runs independently, but they all start with the same refresh token. When one tab refreshes and gets new tokens, the other tabs are left holding invalidated refresh tokens. Users get randomly logged out of some tabs but not others.
The Concurrent Request Problem
Here’s a scenario I’ve debugged multiple times: A dashboard makes multiple API calls on page load. When tokens expire, all calls fail simultaneously and trigger concurrent refresh attempts. The first refresh succeeds and invalidates the refresh token. The remaining attempts fail, causing the auth server to revoke all tokens. Users get logged out unexpectedly.
The race condition questions multiply: Do you trigger multiple refresh attempts? Queue the failed requests? How do you prevent the second refresh request from using an already-invalidated token? Most teams don’t handle this correctly.
Token refresh turns from a simple “get new token” operation into a complex distributed systems problem with race conditions and state synchronization issues.
The SDK Trap: Hiding Complexity, Not Solving It
At this point, many developers turn to OAuth2 SDKs. “Don’t reinvent the wheel”, so you pick a popular library and dive into the configuration.
Get Senthalan Kanagalingam’s stories in your inbox
Join Medium for free to get updates from this writer.
The problem with SDKs isn’t that they’re hard to use, it’s that they make you think you’ve solved OAuth2, when really you’ve just hidden the complexity behind an API.
The False Security of Abstraction
SDKs abstract away the implementation details, but all those OAuth2 problems we just discussed? They’re still there:
Token storage dilemma: The default storage method may vary, and you have to configure it correctly to balance between UX and security, you’re still vulnerable to XSS attacks, you just don’t realize it. The security risk doesn’t disappear because it’s hidden behind auth.getToken().
Token refresh nightmares: The SDK handles refresh automatically, until it doesn’t work across multiple tabs and users get randomly logged out. Now you’re debugging someone else’s refresh logic instead of your own.
Configuration complexity: Your auth config file still grows into a monster with dozens of options, and the documentation assumes you already know which ones you need. You still need to become an OAuth2 expert to configure the SDK correctly.
SDK updates break things regularly. I’ve seen teams stuck with libraries they picked six months ago that now require refactoring half their auth code to upgrade. You think you’ve solved OAuth2, but you’ve just moved the problems into a black box that breaks in production.
The Real-World Consequences
But the frustration goes beyond just technical complexity. These OAuth2 implementation challenges create real business impact:
Security incidents and compliance violations: Token leakage through browser storage, session hijacking, and privilege escalation require incident response and expensive remediation. Auditors flag tokens in browser storage as security risks, triggering months of compliance work.
Development velocity suffers: Teams spend 30–40% of their sprint capacity on auth-related fixes instead of building features. Product roadmaps slip because you’re constantly firefighting authentication issues instead of delivering value.
The cost isn’t just technical debt, it’s real business impact that compounds over time.
The Fundamental Problem: Tokens Don’t Belong in Browsers
Looking at all these problems together reveals a pattern: we kept trying to solve the wrong problem.
We keep trying to make browser-based token management secure, but browsers are fundamentally insecure environments. Think about it — you’re putting sensitive credentials in a runtime that executes arbitrary JavaScript from multiple sources, where any XSS vulnerability or malicious script can access your tokens. Every “solution” just shuffles the same security risks around while creating new problems.
Store tokens securely? Users lose sessions on page refresh. Make the UX smooth? You’re back to XSS vulnerabilities. Use complex SDKs? Now you’re debugging someone else’s authentication logic. Roll your own? You’ve just signed up to become an OAuth2 expert and maintain complex security code forever.
The core insight:
“Tokens don’t need to exist in browser JavaScript at all”
The industry is quietly moving toward Backend-for-Frontend (BFF) architectures for exactly these reasons. Your SPA makes API calls through a BFF layer that handles OAuth complexity and securely forwards requests to your APIs. The browser never sees an access token, refresh token, or any OAuth complexity.
This isn’t a new pattern — it’s how server-rendered applications have worked forever. We just got distracted by the “single page” part and forgot that SPAs can still have backends. Instead of browser-based token management, the BFF handles OAuth2 flows, manages tokens securely, and proxies API requests. The browser only needs to make normal HTTP requests.
A Different Approach: Platform-Managed Authentication
This is exactly why we built managed authentication into Choreo. After watching countless teams struggle with OAuth2 complexity, the pattern became clear — developers kept saying the same thing: “We just want authentication that works reliably without becoming OAuth2 experts.”
Rather than asking teams to become OAuth2 experts or build their own BFF layers, we implemented the BFF pattern at the platform level. This eliminates the burden of implementing OAuth/OIDC requirements and removes the need for applications to manage tokens issued by identity providers.
Enable Authentication with One Toggle
Teams enable Choreo’s managed authentication and follow clear guides on which grant types to enable, without having to manage tokens, handle refresh logic, or deal with the complex state synchronization issues we discussed earlier. Teams can easily connect with any OIDC-compliant identity provider directly from the Choreo console.
Add a Login Button That’s Surprisingly Simple
To allow Choreo to manage sign-in functionality, teams implement a simple authentication flow:
// Sign in
<button onClick={() => window.location.href = '/auth/login'}>
Sign In
</button>
// Sign out
<button onClick={() => window.location.href = '/auth/logout'}>
Sign Out
</button>
// Check if logged in
fetch('/auth/userinfo')
.then(response => response.ok ? response.json() : null)
.then(user => console.log(user?.name || 'Not logged in'));API Calls Without Token Management
For APIs deployed in Choreo, invocations work relative to the app URL. APIs are invoked via <app-url>/choreo-apis/<api-path>. The authenticated session details are used during API invocation to enforce access control automatically.
No manual token attachment, no refresh logic, no concurrent request handling:
// Just call APIs normally
fetch('/choreo-apis/orders')
.then(response => response.json())
.then(data => setOrders(data));Stop Fighting OAuth2, Start Shipping Features
Your users don’t care about OAuth2 implementation details. They want your application to work reliably. Every hour you spend debugging token refresh logic is time you’re not spending on features that matter to your business.
When you’re staring at browser network tabs at 2 AM wondering why tokens work in Chrome but fail in Safari, or explaining to stakeholders why the security best practice from six months ago is now considered harmful, remember: this problem is already solved.
Next time you’re planning authentication for a production SPA, ask yourself: Do you want to spend the next month becoming an OAuth2 expert, or do you want to ship features your users actually care about?
Ready to skip the OAuth2 complexity? Try Choreo’s managed authentication — it’s free to get started, and your future self will thank you when you’re shipping features instead of debugging token refresh race conditions.
OAuth2 doesn’t have to be the hardest part of your SPA development. Sometimes the best code is the code you don’t have to write.








