Home / Blog / OAuth 2.0 flows in practice: authorization code, PKCE, client credentials

OAuth 2.0 flows in practice: authorization code, PKCE, client credentials

OAuth 2.0 is the most widely used authorization protocol. Four flows, each for a different scenario. A practical usage guide.

OAuth 2.0 is not authentication, it’s an authorization protocol. It says “user X has granted permission to access this resource”. It’s the cornerstone of the modern API ecosystem. Google Login, Facebook Login, GitHub OAuth, Stripe Connect: all OAuth 2.0.

Most developers know only one OAuth flow. There are actually four, each for a different client type. Here’s how they’re used in practice.

Core concepts

Actors:
Resource Owner: the user
Client: third-party app (your app)
Authorization Server: identity provider (Google, Facebook, your own)
Resource Server: the protected API

Tokens:
Access token: used on API calls, short-lived (1 hour)
Refresh token: used to get a new access token, long-lived (days/months)

Flow 1: Authorization Code (the most common)

For web apps. Server-to-server token exchange.

Process:

  1. User clicks “Login with Google”
  2. Browser redirects to Google’s authorization_endpoint
  3. User logs in at Google and grants permission
  4. Google redirects the browser to redirect_uri with a code parameter
  5. Your backend POSTs the code to Google and receives access_token plus refresh_token
  6. Tokens are attached to the user session

URL example:

https://accounts.google.com/o/oauth2/v2/auth?
    client_id=YOUR_CLIENT_ID&
    redirect_uri=https://yourapp.com/auth/callback&
    response_type=code&
    scope=email profile&
    state=random_csrf_token

Google redirect:

https://yourapp.com/auth/callback?code=AUTH_CODE&state=random_csrf_token

Backend code to token exchange:

response = requests.post('https://oauth2.googleapis.com/token', data={
    'code': code,
    'client_id': CLIENT_ID,
    'client_secret': CLIENT_SECRET,
    'redirect_uri': 'https://yourapp.com/auth/callback',
    'grant_type': 'authorization_code'
})
tokens = response.json()
# {access_token, refresh_token, expires_in}

Security: the client secret stays on the backend. It never leaks to the browser.

Flow 2: Authorization Code plus PKCE

For mobile or native apps. A “public client” can’t keep a secret.

PKCE (Proof Key for Code Exchange): the client generates a random secret (code_verifier), sends its hash (code_challenge) in the authorization request, and proves possession with the code_verifier at token exchange.

Process:

  1. Client generates a code_verifier (random 43 to 128 chars)
  2. code_challenge = SHA256(code_verifier) (base64url encoded)
  3. Send code_challenge in the authorization URL
  4. Send code_verifier at token exchange
  5. Server matches the hash and issues a token

iOS Swift example:

let verifier = generateRandomString(length: 64)
let challenge = sha256(verifier).base64UrlEncoded

// Add code_challenge to the authorization URL
let authURL = "https://oauth.provider.com/authorize?code_challenge=(challenge)&code_challenge_method=S256&..."

// Token exchange
let tokenRequest = URLRequest(url: tokenEndpoint)
tokenRequest.httpBody = "code=(code)&code_verifier=(verifier)&...".data(using: .utf8)

PKCE is the mobile OAuth standard. Recommended in the RFC since 2019.

Flow 3: Client Credentials

Server-to-server. No user context, the app authorizes on its own behalf.

Use case: a backend job needs access to an API service, not user data. “Your app reports to an admin API”.

Process:

  1. Backend goes directly to the token endpoint with client_id and client_secret
  2. Receives a token
  3. Uses the token on API calls
response = requests.post(token_endpoint, data={
    'grant_type': 'client_credentials',
    'client_id': CLIENT_ID,
    'client_secret': CLIENT_SECRET,
    'scope': 'api.read api.write'
})

No user involvement. Good for backend scripts, cron jobs, admin tasks.

Flow 4: Resource Owner Password Credentials

Deprecated. The user hands their username/password to the app, and the app exchanges them for a token.

Problem: the user’s password touches the app. Security anti-pattern.

Only in: legacy system migration, or first-party apps (your company’s app plus your own OAuth server), and other edge cases.

Never use it in a new system.

Token refresh

Access token expired (1 hour). Get a new one without re-authorizing the user:

response = requests.post(token_endpoint, data={
    'grant_type': 'refresh_token',
    'refresh_token': stored_refresh_token,
    'client_id': CLIENT_ID,
    'client_secret': CLIENT_SECRET  # confidential client
})

New access_token plus optionally a new refresh_token.

Refresh token rotation: for security, issue a new refresh_token on each refresh and invalidate the old one. Shrinks the window a leaked token can be used in.

Scopes

A scope is the permission scope. What the user is granting.

Example Google scopes:
email: email address
profile: name, photo
https://www.googleapis.com/auth/drive.readonly: Drive read
https://www.googleapis.com/auth/drive.file: Drive file write

Principle of least privilege: only request the scope you need. Every extra scope strains the user’s trust.

Dynamic scope request: ask for extra scopes when the user flow actually needs them. Google’s incremental authorization pattern.

Common pitfalls

1. Forgetting the state parameter.

The state parameter on the authorization URL is CSRF protection. You send a random token and check it matches in the callback.

state = generate_random_token()
session['oauth_state'] = state
# Add state to the authorization URL
# In the callback: assert request.args['state'] == session['oauth_state']

2. Storing the access token in plain text.

In LocalStorage, in a non-HttpOnly cookie, in a personal data folder. Use encrypted storage.

iOS: Keychain. Android: EncryptedSharedPreferences. Web: HttpOnly secure cookie.

3. Weak redirect URI validation.

The redirect_uri must be on the authorization server’s registered list. Don’t use wildcards. Open redirect attack risk.

4. Missing token expiration handling.

When the access token expires, API calls return 401. The client needs to grab a new token via refresh and retry the request. Middleware layer.

5. Silent scope elevation.

Don’t try to request more scope via refresh. Refresh only renews the existing scope.

OAuth 2.0 vs OpenID Connect

OAuth 2.0 is authorization only. “This app has access to the resource.” It doesn’t say who the user is.

OpenID Connect (OIDC) is built on top of OAuth 2.0. It adds authentication. The id_token (JWT) carries user info.

Modern “Login with X” implementations use OIDC.

ID Token example:

{
  "sub": "user_12345",
  "email": "user@example.com",
  "name": "Ali Çınaroğlu",
  "iss": "https://accounts.google.com",
  "aud": "your_client_id",
  "exp": 1700000000
}

Verify the signature and user identity is verified.

Implementation tools

Writing your own OAuth client is a slog. Use battle-tested libraries:

Backend:
– Python: authlib, requests-oauthlib
– Node.js: passport, openid-client
– Go: golang.org/x/oauth2
– PHP: league/oauth2-client

Frontend:
– JS: oidc-client-ts, react-oidc-context
– iOS: AppAuth-iOS, Apple ASWebAuthenticationSession
– Android: AppAuth-Android

Rolling your own OAuth invites subtle bugs. Use a library.

Security checklist

For a production-ready OAuth implementation:

  • [ ] HTTPS everywhere (never HTTP)
  • [ ] State parameter for CSRF
  • [ ] PKCE on mobile/native clients
  • [ ] Tokens in encrypted storage
  • [ ] Refresh token rotation
  • [ ] Short access token TTL (1 hour max)
  • [ ] Token revocation endpoint support
  • [ ] Strict redirect URI matching
  • [ ] Minimum scope
  • [ ] ID token signature verification (OIDC)

Gaps in this checklist are open doors to a security breach.

Wrap-up

OAuth 2.0 is the glue of the modern API ecosystem. Four flows, one per client type. Authorization code plus PKCE on mobile, Authorization code on web, Client credentials for server-to-server, Password deprecated.

PKCE, state parameter, refresh rotation, token storage discipline. Use implementation tools, don’t roll your own. Keep the security checklist in view.

OAuth is complex but mature. With a solid library plus spec knowledge, secure auth is doable.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch