Need the #1 custom application developer in Brisbane?Click here →

Security

OAuth 2.0 Deep Dive

12 min readLast reviewed: March 2026

OAuth 2.0 solves a fundamental problem: how do you let users grant your app access to their data on another service without giving your app their password? This guide explores the protocol, common flows, and how to implement it securely.

The Problem OAuth Solves

Before OAuth, if you wanted to let users log in via Google or sync their photos from Dropbox, you had to ask for their password. You'd store it and use it to access their data. This is terrible for security: the user trusts Google or Dropbox, not your app, with their password. If your app is hacked, the attacker gets the password and access to the user's account.

OAuth fixes this with delegation: instead of getting the password, you get a token that grants limited access. The user logs in at Google and grants your app permission to access specific data (email, profile). Google issues a token, and your app uses that token. If your app is compromised, the token grants only limited access and can be revoked.

OAuth Players

OAuth involves four parties:

Resource Owner: The user. They own the data and grant permission.

Client: Your app. It wants to access the user's data.

Authorization Server: The provider's authentication endpoint (e.g., Google's login). It authenticates the user and issues tokens.

Resource Server: The provider's API endpoint. It validates the token and returns protected resources (user profile, photos, etc.).

OAuth 2.0 Grant Types

OAuth 2.0 defines several "grant types"—different flows for different client types. The two most common are Authorization Code and Client Credentials.

Authorization Code Flow (Web Apps)

This is the flow for traditional web apps. The user clicks "Login with Google." Your app redirects them to Google's login page. Google authenticates them and asks for permission: "Example App wants access to your email and profile. Allow?" The user approves. Google redirects back to your app with an authorization code.

Your backend exchanges the code for an access token by making a request to Google's token endpoint, using your app's client secret. Only your backend knows the secret, so only your backend can exchange the code for a token. Once you have the token, you use it to fetch the user's profile from Google's API.

Why this dance? The authorization code is useless without the client secret. If an attacker intercepts the redirect, they get the code, but can't exchange it for a token. The code is also single-use.

PKCE Flow (SPAs and Mobile)

PKCE (Proof Key for Public Clients) is an extension to the Authorization Code flow for apps that can't securely store a client secret (SPAs and mobile apps). Instead of a static secret, PKCE uses a dynamic proof.

Your app generates a random string (the "code verifier") and hashes it to create a "code challenge." It includes the challenge in the authorization request. After getting the authorization code, it exchanges it for a token, including the original code verifier. The server verifies the verifier hashes to the challenge. This prevents attackers who intercept the authorization code from using it because they don't have the original verifier.

Client Credentials Flow (Machine-to-Machine)

This flow is for backend services calling other backend services. There's no user involved. Your service sends its client ID and secret to the authorization server and receives an access token. No user interaction, no redirect.

This flow is simpler but also higher risk. The client secret is your app's password—if it's exposed, an attacker can impersonate your service. Store secrets in a secrets management system (AWS Secrets Manager, HashiCorp Vault), never in code.

Refresh Token Flow

When the authorization server issues an access token, it might also issue a refresh token. Access tokens are short-lived (hours). When an access token expires, your app uses the refresh token to get a new access token without asking the user to log in again.

This is particularly useful for offline scenarios—your app can use the refresh token to fetch new access tokens even after the user has logged out.

OAuth vs OpenID Connect (OIDC)

A common confusion: OAuth 2.0 is about authorization (granting access), not authentication (proving identity). It doesn't say who the user is, just that they granted permission.

OpenID Connect (OIDC) is an identity layer on top of OAuth 2.0. It adds an ID token (a JWT containing identity claims) alongside the access token. When you exchange the code for tokens, you get both. The ID token says "The user is john@example.com"; the access token says "This user granted you access to their calendar." Most modern "OAuth providers" (Google, GitHub) actually implement OIDC.

OAuth for Authorization, OIDC for Authentication
Use OAuth to get access tokens for APIs. Use OIDC to get ID tokens for user authentication. In practice, most providers offer both, and libraries handle the details. The distinction matters when integrating multiple services or building your own OAuth provider.

Common OAuth Providers

The three biggest OAuth providers are Google, GitHub, and Microsoft. Most web apps integrate with at least one. Each provider has slightly different implementation details, but they all follow the OAuth 2.0 standard.

Google: Supports Authorization Code, PKCE, and ID Token flows. Provides profile, email, and calendar access. Good for consumer apps.

GitHub: Supports Authorization Code flow. Provides user info and repository access. Good for developer tools.

Microsoft/Azure: Supports Authorization Code, PKCE, and Client Credentials. Provides Microsoft 365 access. Good for enterprise apps.

Building OAuth Integration

Step 1: Register Your App

Go to the provider's developer console and register your app. You'll get a client ID and client secret. Register a redirect URI—the URL where the provider should send the authorization code. For local development, it might be http://localhost:3000/auth/callback; for production, https://example.com/auth/callback.

Step 2: Redirect to Authorization Endpoint

When the user clicks "Login with Provider," redirect them to the provider's authorization endpoint with your client ID, requested scopes (what data you want), and the redirect URI. For Google, it might be: https://accounts.google.com/o/oauth2/v2/auth?client_id=XXX&scope=email%20profile&redirect_uri=https://example.com/auth/callback&response_type=code

Step 3: Handle the Callback

The provider redirects the user back to your redirect URI with an authorization code: https://example.com/auth/callback?code=abc123&state=xyz. Your backend receives the code and the state parameter (a CSRF prevention token you generated in step 2). Verify the state matches, then exchange the code for a token.

Step 4: Exchange Code for Token

Your backend makes a POST request to the provider's token endpoint with the code, client ID, and client secret. The provider responds with an access token (and optionally a refresh token and ID token). Store the access token to make API calls on the user's behalf.

Step 5: Fetch User Info

Use the access token to call the provider's API to fetch the user's profile. Include the token in the Authorization header: Authorization: Bearer token123. The provider returns the user's email, name, profile picture, etc. Create or update your local user record with this data.

OAuth Security Pitfalls

Missing State Parameter

The state parameter prevents CSRF attacks. You generate a random string, include it in the authorization request, and verify it in the callback. If an attacker tricks a user into clicking a link that initiates an OAuth flow, the state will be missing or wrong, and you'll reject it.

Redirect URI Validation

Only allow redirects to URIs you registered. An attacker might request an authorization code with a redirect URI under their control. If you don't validate it, the code redirects to their site, and they steal the code. The provider validates the redirect URI against your registered list, but double-check in your callback handler too.

Token Leakage

Access tokens should never appear in URLs or logs. Always use HTTPS. Don't include tokens in error messages or debug output. Store tokens securely—if you're storing a user's refresh token in your database, encrypt it. Tokens can be as powerful as passwords, so treat them accordingly.

Never Expose Client Secret
Your client secret is like a password. Never include it in frontend code, URLs, or public repositories. Only your backend should have it. If you accidentally leak it, regenerate it immediately in the provider's console.

OAuth Libraries vs Rolling Your Own

OAuth is complex, with many edge cases and security considerations. Many battle-tested libraries exist (Passport.js for Node, python-social-auth for Python, Devise for Rails). Using a library is safer than rolling your own—libraries handle state generation, token rotation, PKCE verification, and other details correctly.

If you must build it yourself, use a well-maintained OAuth library. Don't implement cryptographic operations or token signing yourself. Focus on the flow logic; let the library handle the cryptography.

When to Use OAuth vs Username/Password

OAuth is excellent when users already have accounts at popular providers (Google, GitHub) and trust those providers. It eliminates password management burden from your team. But not all apps should use OAuth exclusively.

Use OAuth for consumer-facing apps where users expect social login. Use traditional passwords for internal tools, enterprise apps, or applications where users might not have Google accounts. Use both—let users register with email/password or OAuth. This maximizes adoption while offloading authentication complexity when possible.

OAuth is powerful when used correctly, but it's a protocol, not a magic solution. Understand the flows, implement the security checks, and use a library you trust. The effort is worth it—users love not having to manage another password.