OAuth Integration Best Practices
Comprehensive guide to implementing OAuth authentication securely and efficiently.
Overview
OAuth 2.0 is the industry-standard protocol for authorization. This guide covers best practices for integrating OAuth providers with your application using the OpenDev platform.
Architecture Patterns
Recommended OAuth Flow
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐
│ User │────▶│ App │────▶│ OpenDev │────▶│ Provider │
│ │◀────│ Client │◀────│ Backend │◀────│ (Google) │
└─────────┘ └─────────┘ └──────────┘ └──────────┘
1. Click Login 2. Redirect 3. OAuth Flow 4. Token
Mobile App Flow
For mobile applications, use the Authorization Code Flow with PKCE:
// 1. Generate code verifier and challenge
const codeVerifier = generateRandomString(64);
const codeChallenge = base64UrlEncode(sha256(codeVerifier));
// 2. Start OAuth flow
const authUrl = `${provider}/authorize?
client_id=${clientId}&
redirect_uri=${redirectUri}&
code_challenge=${codeChallenge}&
code_challenge_method=S256&
response_type=code`;
// 3. Exchange code for tokens (include code_verifier)
const tokens = await exchangeCode(code, codeVerifier);
Web App Flow
For web applications, use the Server-side Flow:
// Server-side: Exchange authorization code for tokens
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state to prevent CSRF
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
// Exchange code for tokens
const tokens = await oauth.exchangeCode(code);
// Store tokens securely
req.session.accessToken = tokens.access_token;
});
Provider-Specific Guidelines
Google OAuth
Configuration Requirements:
- Enable Google+ API and People API
- Configure OAuth consent screen
- Set authorized redirect URIs
Scopes:
const scopes = [
'openid',
'email',
'profile'
];
Token Refresh:
// Google tokens refresh automatically
// Use refresh_token to get new access_token
const newTokens = await oauth2Client.refreshAccessToken(refreshToken);
Apple Sign In
Key Considerations:
- Requires App ID with Sign In with Apple capability
- Service ID for web authentication
- Private key for token generation
Web vs Native:
| Feature | Web | Native iOS |
|---|---|---|
| Auth Method | Popup/Redirect | Native UI |
| Private Key | Required | Not Required |
| User Data | Limited on refresh | Full access |
Handling User Data:
// Apple only provides name on first auth
// Store it immediately
if (user.name) {
await saveUserProfile(user.id, {
firstName: user.name.firstName,
lastName: user.name.lastName
});
}
WeChat OAuth
Environment Handling:
// Different apps for different platforms
const wechatConfig = {
mobile: {
appId: 'wx_mobile_app_id',
appSecret: 'mobile_secret'
},
web: {
appId: 'wx_web_app_id',
appSecret: 'web_secret'
},
miniProgram: {
appId: 'wx_mini_program_id',
appSecret: 'mini_program_secret'
}
};
UnionID vs OpenID:
- OpenID: Unique per application
- UnionID: Same across all apps under one account
- Use UnionID for cross-app user identification
Security Best Practices
Token Storage
Client-Side (Web):
// DON'T: Store in localStorage (XSS vulnerable)
localStorage.setItem('token', accessToken);
// DO: Use httpOnly cookies
res.cookie('session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
Mobile:
// iOS: Use Keychain
let keychain = Keychain(service: "com.yourapp")
keychain["accessToken"] = accessToken
// Android: Use EncryptedSharedPreferences
val prefs = EncryptedSharedPreferences.create(...)
prefs.edit().putString("accessToken", token).apply()
State Parameter
Always use cryptographically secure state:
const crypto = require('crypto');
function generateState() {
return crypto.randomBytes(32).toString('hex');
}
// Store in session before redirect
req.session.oauthState = generateState();
// Verify on callback
if (req.query.state !== req.session.oauthState) {
throw new Error('State mismatch - possible CSRF attack');
}
Nonce for ID Tokens
For OpenID Connect providers:
const nonce = crypto.randomBytes(16).toString('hex');
req.session.nonce = nonce;
const authUrl = `${provider}/authorize?nonce=${nonce}&...`;
// Verify nonce in ID token
const decoded = jwt.verify(idToken, publicKey);
if (decoded.nonce !== req.session.nonce) {
throw new Error('Nonce mismatch');
}
Error Handling
Common OAuth Errors
app.get('/auth/callback', async (req, res) => {
const { error, error_description } = req.query;
if (error) {
switch (error) {
case 'access_denied':
// User cancelled authentication
return res.redirect('/login?error=cancelled');
case 'invalid_scope':
// Requested scope not allowed
return res.redirect('/login?error=scope');
case 'server_error':
// Provider server issue
return res.redirect('/login?error=server');
default:
logger.error('OAuth error', { error, error_description });
return res.redirect('/login?error=unknown');
}
}
// Continue with token exchange...
});
Retry Logic
async function exchangeCodeWithRetry(code, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await exchangeCode(code);
} catch (error) {
if (i === maxRetries - 1) throw error;
// Exponential backoff
const delay = Math.pow(2, i) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
}
Performance Optimization
Token Caching
const tokenCache = new Map();
async function getValidToken(userId) {
const cached = tokenCache.get(userId);
if (cached && cached.expiresAt > Date.now()) {
return cached.accessToken;
}
// Refresh token
const newTokens = await refreshToken(userId);
tokenCache.set(userId, {
accessToken: newTokens.access_token,
expiresAt: Date.now() + (newTokens.expires_in * 1000)
});
return newTokens.access_token;
}
Parallel Provider Initialization
// Initialize all OAuth clients in parallel
const [googleClient, facebookClient, appleClient] = await Promise.all([
initializeGoogle(),
initializeFacebook(),
initializeApple()
]);
Testing
Unit Testing OAuth
describe('OAuth Flow', () => {
it('should generate valid state', () => {
const state = generateState();
expect(state).toHaveLength(64);
expect(state).toMatch(/^[a-f0-9]+$/);
});
it('should verify callback state', async () => {
req.session.oauthState = 'test-state';
req.query.state = 'different-state';
await expect(handleCallback(req, res))
.rejects.toThrow('State mismatch');
});
});
Integration Testing
describe('OAuth Integration', () => {
it('should complete Google OAuth flow', async () => {
// Mock Google responses
nock('https://oauth2.googleapis.com')
.post('/token')
.reply(200, {
access_token: 'mock-token',
refresh_token: 'mock-refresh',
expires_in: 3600
});
const result = await completeOAuthFlow('google', mockCode);
expect(result.accessToken).toBe('mock-token');
});
});
Monitoring & Logging
Key Metrics
// Track OAuth metrics
const metrics = {
oauthAttempts: new Counter('oauth_attempts_total'),
oauthSuccess: new Counter('oauth_success_total'),
oauthFailure: new Counter('oauth_failure_total'),
oauthDuration: new Histogram('oauth_duration_seconds')
};
async function handleOAuth(provider) {
metrics.oauthAttempts.inc({ provider });
const timer = metrics.oauthDuration.startTimer({ provider });
try {
const result = await processOAuth(provider);
metrics.oauthSuccess.inc({ provider });
return result;
} catch (error) {
metrics.oauthFailure.inc({ provider, error: error.code });
throw error;
} finally {
timer();
}
}
Audit Logging
function logOAuthEvent(event) {
logger.info('OAuth event', {
type: event.type,
provider: event.provider,
userId: event.userId,
ip: event.ipAddress,
userAgent: event.userAgent,
timestamp: new Date().toISOString()
});
}
Last updated: January 2026