Back to blog
Security
Intermediate

Implementing OAuth 2.0 with PKCE in Mobile Apps: A Complete Guide

OAuth 2.0 with PKCE is the standard for mobile app authentication. This comprehensive guide covers the complete implementation flow with real code examples for both iOS and Android platforms.

June 17, 202525 min read

Introduction

Implementing secure authentication in mobile applications is one of the most critical aspects of app development. OAuth 2.0 with Proof Key for Code Exchange (PKCE) has emerged as the de facto standard for securing mobile app authentication. Unlike traditional OAuth 2.0 flows that rely on client secrets, PKCE provides an additional layer of security that's essential for public clients like mobile apps.

In this comprehensive guide, we'll dive deep into implementing OAuth 2.0 with PKCE across both iOS and Android platforms. You'll learn the complete authentication flow, security considerations, and practical implementation patterns that you can immediately apply to your projects.

Table of Contents

Core Concepts

What is OAuth 2.0?

OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service, like Google, Facebook, or GitHub. It works by delegating user authentication to the service that hosts the user account, and authorizing third-party applications to access the user account without sharing the user's password.

Understanding PKCE

Proof Key for Code Exchange (PKCE, pronounced "pixie") is an extension to the OAuth 2.0 authorization code flow that adds an extra layer of security. Originally designed for mobile and JavaScript applications where the client secret cannot be securely stored, PKCE prevents authorization code interception attacks by requiring a dynamically generated secret that proves the client initiating the request is the same client that receives the token.

The PKCE Flow

The PKCE flow involves three key components:

  1. Code Verifier: A cryptographically random string generated by the client
  2. Code Challenge: The base64 URL-encoded SHA-256 hash of the code verifier
  3. Code Challenge Method: The method used to derive the code challenge from the code verifier (plain or S256)

Why PKCE is Essential for Mobile Apps

Mobile apps are considered public clients because their code can be easily decompiled or reverse-engineered. This means any client secret embedded in the app could be extracted by malicious actors. PKCE solves this problem by eliminating the need for a static client secret and instead using a dynamic, one-time secret for each authorization request.

Architecture Overview

The OAuth 2.0 PKCE flow involves several components working together:

  • Mobile App (Client): Initiates the authentication request and handles the callback
  • Authorization Server: Authenticates the user and issues authorization codes
  • Resource Server: Provides access to protected resources using access tokens
  • Redirect URI: The endpoint where the authorization server sends the user after authentication

High-Level Flow Diagram

Mobile App → Authorization Server: /authorize?client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256Authorization Server → User: Login promptUser → Authorization Server: CredentialsAuthorization Server → Mobile App: /callback?code=...Mobile App → Authorization Server: /token?code=...&code_verifier=...Authorization Server → Mobile App: access_token, refresh_token

Step-by-Step Guide

Step 1: Generate Code Verifier and Challenge

Start by generating a secure code verifier and its corresponding code challenge:

function generateCodeVerifier() {  const array = new Uint32Array(56);  crypto.getRandomValues(array);  return base64URLEncode(String.fromCharCode(...array));}function base64URLEncode(str) {  return btoa(str)    .replace(/\+/g, '-')    .replace(/\//g, '_')    .replace(/=+$/, '');}function generateCodeChallenge(verifier) {  return base64URLEncode(sha256(verifier));}

Step 2: Construct Authorization Request

Build the authorization URL with all required parameters:

const authUrl = `https://auth.example.com/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scopes)}&code_challenge=${codeChallenge}&code_challenge_method=S256`;

Step 3: Launch Authentication in Web View

Present the authorization URL in a secure web view within your mobile app:

// iOS - Using SFSafariViewControllerlet safari = SFSafariViewController(url: URL(string: authUrl)!)safari.delegate = selfpresent(safari, animated: true)

Step 4: Handle Authorization Response

Capture the authorization code from the redirect:

// iOS - In your view controllerfunc safariViewController(_ controller: SFSafariViewController, didCompleteInLoad url: URL) {  if url.pathComponents.contains("callback") {    let components = URLComponents(url: url, resolvingAgainstBaseURL: false)    let code = components?.queryItems?.first(where: { $0.name == "code" })?.value    // Exchange code for tokens  }}

Step 5: Exchange Code for Tokens

Send the authorization code and code verifier to the token endpoint:

const tokenResponse = await fetch('https://auth.example.com/token', {  method: 'POST',  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },  body: new URLSearchParams({    grant_type: 'authorization_code',    code: authCode,    redirect_uri: redirectUri,    client_id: clientId,    code_verifier: codeVerifier  })});const tokens = await tokenResponse.json();

Step 6: Secure Token Storage

Store the access and refresh tokens securely using platform-specific secure storage:

// iOS - Using Keychainimport Securityclass KeychainService {  static let shared = KeychainService()    func saveToken(_ token: String, for key: String) {    let data = token.data(using: .utf8)!    let query = [      kSecClass: kSecClassGenericPassword,      kSecAttrAccount: key,      kSecValueData: data    ] as CFDictionary        SecItemDelete(query as CFDictionary)    SecItemAdd(query as CFDictionary, nil)  }}

Real-World Examples

Example 1: Google Sign-In with PKCE

Implementing Google authentication using PKCE follows the standard flow with Google's OAuth 2.0 endpoints. The key differences include using Google's authorization URL and handling their specific response format.

Example 2: Custom Backend Authentication

For applications with their own authentication server, you'll need to implement both the client-side PKCE flow and the server-side token validation logic. This gives you full control over the authentication experience.

Example 3: Social Login Integration

When integrating multiple social providers (Facebook, Apple, GitHub), PKCE can be implemented consistently across all providers, simplifying your authentication codebase.

Production Code Examples

iOS Implementation with Auth0

import Auth0class AuthManager {  private let auth0 = Auth0.authentication()    func loginWithPKCE(completion: @escaping (Result) -> Void) {    let pkce = PKCE()        auth0.login(with: .web, connection: "google",                state: pkce.state,                code_challenge: pkce.codeChallenge,                code_challenge_method: "S256")      { result in        switch result {        case .success(let credentials):          // Store tokens securely          self.storeTokens(credentials)          completion(.success(credentials))        case .failure(let error):          completion(.failure(error))        }      }  }    private func storeTokens(_ credentials: Credentials) {    // Use Keychain for secure storage    KeychainService.shared.saveToken(credentials.accessToken, for: "access_token")    KeychainService.shared.saveToken(credentials.refreshToken ?? "", for: "refresh_token")  }}

Android Implementation with AppAuth

class AuthManager(private val context: Context) {    private val authClient = Auth0AuthenticationClient(context)        fun loginWithPKCE(callback: (Result) -> Unit) {        val pkce = PKCEGenerator()                authClient.loginWithWebAuth(            connection = "google",            state = pkce.state,            codeChallenge = pkce.codeChallenge,            codeChallengeMethod = "S256"        ) { result ->            when (result) {                is AuthResult.Success -> {                    // Store tokens securely                    storeTokens(result.credentials)                    callback(Result.Success(result.credentials))                }                is AuthResult.Failure -> {                    callback(Result.Failure(result.error))                }            }        }    }        private fun storeTokens(credentials: Credentials) {        // Use Android Keystore for secure storage        val encryptedToken = encrypt(credentials.accessToken)        SharedPreferences.edit()            .putString("access_token", encryptedToken)            .apply()    }}

Backend Token Validation

// Node.js Express middleware for token validationconst jwt = require('jsonwebtoken');function authenticateToken(req, res, next) {  const authHeader = req.headers['authorization'];  const token = authHeader && authHeader.split(' ')[1];    if (!token) return res.sendStatus(401);    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {    if (err) return res.sendStatus(403);    req.user = user;    next();  });}// Token refresh endpointapp.post('/refresh', (req, res) => {  const { refreshToken } = req.body;    // Verify refresh token and issue new access token  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, decoded) => {    if (err) return res.sendStatus(403);        const newAccessToken = jwt.sign(      { userId: decoded.userId },      process.env.JWT_SECRET,      { expiresIn: '15m' }    );        res.json({ accessToken: newAccessToken });  });});

Comparison Table

Authentication Method Security Level Complexity Mobile Support Recommended Use Case
OAuth 2.0 with Client Secret Medium Low Poor Server-to-server only
OAuth 2.0 with PKCE High Medium Excellent Mobile and SPAs
OAuth 2.0 with PKCE + Refresh Tokens Very High High Excellent Long-lived mobile sessions
OpenID Connect with PKCE Very High High Excellent User identity and authentication

Best Practices

1. Use S256 Code Challenge Method

Always use the S256 method for code challenge derivation. The plain method is not secure and should never be used in production environments.

2. Implement Secure Token Storage

Never store tokens in plain preferences or local storage. Use platform-specific secure storage solutions like iOS Keychain or Android Keystore.

3. Set Appropriate Scopes

Request only the minimum necessary scopes. Excessive permissions can lead to security vulnerabilities and user distrust.

4. Handle Token Expiry Gracefully

Implement automatic token refresh logic before tokens expire. Provide clear user feedback when re-authentication is required.

5. Validate Redirect URIs

Always validate redirect URIs on both client and server sides to prevent authorization code interception attacks.

6. Implement Proper Error Handling

Handle authentication failures gracefully with clear error messages and recovery options.

Common Mistakes

1. Using Plain Code Challenge Method

One of the most common mistakes is using the plain code challenge method. This eliminates the security benefits of PKCE and should be avoided entirely.

2. Storing Client Secrets Insecurely

Even with PKCE, some developers still embed client secrets in their mobile apps. This is unnecessary and dangerous, as the secret can be easily extracted.

3. Not Validating State Parameters

Failure to validate the state parameter in the authorization response makes your application vulnerable to CSRF attacks.

4. Improper Token Storage

Storing access tokens in insecure locations like SharedPreferences or UserDefaults exposes them to potential theft by malicious apps or through device compromise.

5. Ignoring Token Expiry

Failing to handle token expiration leads to poor user experiences when the app suddenly stops working without warning.

Performance Tips

1. Cache Authorization Responses

Cache successful authentication responses to reduce redundant network requests and improve user experience.

2. Implement Token Refresh Logic

Proactively refresh tokens before they expire to avoid authentication interruptions during critical user operations.

3. Use Background Refresh

For iOS, implement background app refresh to update tokens when the app is not in the foreground.

4. Optimize Web View Performance

Configure web views to cache static resources and minimize JavaScript execution for faster authentication flows.

5. Implement Request Batching

Batch multiple token validation requests to reduce network overhead and improve app responsiveness.

Security Considerations

1. Code Verifier Entropy

Ensure code verifiers have sufficient entropy (at least 256 bits) to prevent brute-force attacks. Use cryptographically secure random number generators.

2. HTTPS Enforcement

Always use HTTPS for all communication with the authorization server. Implement certificate pinning in production apps to prevent man-in-the-middle attacks.

3. Token Lifetime Management

Set appropriate access token lifetimes (typically 15-60 minutes) and implement secure refresh token rotation.

4. Biometric Authentication

Consider adding an additional layer of security by requiring biometric authentication to access stored tokens.

5. Secure Coding Practices

Follow secure coding guidelines to prevent common vulnerabilities like buffer overflows, injection attacks, and insecure data storage.

Deployment Notes

1. Environment Configuration

Use different client IDs and authorization server URLs for development, staging, and production environments. Never hardcode production credentials in development builds.

2. Certificate Management

Implement proper certificate management for both development and production environments. Use different certificates for each environment to prevent cross-environment authentication.

3. Monitoring and Logging

Implement comprehensive monitoring and logging for authentication events. Track failed login attempts and unusual authentication patterns.

4. Rollback Strategy

Have a rollback strategy for authentication changes. Maintain backward compatibility with existing authentication methods during transitions.

5. Compliance Requirements

Ensure your implementation meets relevant compliance requirements such as GDPR, CCPA, or industry-specific regulations.

Debugging Tips

1. Enable Detailed Logging

During development, enable detailed logging for authentication flows. Use different log levels to control verbosity in production.

2. Use Network Proxies

Use tools like Charles Proxy or Fiddler to inspect authentication requests and responses during debugging.

3. Test with Multiple Environments

Test your implementation across different environments and devices to catch edge cases and platform-specific issues.

4. Monitor Token Lifecycles

Implement token lifecycle monitoring to identify issues with token expiration, refresh failures, or storage problems.

5. Use Debugging Tools

Leverage platform-specific debugging tools like Xcode's network inspector or Android Studio's HTTP client to diagnose authentication issues.

FAQ

What is the difference between OAuth 2.0 and PKCE?

OAuth 2.0 is the authorization framework, while PKCE is an extension to OAuth 2.0 that adds security for public clients like mobile apps. PKCE prevents authorization code interception attacks by requiring a dynamic secret for each authentication request.

Why can't I just use client secrets in mobile apps?

Mobile apps are considered public clients because their code can be easily decompiled or reverse-engineered. Any client secret embedded in the app could be extracted by malicious actors, making it insecure for production use.

Is PKCE only for mobile apps?

No, PKCE is also recommended for single-page applications (SPAs) and any public client where the client secret cannot be securely stored. It provides an additional layer of security for all OAuth 2.0 implementations.

What code challenge method should I use?

You should always use the S256 method for code challenge derivation. The plain method is not secure and should never be used in production environments.

How do I securely store tokens in my mobile app?

Use platform-specific secure storage solutions: iOS Keychain for iOS apps and Android Keystore for Android apps. Never store tokens in plain preferences, localStorage, or other insecure locations.

What should I do when my access token expires?

Implement automatic token refresh using the refresh token before the access token expires. If the refresh fails, prompt the user to re-authenticate.

Can I use PKCE with OpenID Connect?

Yes, PKCE works perfectly with OpenID Connect. In fact, many OpenID Connect providers recommend or require PKCE for mobile and SPA applications.

How do I handle authentication errors gracefully?

Implement proper error handling with clear user-facing messages. Provide options to retry authentication or contact support if errors persist. Avoid exposing sensitive error details to users.

What are the common security vulnerabilities in mobile authentication?

Common vulnerabilities include insecure token storage, man-in-the-middle attacks, authorization code interception, and insufficient state validation. Implementing PKCE and using HTTPS helps mitigate these risks.

How do I test my PKCE implementation?

Test with multiple authorization servers, different environments, and various network conditions. Use debugging tools to inspect authentication flows and verify that security measures are working correctly.

Conclusion

Implementing OAuth 2.0 with PKCE in mobile apps is not just a best practice—it's essential for secure authentication in today's mobile landscape. By following the patterns and code examples in this guide, you can build robust authentication flows that protect user credentials while providing a seamless login experience.

Remember that security is an ongoing process. Regularly review and update your authentication implementation to address emerging threats and comply with evolving standards and regulations.

Start implementing PKCE in your mobile apps today, and provide your users with the secure authentication experience they deserve. Your users' trust—and your app's security—depends on it.