OAuth2 Flow Explained for Students

"Sign in with Google" runs on OAuth2, but the docs read like legal contracts. This guide walks through the authorization code flow step by step — the four roles, the code exchange, PKCE, scopes — and the security mistakes that break student implementations.

Written by Projectech8 min readPublished
For B.E./B.Tech Computer Science and IT students adding social login or third-party API access to web apps who have heard of OAuth2 but not how the flow works Topics: OAuth2, OpenID Connect, APIs, Authentication
Illustration of the OAuth2 authorization code flow showing a user, an app, and a provider exchanging redirects, codes, and tokens.
Illustration generated for this guide.
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 exactly
  • scope — what permissions you are asking for; ask for the minimum you need
  • state — 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 an id_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.readonly instead of full drive)
  • 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 state parameter. Without it, an attacker can trick a user into logging into your app with the attacker's account (login CSRF). Always generate, store, and verify state.
  • Not validating the redirect URI. Register exact redirect URIs with the provider; never accept a redirect_uri from 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

More project guides

More in Web Development