In this guide
Every time you click "Sign in with Google" and get redirected to Google, approve access, and land back in the app — that is OAuth2. It powers logins for millions of apps and lets your project access a user's Google Drive, GitHub repos, or Spotify playlists without ever seeing their password.
OAuth2 looks intimidating because the documentation is written for specification lawyers. The underlying idea is simple, and this guide walks through it the way you would explain it to a friend: who the players are, what messages they exchange, and which flow to use for your project.
The four players
Every OAuth2 interaction involves four roles:
| Role | Who it is | Example |
|---|---|---|
| Resource Owner | The user | You, granting access to your photos |
| Client | Your application | Your project website |
| Authorization Server | Issues tokens | Google's accounts server |
| Resource Server | Holds the protected data | Google Photos API |
The golden rule of OAuth2: your app never sees the user's password. The user authenticates directly with the provider (Google, GitHub), and your app receives a token — a limited permission slip — instead of credentials.
Why not just use passwords?
Imagine your project needs to read a user's Google Calendar. The naive approach: ask for their Google password. This is terrible because:
- Your app now holds a master key to their entire Google account
- The user cannot revoke access to just your app — they must change their password
- If your database leaks, attackers get real passwords
OAuth2 fixes all three: the app gets a token with limited scope ("read calendar only"), the user can revoke it anytime from their Google account page, and a leaked token is far less valuable than a password.
The Authorization Code flow, step by step
This is the flow you will use for server-side web apps — the most common and most secure. Here is the complete dance:
Step 1 — Your app redirects the user to the provider. Your login button sends the user to the authorization server with parameters describing what you want:
GET https://accounts.google.com/o/oauth2/v2/auth
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/auth/callback
&response_type=code
&scope=openid%20email%20profile
&state=RANDOM_STRING_YOU_GENERATE
Key parameters:
client_id— identifies your app (public, not secret)redirect_uri— where the provider sends the user back; must match what you registered exactlyscope— what permissions you are asking for; ask for the minimum you needstate— a random value you generate; the provider echoes it back so you can verify the response matches your request (prevents CSRF attacks)
Step 2 — The user logs in and consents. The user authenticates with the provider directly (your app is not involved) and sees a consent screen: "YourApp wants to: view your email address, view your profile." They approve or deny.
Step 3 — The provider redirects back with a code. If the user approves, the provider redirects to your redirect_uri with a short-lived authorization code:
GET https://yourapp.com/auth/callback?code=AUTH_CODE_HERE&state=RANDOM_STRING_YOU_GENERATE
This code is useless by itself — it is not a token, and it expires in minutes. Think of it as a claim ticket.
Step 4 — Your server exchanges the code for tokens. Your backend (never the browser) sends the code to the provider's token endpoint along with your client secret:
import requests
response = requests.post("https://oauth2.googleapis.com/token", data={
"code": "AUTH_CODE_HERE",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET", # server-side only, never in frontend code
"redirect_uri": "https://yourapp.com/auth/callback",
"grant_type": "authorization_code",
})
tokens = response.json()
# tokens["access_token"] -> use this to call APIs
# tokens["refresh_token"] -> use this to get new access tokens later
# tokens["id_token"] -> JWT with the user's identity (OpenID Connect)
Step 5 — Call APIs with the access token. Include it in the Authorization header:
profile = requests.get(
"https://www.googleapis.com/oauth2/v3/userinfo",
headers={"Authorization": "Bearer " + tokens["access_token"]},
).json()
Step 6 — Refresh when it expires. Access tokens are short-lived (often 1 hour). When one expires, use the refresh token to get a new access token without bothering the user. The JWT refresh token rotation guide covers the rotation pattern in depth.
Note: OpenID Connect (OIDC) is a thin identity layer on top of OAuth2. When you add
scope=openid, you get anid_token— a signed JWT containing the user's identity (name, email, profile picture). "Sign in with Google" is OIDC. OAuth2 alone is authorization (access to resources); OIDC adds authentication (who the user is).
The other flows (and when you meet them)
| Flow | Used for | Key idea |
|---|---|---|
| Authorization Code | Server-side web apps | The full dance above; most secure |
| Authorization Code + PKCE | Mobile apps, SPAs | Adds a per-request secret so public clients (no backend) stay safe |
| Client Credentials | Server-to-server | Your backend talks to an API as itself, no user involved |
| Device Code | TVs, CLI tools, IoT | User visits a URL on their phone and enters a code shown on the device |
| Implicit (legacy) | Old browser apps | Returns tokens directly in the URL — deprecated, do not use in new projects |
PKCE (pronounced "pixy") deserves a sentence: mobile apps and single-page apps cannot keep a client secret (anyone can decompile the app or read the JS). PKCE replaces the secret with a code challenge/verifier pair generated fresh for each login. If your frontend talks to the provider directly, use Authorization Code + PKCE — every major provider supports it.
Scopes: ask for less than you want
Scopes define what your token can do. email profile is polite; https://www.googleapis.com/auth/drive (full Drive access) makes users nervous and triggers stricter provider review. Rules:
- Request the narrowest scope that works (
drive.readonlyinstead of fulldrive) - Explain on your login page why you need each permission
- Some providers show a scary warning screen for sensitive scopes — design around it, not through it
Debugging the flow: errors you will meet
OAuth2 failures are cryptic the first time. The common ones:
| Error | Usual cause | Fix |
|---|---|---|
redirect_uri_mismatch |
The redirect URI in your request does not exactly match a registered one (trailing slash, http vs https, port) | Copy the registered URI character-for-character |
invalid_grant |
Authorization code expired (they live minutes), already used, or code came from a different client | Exchange codes immediately, use each code once |
access_denied |
User clicked "deny" on the consent screen | Handle gracefully — show a friendly message, not a stack trace |
| State mismatch | The state returned does not match what you sent |
Your session handling is broken — fix session storage before shipping |
invalid_scope |
You requested a scope that does not exist or your app is not approved for | Check the provider's scope documentation; request narrower scopes |
Two debugging habits: first, most providers have an admin dashboard showing recent auth attempts with error details — look there before guessing. Second, log the full authorization URL your app generates during development; nine times out of ten the bug is a wrong parameter you can see by reading it.
Common mistakes
- Putting the client secret in frontend code or a mobile app. It is called a secret. If it ships to a browser, it is public. Use PKCE for public clients instead.
- Skipping the
stateparameter. Without it, an attacker can trick a user into logging into your app with the attacker's account (login CSRF). Always generate, store, and verifystate. - Not validating the redirect URI. Register exact redirect URIs with the provider; never accept a
redirect_urifrom user input at runtime. - Storing tokens in localStorage. XSS attacks can read localStorage. Prefer httpOnly cookies or secure backend session storage. The OWASP Top 10 guide explains the XSS angle.
- Treating the access token as a session. Access tokens expire. Build the refresh flow from day one, not as an afterthought.
- Requesting every scope "just in case". Users abandon consent screens that ask for too much.
Quick implementation checklist
- Registered your app with the provider; noted client ID and secret
- Registered exact redirect URIs (including localhost for development)
- Backend endpoint that builds the authorization URL with random
state - Callback endpoint that verifies
state, exchanges the code server-side - Tokens stored securely (server session or httpOnly cookie — not localStorage)
- Refresh logic so users are not logged out every hour
- "Disconnect" option that revokes the token with the provider
Where to go from here
- For the token mechanics underneath (structure, expiry, rotation), read JWT Refresh Token Rotation Done Right.
- The provider-agnostic alternative: Firebase Authentication for web apps handles OAuth2 for you.
- Secure the rest of your app with the OWASP Top 10 for student web apps.
- Serve your callback URLs over HTTPS — how to get a free SSL certificate.
- More backend topics in the Web Development branch hub.