Why This Matters
Your application needs to integrate with third-party services like Google Workspace, Salesforce, and GitHub without collecting and storing user passwords. You've heard "just use OAuth," but the OAuth 2.0 specification defines multiple flows, and choosing the wrong one can expose your application to token theft, session hijacking, and unauthorized access.
A common mistake is implementing the Implicit Flow because it seems simpler or skipping PKCE because your documentation is outdated. The OAuth 2.0 framework, finalized by the IETF OAuth Working Group in 2012 (RFC 6749), has evolved significantly. What worked five years ago may now violate current security guidance.
This playbook guides you through implementing the Authorization Code Flow with PKCE (Proof Key for Code Exchange)—the recommended approach for most modern applications. You'll configure a working integration that meets PCI DSS v4.0.1 Requirement 6.4.3 for secure authentication mechanisms and aligns with OWASP ASVS v4.0.3 verification requirements.
Prerequisites
Access and Accounts:
- Admin access to your OAuth provider (Google, Okta, Auth0, etc.)
- Ability to register redirect URIs in your DNS
- SSL/TLS certificate for your callback endpoint (OAuth 2.0 requires HTTPS)
Technical Prerequisites:
- A web application with session management
- Backend server that can make HTTPS requests
- Secure storage mechanism for tokens (encrypted database, key management service)
- Logging infrastructure to track authorization events
Security Decisions:
- Determine necessary scopes (request the minimum—if you only need email, don't request calendar access)
- Set token lifetime policies (access tokens should expire in 15-60 minutes, refresh tokens in hours or days depending on risk)
- Decide where tokens will be stored (never localStorage—use httpOnly cookies or backend session storage)
Documentation Ready:
- Your OAuth provider's authorization endpoint
- Token endpoint URL
- Required headers and parameters
- Scope syntax (varies by provider)
Step-by-Step Implementation
Step 1: Register Your Application
Log into your OAuth provider's developer console. Create a new application registration. You'll receive:
- Client ID (public identifier)
- Client secret (treat this like a password—never commit to git, never expose in frontend code)
Configure your redirect URI exactly: https://yourapp.com/oauth/callback. The provider will reject requests with mismatched URIs.
Step 2: Generate PKCE Parameters
Before redirecting users, your backend generates a code verifier and code challenge:
import secrets
import hashlib
import base64
# Generate code verifier (43-128 characters)
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
# Create code challenge
challenge_bytes = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(challenge_bytes).decode('utf-8').rstrip('=')
# Store code_verifier in session - you'll need it later
session['oauth_code_verifier'] = code_verifier
PKCE prevents authorization code interception attacks. Even if an attacker steals the authorization code, they cannot exchange it for tokens without the original code_verifier.
Step 3: Build the Authorization Request
Construct the authorization URL with required parameters:
https://provider.com/oauth/authorize?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/oauth/callback
&scope=email%20profile
&state=RANDOM_STATE_VALUE
&code_challenge=CODE_CHALLENGE_FROM_STEP2
&code_challenge_method=S256
The state parameter must be a cryptographically random value stored in the user's session. This prevents CSRF attacks where an attacker tricks a user into authorizing the attacker's account.
Step 4: Handle the Callback
When the user authorizes your application, the provider redirects to your callback URI with an authorization code:
https://yourapp.com/oauth/callback?code=AUTH_CODE&state=STATE_VALUE
Your callback handler must:
- Verify the
statematches the session value - Extract the authorization code
- Retrieve the stored
code_verifier - Exchange the code for tokens
Step 5: Exchange Code for Tokens
Make a POST request to the token endpoint:
import requests
response = requests.post('https://provider.com/oauth/token', data={
'grant_type': 'authorization_code',
'code': authorization_code,
'redirect_uri': 'https://yourapp.com/oauth/callback',
'client_id': YOUR_CLIENT_ID,
'client_secret': YOUR_CLIENT_SECRET,
'code_verifier': code_verifier
})
tokens = response.json()
# Returns: access_token, refresh_token, expires_in, token_type
Step 6: Store Tokens Securely
Encrypt tokens before storage. If you're using PostgreSQL with pgcrypto:
INSERT INTO oauth_tokens (user_id, access_token, refresh_token, expires_at)
VALUES (
$1,
pgp_sym_encrypt($2, 'encryption_key'),
pgp_sym_encrypt($3, 'encryption_key'),
NOW() + INTERVAL '3600 seconds'
);
Never store tokens in plaintext. If your database is compromised, encrypted tokens give you time to rotate before attackers can use them.
Step 7: Use the Access Token
Include the access token in API requests:
Authorization: Bearer ACCESS_TOKEN
Check expiration before each request. If expired, use the refresh token to obtain a new access token without re-prompting the user.
Validation - How to Verify It Works
Test the Complete Flow:
- Clear all sessions and cookies
- Initiate authorization—you should be redirected to the provider
- Authorize the application
- Verify you're redirected back with a code parameter
- Check your logs—you should see a successful token exchange
- Make an API call using the access token—it should succeed
Security Validation Checklist:
- Authorization URL includes
code_challengeandcode_challenge_method=S256 - State parameter is validated on callback
- Redirect URI exactly matches registered URI (including trailing slashes)
- Tokens are encrypted at rest
- Access tokens expire within 60 minutes
- Client secret never appears in frontend code or logs
- HTTPS is enforced on all OAuth endpoints
- Token refresh works without user interaction
Test Failure Scenarios:
- Tamper with the state parameter—request should be rejected
- Modify the redirect URI—authorization should fail
- Attempt to reuse an authorization code—second exchange should fail
- Use an expired access token—API should return 401
Maintenance / Ongoing Tasks
Weekly:
- Review OAuth authorization logs for unusual patterns (multiple failed attempts, authorizations from unexpected locations)
- Check for tokens approaching their maximum lifetime
Monthly:
- Audit active refresh tokens—revoke any for inactive users
- Review requested scopes—remove any you're no longer using
- Verify redirect URIs are still valid and under your control
Quarterly:
- Rotate client secrets (coordinate with your OAuth provider)
- Review token encryption keys—plan rotation if needed
- Test your token revocation process
When OAuth Provider Updates:
- Read security advisories immediately
- Test your implementation against new endpoints
- Update deprecated parameters (OpenID Connect, introduced in 2014 as an identity layer on OAuth 2.0, continues to evolve)
Monitoring Alerts to Configure:
- Failed token exchanges (may indicate authorization code theft attempts)
- Tokens used from unexpected IP ranges
- Refresh token reuse (indicates possible token theft)
- Authorization requests with missing PKCE parameters
Your OAuth implementation is not set-and-forget infrastructure. Treat it like any other authentication system—monitor, audit, and update as threats evolve.



