OAuth 2.0 is a protocol that allows third-party applications to access user data on other services without giving away user credentials. It’s widely used for "Login with Google," "Login with Facebook," and similar features.
Let’s see it in action. Imagine a simple web app, my-app.com, that wants to access a user’s public profile information from a service called auth-server.com.
Here’s a simplified flow:
- User initiates login on
my-app.com: The user clicks a "Login with AuthServer" button. my-app.comredirects the user toauth-server.com:GET https://auth-server.com/auth? response_type=code& client_id=MY_APP_CLIENT_ID& redirect_uri=https://my-app.com/callback& scope=profile& state=aBcDeFgHiJkLmNoPqRsTuVwXyZresponse_type=code: Tells AuthServer we want an authorization code.client_id: Identifiesmy-app.com.redirect_uri: Where AuthServer should send the user back after they authorize. This is crucial.scope: What permissionsmy-app.comis requesting (e.g., read profile).state: A random string to prevent CSRF attacks.my-app.comgenerates this and expects it back.
- User logs in on
auth-server.com: The user enters their credentials on AuthServer. - User authorizes
my-app.com: AuthServer shows a screen like "Allow My App to access your profile?". The user clicks "Allow." auth-server.comredirects the user back tomy-app.comwith an authorization code:GET https://my-app.com/callback? code=AUTH_CODE_XYZ123& state=aBcDeFgHiJkLmNoPqRsTuVwXyZcode: The temporary authorization code.state: The same random stringmy-app.comsent earlier.
my-app.comexchanges the code for an access token:my-app.commakes a server-to-server request to AuthServer:POST https://auth-server.com/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code=AUTH_CODE_XYZ123& redirect_uri=https://my-app.com/callback& client_id=MY_APP_CLIENT_ID& client_secret=MY_APP_SECRETgrant_type=authorization_code: Specifies this is an exchange for an authorization code.client_secret:my-app.com’s secret, proving its identity.
auth-server.comreturns an access token:{ "access_token": "ACCESS_TOKEN_ABC987", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "REFRESH_TOKEN_DEF654" }my-app.comuses the access token to call an API:GET https://auth-server.com/api/profile Authorization: Bearer ACCESS_TOKEN_ABC987
This looks pretty secure, right? But attackers can exploit misconfigurations or vulnerabilities at several points.
Cross-Site Request Forgery (CSRF) in OAuth
The state parameter is your primary defense against CSRF during the authorization flow. Without it, an attacker could trick a user into clicking a malicious link that initiates an OAuth flow. If the user is already logged into auth-server.com, they might accidentally grant access to the attacker’s application instead of the legitimate one.
Diagnosis:
Check your OAuth client implementation. Does it generate a unique, unpredictable state parameter for each authorization request? Does it verify that the state parameter returned in the callback matches the one it sent?
Fix: On the client side (your application), before redirecting to the authorization server:
import secrets
session['oauth_state'] = secrets.token_urlsafe(16) # Generate a random 16-byte string
redirect_url = f"{AUTH_SERVER_URL}?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={session['oauth_state']}"
In your callback handler:
if request.args.get('state') != session.get('oauth_state'):
return "Invalid state parameter", 400
# ... proceed with code exchange
This works because the state parameter acts as a secret token. If the state returned by the authorization server doesn’t match what the client originally sent, it indicates that the request might have originated from a different source, preventing an attacker from hijacking the flow.
Redirect URI Hijacking
This is arguably the most critical vulnerability if not configured properly. The redirect_uri is where the authorization server sends the user back with the code. If an attacker can trick the authorization server into sending the code to a URI they control, they can intercept it.
Diagnosis:
Examine the redirect_uri configurations on both the client application and the authorization server. Are they registered exactly? Are they overly permissive (e.g., allowing any subdomain or path)?
Fix:
On the authorization server (e.g., AuthServer’s admin panel for your application):
Register the redirect_uri as precisely as possible. For my-app.com, this would be:
https://my-app.com/callback
Do NOT register https://my-app.com/ or https://my-app.com/*.
On the client application (my-app.com):
Ensure the redirect_uri parameter in the authorization request exactly matches one of the registered URIs on the authorization server. The redirect_uri in the authorization request must be one of the pre-registered URIs for your client_id.
REGISTERED_REDIRECT_URIS = [
"https://my-app.com/callback",
"https://staging.my-app.com/callback"
]
# ... when building redirect URL
if redirect_uri not in REGISTERED_REDIRECT_URIS:
return "Invalid redirect URI", 400
# ... proceed to build URL
This prevents an attacker from specifying a malicious redirect_uri in their authorization request, as the authorization server will only send the code back to a URI that was pre-approved for that client_id.
Client Secret Leakage
If your client_secret is exposed, an attacker can impersonate your application. This is especially dangerous if your application is a public client (like a mobile app or single-page JavaScript app) that cannot securely store a secret.
Diagnosis:
Review where your client_secret is stored. Is it in client-side code? Is it committed to a public Git repository? Is it accessible via an unauthenticated API endpoint?
Fix:
For server-side applications, store the client_secret securely in environment variables or a secrets management system. For public clients, use the PKCE (Proof Key for Code Exchange) extension.
PKCE Fix Example (Client Side): Before redirecting to AuthServer:
import secrets
code_verifier = secrets.token_urlsafe(32) # Generate a random 32-byte string
# Store code_verifier in session for later
session['oauth_code_verifier'] = code_verifier
import hashlib
code_challenge = hashlib.sha256(code_verifier.encode()).hexdigest()
redirect_url = f"{AUTH_SERVER_URL}?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&code_challenge={code_challenge}&code_challenge_method=S256&state=..."
In your callback handler, when exchanging the code:
code_verifier = session.get('oauth_code_verifier')
response = requests.post(AUTH_SERVER_TOKEN_URL, data={
'grant_type': 'authorization_code',
'code': request.args.get('code'),
'redirect_uri': REDIRECT_URI,
'client_id': CLIENT_ID,
'code_verifier': code_verifier # Send the original verifier
})
PKCE works by having the client generate a secret (code_verifier) and then a hash of that secret (code_challenge). This challenge is sent in the initial authorization request. When the client exchanges the authorization code for an access token, it sends the original code_verifier. The authorization server re-hashes the received code_verifier and compares it to the code_challenge it received earlier. If they match, it proves that the client requesting the token is the same one that initiated the flow, even if the client_secret wasn’t used or was compromised.
Implicit Grant Flow Vulnerabilities (Deprecated)
The implicit grant flow (where the access token is returned directly in the URL fragment, e.g., redirect_uri#access_token=...) is now considered insecure and should be avoided. It’s susceptible to token leakage through browser history, referer headers, and JavaScript execution.
Diagnosis:
Check your OAuth client configuration and the authorization server’s supported grant types. If you’re using response_type=token, you’re using the implicit flow.
Fix: Migrate to the Authorization Code grant flow, preferably with PKCE. This is the modern, recommended approach for most clients.
The next common issue after securing your redirect URIs and preventing CSRF is understanding how to properly manage the lifecycle of access tokens, especially when they expire.