OAuth 2.0 Series
OAuth 2.0 Grant Types: Authorization Code Grant with PKCE
PKCE (Proof Key for Code Exchange) is an extension to the Authorization Code Grant that enhances security for Public Clients like SPAs and mobile apps, where a client_secret cannot be kept confidential. It's the recommended flow for modern client-side applications.
What is PKCE (Proof Key for Code Exchange)?
PKCE (pronounced "pixy") prevents "authorization code interception attacks" by introducing a cryptographic proof that ensures only the original client can exchange an authorization_code for an access_token.
When to use it?
- Single-Page Applications (SPAs): Applications running entirely in the user's web browser (e.g., React, Angular, Vue apps).
- Mobile Applications: Native iOS or Android apps.
- Desktop Applications: Client-side applications installed on a user's computer.
- Any Public Client: Essentially, any application where a client_secret cannot be stored securely.
When NOT to use it?
While it can be used with confidential clients (web applications with a backend), it's often not strictly necessary as the client_secret already provides strong client authentication. However, using PKCE even with confidential clients provides an additional layer of defense-in-depth and is increasingly seen as a best practice by some security experts.
Key Roles Involved (Refresher)
The roles remain the same as the standard Authorization Code Grant:
- Resource Owner: The user.
- Client (Application): Your SPA, mobile, or desktop app.
- Authorization Server: The service that authenticates the user and issues tokens.
- Resource Server: The API hosting the protected user data.
The Flow of Authorization Code Grant with PKCE
The PKCE flow adds two new parameters to the standard Authorization Code Grant: code_verifier and code_challenge.
Let's illustrate with an example: Your mobile app (Client App) wants to access your contacts on Google Contacts (Resource Server).
Step-by-Step Explanation with Examples:
Step 1: Client Generates code_verifier and code_challenge
- The Client App generates a high-entropy cryptographically random string called the code_verifier (typically 43-128 characters long, using A-Z, a-z, 0-9, and specific punctuation).
- It then derives a code_challenge from the code_verifier by applying a transformation. The most common transformation is SHA256 hashing followed by Base64 URL-safe encoding.
- The Client App stores the code_verifier securely locally (e.g., in memory, or securely within the app's sandboxed storage) for later use.
Step 2: Client Requests Authorization (with code_challenge)
- The Client App redirects the user's browser (or uses an in-app browser/webview) to the Authorization Server's
/authorize
endpoint. - The URL includes the standard parameters (
response_type=code
,client_id
,redirect_uri
,scope
,state
) along with the two new PKCE parameters: - code_challenge: The transformed code_verifier.
- code_challenge_method: The method used to transform the code_verifier (e.g.,
S256
for SHA256).
Request Example (User's Browser):
GET https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&
client_id=YOUR_PUBLIC_CLIENT_ID&
redirect_uri=https://your-spa.com/callback&
scope=https://www.googleapis.com/auth/contacts.readonly&
state=YOUR_SECURE_RANDOM_STATE&
code_challenge=YOUR_CODE_CHALLENGE_DERIVED_FROM_VERIFIER&
code_challenge_method=S256
Parameters Explained:
- response_type=code: Indicates that the Client wants an authorization_code.
- client_id: A unique ID assigned to your Public Client App.
- redirect_uri: The URL where the Authorization Server will redirect the user's browser back to your Client App. This URL must be pre-registered.
- scope: The specific permissions being requested.
- state: Crucial for preventing CSRF attacks.
- code_challenge: The SHA256 hash (Base64 URL-encoded) of the code_verifier.
- code_challenge_method: Specifies the method used to create the code_challenge (e.g., S256 for SHA256).
Step 3: User Authenticates and Grants Consent
- The Authorization Server displays a login page and a consent screen to the user.
- The user logs in and grants consent to the Client App.
Step 4: Authorization Server Issues Authorization Code
- If consent is granted, the Authorization Server redirects the user's browser back to the Client's redirect_uri, appending the authorization_code and the original state value.
- Crucially, the Authorization Server also stores the
code_challenge
andcode_challenge_method
received in Step 2, associating them with this specificauthorization_code
.
Response Example (User's Browser Redirect):
HTTP/1.1 302 Found
Location: https://your-spa.com/callback?
code=4/P7q7W_F_yC0d...&
state=YOUR_SECURE_RANDOM_STATE
Step 5: Client Exchanges Authorization Code for Tokens (with code_verifier)
- Upon receiving the authorization_code (and after validating the state parameter!), the Client App makes a direct POST request to the Authorization Server's
/token
endpoint. - This request includes:
- grant_type=authorization_code
- code: The authorization_code received in Step 4.
- redirect_uri: Must exactly match the one used in Step 2.
- client_id: The Client App's public ID.
- code_verifier: The original, untransformed code_verifier generated in Step 1.
Request Example (Client App to Authorization Server):
POST /oauth2/v4/token HTTP/1.1
Host: accounts.google.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=4/P7q7W_F_yC0d...&
redirect_uri=https://your-spa.com/callback&
client_id=YOUR_PUBLIC_CLIENT_ID&
code_verifier=YOUR_ORIGINAL_CODE_VERIFIER
Step 6: Authorization Server Verifies and Returns Tokens
- The Authorization Server receives the code_verifier from the Client.
- It then applies the same transformation (
code_challenge_method
) to the receivedcode_verifier
to recreate thecode_challenge
. - The Authorization Server compares this newly generated
code_challenge
with thecode_challenge
it stored in Step 4 (the one it received during the initial authorization request). - If the code_challenge values match, the authorization_code is valid, and the client_id is correct, the Authorization Server issues the access_token and potentially a refresh_token. If they don't match, the request is denied.
Response Example (Authorization Server to Client App):
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "ya29.a0AWTD...",
"token_type": "Bearer",
"expires_in": 3599,
"refresh_token": "1//0ewt_P...",
"scope": "https://www.googleapis.com/auth/contacts.readonly"
}
Step 7: Client Uses Access Token to Access Resources
- The Client App uses the access_token to make authenticated requests to the Resource Server's API.
- The access_token is typically included in the Authorization header as a Bearer token.
Advantages of Authorization Code Grant with PKCE
- Enhanced Security for Public Clients: PKCE effectively prevents authorization code interception attacks, making the Authorization Code Grant secure for applications that cannot keep a client_secret confidential. This is its primary and most significant advantage.
- No client_secret required on client-side: Eliminates the security risk of embedding secrets in distributed client applications.
- Supports Refresh Tokens: Allows long-lived sessions and seamless token renewal without repeated user interaction.
- Industry Recommended: This is the de-facto standard and recommended flow for mobile and single-page applications.
Disadvantages of Authorization Code Grant with PKCE
- Increased Complexity: Adds an extra step (generating and verifying the code_verifier/code_challenge) compared to the basic Authorization Code Grant. However, this complexity is handled by OAuth client libraries.
- Relies on Secure Client Storage for code_verifier: While it protects against network interception, the code_verifier must still be kept safe from other malicious processes on the user's device (e.g., malware reading app memory).
Common Issues and Security Considerations (Specific to PKCE and Public Clients)
Addressing the Question: "client_id and redirect_uri exposed. How to trust the client app?"
You've hit on a critical point for public clients. While client_id and redirect_uri are inherently public information (and their exposure is normal), the question of how to trust the Client App when it can't use a client_secret is precisely what PKCE solves.
1. client_id and redirect_uri are Public, but not the Vulnerability:
- client_id: This is simply an identifier for your application. Knowing it doesn't give an attacker any power on its own.
- redirect_uri: This must be strictly pre-registered with the Authorization Server. The Authorization Server will only send the authorization_code back to a URI that has been explicitly registered for that client_id. This prevents attackers from simply providing their own URL to receive the code.
2. How PKCE Ensures Trust for Public Clients:
- The "Secret-per-Request": PKCE introduces a unique, one-time secret (code_verifier) for each authorization request. Since this code_verifier is never sent in the initial browser redirect (only its hash, the code_challenge), an attacker who intercepts the authorization_code will not have the original code_verifier.
Other Security Considerations for PKCE/Public Clients:
1. Browser/OS-level Malware:
Issue: While PKCE protects against network interception, if a user's device is compromised with malware that can read application memory or inspect browser processes, the code_verifier (while in memory) could theoretically be exposed.
Prevention: This is a very difficult problem to solve at the OAuth level. Best practices involve general device security, anti-malware, and ensuring the access_tokens issued are short-lived.
2. Redirect URI Hostname Verification (for custom URI schemes on mobile):
Issue: On mobile platforms, apps often use custom URI schemes (e.g., myapp://callback
) or universal links for redirect_uri. If not properly configured, another app could register the same custom URI scheme and intercept the code.
Prevention: Implement strict URI scheme validation on the Authorization Server. For iOS and Android, utilize Universal Links/App Links which allow deep linking to specific app content and prevent other apps from claiming the same URI. The Authorization Server must verify the redirect is truly coming from the correct registered app.
Conclusion
The Authorization Code Grant with PKCE is the gold standard for securing OAuth 2.0 flows for Public Clients like SPAs, mobile, and desktop applications. By adding a robust cryptographic proof (code_verifier/code_challenge), it effectively mitigates the risk of authorization code interception, making it safe to use even when a client_secret cannot be kept confidential. Its adoption is critical for building secure client-side applications that interact with OAuth 2.0 protected APIs.