A Google OAuth bug in my side project accidentally allowed citizens to access an admin dashboard.
Three months later, that same debugging experience helped me contribute to designing a secure cross-application authentication flow at work.
This post walks through the bug, how I fixed it, and how those same ideas translated into an enterprise SSO architecture.
Table of Contents
- The Side Project
- The Bug
- How I Fixed It
- The Enterprise Challenge
- Designing the Handoff Token Flow
- Security Considerations
- Lessons Learned
The Side Project
My emergency management platform, CrisisOps, has a fairly straightforward architecture.
- Node.js + Express
- PostgreSQL + Prisma
- Two frontend applications
- Citizen Portal
- Admin Dashboard
Both applications shared the same authentication backend.
Google OAuth handled authentication.
Everything worked...
Until it didn't.
The Bug
During testing, I discovered something worrying.
A citizen could:
- Sign in normally.
- Open the Admin Dashboard URL.
- Be treated as authenticated.
The backend correctly verified that the session existed.
What it never verified was whether that session belonged to the application requesting access.
Instead, the same authentication state leaked across application boundaries.
Architecture Diagram Here
The problem wasn't broken authentication.
It was cross-application session bleeding.
The backend answered:
"Is this session valid?"
But never asked:
"Is this session valid for this application?"
How I Fixed It
Rather than relying solely on authentication, I added multiple authorization layers.
1. Scope Sessions to Their Origin
Instead of issuing one generic session cookie, the backend first determines which frontend initiated the request.
const getAppSource = (req: Request): 'admin' | 'user' => {
const headerSource = (req.headers['x-app-source'] as string | undefined)?.toLowerCase();
if (headerSource === 'admin' || headerSource === 'user')
return headerSource;
const cookieSource = req.cookies?.oauth_from?.toLowerCase();
if (cookieSource === 'admin' || cookieSource === 'user')
return cookieSource;
const referer = (req.headers.referer || '').toLowerCase();
if (referer.includes('admin'))
return 'admin';
return 'user';
};
Based on that context, the backend issues application-specific cookies.
admin_accessTokenadmin_refreshTokenuser_accessTokenuser_refreshToken
Now each frontend only accepts sessions intended for it.
Note
The
Refererheader is only a fallback.In production, the frontend always sends a custom
X-App-Sourceheader, making the origin explicit instead of relying on browser behavior.
2. Reject Unauthorized OAuth Requests Early
Instead of issuing tokens first and checking permissions later, the OAuth callback validates the destination before authentication completes.
const from =
req.cookies?.oauth_from === 'admin'
? 'admin'
: 'user';
if (from === 'admin' && googleUser.role === 'CITIZEN') {
return res.redirect(
`${fallbackUrl}/auth/error?message=${encodeURIComponent(
'Access denied'
)}`
);
}
If a citizen attempts to authenticate into the Admin Portal, the login ends immediately.
No admin cookies are ever created.
3. Protect Every Endpoint
Authentication tells the system who you are.
Authorization determines what you're allowed to do.
The backend enforces a role hierarchy.
const ROLE_HIERARCHY: Record<UserRole, number> = {
CITIZEN: 0,
RESPONDER: 1,
DISPATCHER: 2,
ORG_ADMIN: 3,
GOV_ADMIN: 4,
SUPER_ADMIN: 5,
};
Beyond role checks, routes can also require individual permissions such as:
incidents:assignresources:deployaudit:read
That keeps authorization flexible without creating dozens of specialized roles.
The Enterprise Challenge
A few months later, our engineering team faced a similar—but much larger—problem.
Users needed to move between multiple internal applications with one click.
No login screen.
No password prompt.
Just seamless authentication.
Immediately we started asking questions.
- How do we prevent JWT leakage?
- How do we stop replay attacks?
- How do we defend against XSS?
- How do we protect against CSRF?
Our backend engineer proposed using a Handoff Token, inspired by OAuth 2.0 Token Exchange (RFC 8693).
Because I had already spent time solving authentication boundaries in CrisisOps, the architecture immediately made sense.
Designing the Handoff Token Flow
Instead of passing a user's JWT directly between applications, we issue a short-lived, single-use transition token.
Architecture Diagram Here
The destination application exchanges that token for its own session.
The original JWT never crosses trust boundaries.
Security Considerations
Every design decision addressed a specific attack surface.
Preventing Token Leakage
Rather than storing sessions in local storage, the exchanged session is returned as an HttpOnly, Secure, SameSite=Strict cookie.
Even if an attacker successfully executes JavaScript through an XSS vulnerability, the session cookie remains inaccessible.
Preventing Replay Attacks
The handoff token is:
- Single-use
- Valid for only a few seconds
- Immediately invalidated after exchange
Even if intercepted, it becomes practically useless.
Preventing CSRF
Every transition includes a nonce (or state parameter) generated by the originating application.
The Identity Provider validates this value before issuing a handoff token, preventing unauthorized cross-site transitions.
Lessons Learned
Looking back, the OAuth bug wasn't just a bug.
It was my introduction to trust boundaries.
Three lessons stuck with me.
- Verify at application boundaries—not just user identity.
- Never pass long-lived tokens between applications.
- Side projects teach production architecture in unexpected ways.
The debugging session that once felt like an annoying weekend problem eventually became the mental model I relied on during an enterprise authentication design discussion.
Sometimes the bugs that frustrate us the most become the ideas we build systems around.
Final Thoughts
One of the biggest reasons I continue building side projects is that they let me fail in places where failure is inexpensive.
Those failures often become experience long before they're needed professionally.
Have you ever had a side project teach you something that later became useful at work?
I'd love to hear your story.


Top comments (0)