Yashveer Singh
Connect
<- All posts
Backend, APIs, and System Design6 min read

OAuth 2.0 Without Tears: A Founder Engineer's Guide

OAuth 2.0 is an authorization framework that allows applications to access resources on behalf of a user without handling or storing the user's credentials. The user authorizes the application through an authorization server (Google, GitHub, Okta) and the application receives an access token it can use to call APIs. OAuth 2.0 defines several flows (authorization code, implicit, client credentials, device code) for different use cases. Implementing the wrong flow or mishandling tokens introduces security vulnerabilities.

Written by Yashveer Singh, founder of Yashveer Labs.

What you need to know

  • OAuth 2.0 is an authorization framework, not an authentication protocol. OpenID Connect (OIDC) adds the identity layer that makes "sign in with Google" possible.
  • The authorization code flow with PKCE is the correct flow for web applications and mobile apps. The implicit flow is deprecated. Do not use it in new implementations.
  • Access tokens are short-lived credentials for API calls. Refresh tokens are long-lived credentials for obtaining new access tokens. Both must be stored securely and both have different security risk profiles.
  • The state parameter in OAuth flows is a CSRF protection mechanism. Not validating it allows attackers to inject authorization codes from a different session.
  • Using a maintained OAuth library prevents the most common implementation mistakes. Manual OAuth implementation introduces subtle vulnerabilities that library authors have already solved.

The core argument

OAuth 2.0 has a reputation for being complex, and implementations done without careful attention to the security requirements produce vulnerabilities that are not obvious until they are exploited. The complexity is real but manageable when understood correctly. The protocol has a small number of core concepts (client, resource server, authorization server, access token, refresh token) and a small number of flows (authorization code, client credentials, device code). The security properties of each flow follow logically from those concepts.

The authorization code flow works as follows: the user is redirected to the authorization server (Google, GitHub) with a request that includes the client's identity, the requested permissions, and a state value. The user authenticates and approves the request at the authorization server. The authorization server redirects back to the application with a code and the state value. The application validates the state, exchanges the code for an access token in a server-side call, and uses the access token for API calls. The PKCE extension adds a code verifier and challenge to this flow, which prevents an intercepted authorization code from being exchanged for a token by a different party.

The security mistakes I see most often in OAuth implementations are token storage mistakes. Access tokens stored in localStorage are accessible to any JavaScript on the page, including third-party scripts. An XSS vulnerability that injects JavaScript can exfiltrate all localStorage tokens. The correct storage for web applications is httpOnly cookies (not accessible to JavaScript) for refresh tokens, and in-memory storage (a JavaScript variable) for access tokens, with the understanding that the access token is lost on page refresh and must be obtained from the refresh token each time. This is not how most tutorial implementations work, which is why tutorial-based OAuth implementations often have storage vulnerabilities.

Common mistakes

  1. Using the implicit flow for a single-page application. The implicit flow returns access tokens as URL fragments. These appear in browser history, are sent in Referer headers, and are logged by server access logs. The authorization code flow with PKCE provides equivalent functionality without the token exposure. All new SPA implementations should use authorization code with PKCE.
  1. Not validating the state parameter on the callback. The state parameter is a random value generated before the authorization redirect that the authorization server echoes back on the callback. Validating that the returned state matches the generated value prevents CSRF attacks. Skipping this validation is a security vulnerability in the implementation.
  1. Implementing OAuth manually instead of using a maintained library. Libraries like Auth.js, Passport.js, and the official Okta and Auth0 SDKs implement the flows correctly and handle the edge cases. Manual implementations often miss token validation, PKCE implementation details, or token rotation logic.
  1. Storing refresh tokens insecurely in client-side applications. Refresh tokens in localStorage are accessible to any JavaScript on the page. Mobile applications should use the platform's secure storage (Keychain on iOS, Keystore on Android). Web applications should store refresh tokens in httpOnly, Secure, SameSite=Strict cookies.
  1. Not implementing token revocation. When a user logs out, the access token and refresh token should be revoked at the authorization server. Applications that only delete the local token without revoking it at the server allow the token to be used until it expires, which for refresh tokens may be weeks or months.

Where to start

  1. For a new implementation: choose a maintained OAuth library before writing any auth code. For Next.js, Auth.js (formerly NextAuth) handles OIDC and OAuth 2.0. For Node.js backends, Passport.js has strategies for most providers. For React Native, the Expo Auth Session library handles authorization code with PKCE correctly.
  1. Review the token storage strategy in any existing implementation. Find where access tokens and refresh tokens are currently stored. If they are in localStorage, that is a security gap that should be addressed. Document the storage location and the risk before the next review cycle.
  1. Verify that the state parameter is being validated. Find the OAuth callback handler in the codebase and confirm that the state parameter returned from the authorization server is compared against the state value stored before the redirect. If it is not, add that validation.

Related reading

FAQ

Frequently asked

Author

Why I am built for this project type

I have worked on five production systems before turning eighteen. That is not a flex. That is a statement of capability. Yashveer Singh, founder of Yashveer Labs. The work in this article is the work I do on a weekly basis. If you are facing the problem I just described, I do not need to be sold on solving it. I need to be told the constraints.

Related reading