OAuth 2.0 is the industry standard for delegated authorization. Almost every SaaS product implements it, either as a provider (allowing third-party apps to access your API) or as a consumer (letting users sign in with Google, GitHub, or Slack). The protocol itself is well-designed. The implementations are where things go wrong.
Mistake 1: Not validating the redirect URI
The redirect URI is where the authorization server sends the user after they approve access. If you do not validate this URI strictly, an attacker can substitute their own URL and steal the authorization code or access token. This is the single most exploited OAuth vulnerability.
Always use exact string matching for redirect URIs. Do not allow wildcard subdomains, path prefixes, or partial matches. Register every redirect URI explicitly in your OAuth configuration. If your application needs multiple redirect URIs (for different environments or features), register each one individually.
Mistake 2: Using the implicit flow
The implicit flow was designed for browser-based applications that could not securely store a client secret. It returns an access token directly in the URL fragment. This token is visible in browser history, server logs, and referrer headers. The implicit flow is officially deprecated by the OAuth working group.
Use the authorization code flow with PKCE (Proof Key for Code Exchange) instead. PKCE works in browsers, mobile apps, and server-side applications. It prevents authorization code interception attacks and does not expose tokens in URLs.
Mistake 3: Long-lived access tokens without refresh
Some implementations issue access tokens that last for days or weeks. If one of these tokens is compromised, the attacker has extended access. Instead, issue short-lived access tokens (15 minutes to 1 hour) and use refresh tokens for long-term access. Refresh tokens should be single-use and rotated on every use.
Implement token revocation so that when a user disconnects an integration or changes their password, all associated tokens are invalidated immediately. This seems obvious but many startups skip it and only discover the gap during a security assessment.
Mistake 4: Skipping the state parameter
The state parameter prevents CSRF attacks against the OAuth flow. Without it, an attacker can trick a user into authorizing a malicious application by initiating the OAuth flow from a forged page. Always generate a random, unguessable state value, store it in the user session, and verify it when the callback is received.
Mistake 5: Insufficient scope validation
When your application acts as an OAuth provider, third-party applications request specific scopes (permissions). A common mistake is granting all requested scopes without letting the user see or modify them. Always show users exactly what permissions an application is requesting and let them deny individual scopes where possible.
On the consumer side, request only the minimum scopes you need. An application that asks for read and write access to a user is entire GitHub account when it only needs to read their email address is a red flag that will reduce your OAuth consent rates.
Mistake 6: Storing tokens insecurely
Access tokens and refresh tokens are credentials. Treat them like passwords. Store them encrypted at rest in your database. Never log them. Never include them in error messages. Never store them in localStorage in the browser (use secure, HttpOnly cookies or in-memory storage instead).
If you are building a mobile app, use the platform keychain (iOS Keychain, Android Keystore) to store tokens. These provide hardware-backed encryption and are significantly more secure than shared preferences or plain file storage.
Need help with authentication security?
traztech helps startups implement secure OAuth flows, review authentication architectures, and fix security vulnerabilities before they get exploited.
Book a free strategy callMistake 7: Treating an access token as proof of identity
This is the error that produces the worst outcomes and it is the one engineers argue about the longest. OAuth 2.0 is an authorization protocol. An access token says that some client was granted permission to call some API on behalf of some resource owner. It does not say who is currently sitting at the keyboard, and it was never designed to.
The broken pattern looks like this. Your mobile app obtains a Google access token, sends it to your backend, and your backend calls Google's userinfo endpoint, gets back an email address, and logs that user in. It works in testing, so it ships. The problem is that any other application holding a Google access token for that user can send it to your endpoint and get a session. A malicious app with a completely unrelated purpose, granted only basic profile scope by the user, now has an account takeover primitive against your product.
The correct approach is OpenID Connect. Request an ID token, verify its signature against the provider's published keys, and then check three claims before you trust anything: iss matches the expected issuer, aud matches your own client ID, and exp has not passed. The audience check is the one that stops the attack above, because a token minted for someone else's client will fail it. If you take one thing from this article, take the audience check.
Mistake 8: Trusting the email claim without verifying it
Social login flows almost always match users by email address. If your provider returns an email claim and you link it to an existing account without checking email_verified, an attacker can register an account at a provider using a victim's email address, never confirm it, and then use that identity to take over the victim's account in your product.
There is a second version of this that hits B2B products specifically. A user signs up with an address at a company domain, leaves that company, and someone else is later issued the same address. Or a small company lets its domain lapse and an attacker buys it. If your product grants access to a shared workspace based on email domain matching, you have made domain ownership a security boundary without telling anyone. Domain-based auto-join is a genuinely useful feature, but it should require an explicit admin opt-in and it should be re-verified rather than trusted forever.
Mistake 9: One client configuration for very different clients
Teams frequently register a single OAuth client and use it for their web app, their mobile app, and their server integrations, because it is one fewer thing to manage. That forces the configuration down to the weakest member. A client secret shipped inside a mobile binary is not a secret, it is a string in an APK that anyone can extract in ten minutes. If that same client is marked confidential and allowed to use flows that assume secret confidentiality, you have handed those flows to whoever bothers to decompile.
Register separate clients. Public clients (mobile, single page apps) get authorization code with PKCE and no secret. Confidential clients (your backend) get a real secret held in a secrets manager. Machine-to-machine integrations get client credentials with their own narrow scopes, and those scopes should be nowhere near the ones your interactive users hold. When we run authentication reviews, an over-scoped service client is one of the more common findings, usually because it was created during a migration and given broad permissions to unblock a deadline.
What we actually try during a test
When assessing an OAuth implementation, the first thirty minutes go to the redirect URI handling, because it fails more often than anything else. We try appending a path, changing the port, adding a userinfo prefix such as https://[email protected], swapping the scheme, adding a trailing slash, and using an open redirect elsewhere on your own domain as the hop. That last one matters: if your marketing site has a ?next= parameter that redirects anywhere, and your OAuth config allows any path on that domain, you have an exact-match allowlist that still leaks codes.
After that we look at whether the authorization code is single use and whether it is bound to the PKCE verifier, whether the state value is actually checked or merely echoed, whether refresh token rotation detects reuse, and what happens to existing sessions and tokens when a password changes. Reuse detection is worth calling out separately. If a rotated refresh token is presented twice, the correct behavior is to revoke the entire token family, because the second presentation means one copy was stolen. Plenty of implementations simply return an error and leave the attacker's copy working.
The last pass is on logout. Ending a local session while leaving access tokens valid for another fifty minutes is defensible if you decided it deliberately and documented the window. It is usually not deliberate. Your users, and any auditor asking about session termination, assume logout means access ends.
What auditors and enterprise buyers ask
Authentication comes up in almost every enterprise security review, and the questions are more specific than people expect. Expect to be asked how session tokens are invalidated on termination, how long tokens live, whether MFA is enforced at the identity provider or by your application, how third-party integration access is revoked when an employee leaves, and whether you keep an auditable log of authorization grants and revocations.
For SOC 2, this lands in the logical access criteria and the evidence is mundane: a screenshot of your token lifetime configuration, a sample of revocation log entries, and a written statement of your session policy that matches what the code does. The gap that trips people up is the mismatch. The policy document says sessions expire after eight hours, the code says thirty days, and the auditor finds it in five minutes. Write the policy after you read the configuration, never before.
When something is already wrong
If you suspect tokens have been stolen, the order of operations matters. Revoke refresh tokens first, because they are the persistence mechanism. Access tokens will expire on their own if they are short-lived, which is the practical reason short lifetimes are worth the engineering. Rotate any client secrets and signing keys, and remember that rotating a signing key invalidates every token you issued, which is disruptive and is exactly why you want key rotation tested before you need it. Then work out which grants existed, which scopes they carried, and what those scopes could reach.
Your logs decide whether this investigation is possible. If you are not recording token issuance with client ID, user, scopes, and IP, you will not be able to tell an affected customer what was accessed. That conversation is far worse than the incident itself.
When you should not build this yourself
Most startups should not be running their own OAuth authorization server. If you are implementing sign-in for your product and you are choosing between a well-maintained identity provider and building the flows in-house, buy the provider. WorkOS, Auth0, Clerk, and the identity services from the major clouds have all had more attacker attention than your implementation ever will, and the enterprise features that show up in year two, meaning SAML, SCIM provisioning, and directory sync, are painful to retrofit.
The honest version of that advice cuts against selling assessment work, so here it is plainly: if you use a mainstream provider with default settings, keep secrets out of your client bundle, verify the audience claim, and check state, you have removed most of the exposure without hiring anyone. Bring in an outside review when you are the OAuth provider rather than the consumer, when you have built a custom token exchange between services, or when a customer's security team has already asked a question you could not answer. If that is where you are, the fixed-scope options on pricing and our security work are the right starting point, and a fractional CISO is the better fit if the underlying problem is that nobody owns these decisions.
Want this handled? Tell us what your buyer is asking for and we will tell you what the work involves, what it costs, and what you can do yourself.
Talk to usOr talk about a retainer