mirror of
https://github.com/mintlify/docs.git
synced 2026-09-14 13:35:46 +08:00
65fd3621dc
- Created 111 MDX documentation files - Added 89 API endpoint pages across 14 categories - Included 11 concept guides and 7 tutorials - Configured Mintlify with Spotify branding (#1DB954) - All pages validated successfully
305 lines
8.2 KiB
Plaintext
305 lines
8.2 KiB
Plaintext
---
|
|
title: "Authorization Code with PKCE"
|
|
description: "Implement PKCE flow for mobile and single-page applications"
|
|
---
|
|
|
|
# Authorization Code with PKCE tutorial
|
|
|
|
PKCE (Proof Key for Code Exchange) is an extension to the Authorization Code Flow that provides additional security for applications that can't securely store a client secret.
|
|
|
|
## When to use PKCE
|
|
|
|
Use this flow for:
|
|
- Single-page applications (SPAs)
|
|
- Mobile applications
|
|
- Desktop applications
|
|
- Any public client that can't secure a client secret
|
|
|
|
## How PKCE works
|
|
|
|
PKCE adds two parameters to the authorization flow:
|
|
|
|
- **Code verifier**: Random string generated by your app
|
|
- **Code challenge**: SHA256 hash of the code verifier
|
|
|
|
This prevents authorization code interception attacks.
|
|
|
|
## Implementation
|
|
|
|
### Step 1: Generate code verifier and challenge
|
|
|
|
```javascript
|
|
// Generate random code verifier
|
|
function generateCodeVerifier() {
|
|
const array = new Uint8Array(32);
|
|
crypto.getRandomValues(array);
|
|
return base64URLEncode(array);
|
|
}
|
|
|
|
// Create code challenge from verifier
|
|
async function generateCodeChallenge(verifier) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(verifier);
|
|
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
return base64URLEncode(new Uint8Array(hash));
|
|
}
|
|
|
|
// Base64 URL encoding
|
|
function base64URLEncode(buffer) {
|
|
return btoa(String.fromCharCode(...buffer))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=+$/, '');
|
|
}
|
|
```
|
|
|
|
### Step 2: Build authorization URL
|
|
|
|
```javascript
|
|
const clientId = 'your_client_id';
|
|
const redirectUri = 'https://yourapp.com/callback';
|
|
const scope = 'user-read-private user-read-email';
|
|
|
|
// Generate PKCE parameters
|
|
const codeVerifier = generateCodeVerifier();
|
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
|
|
// Store verifier for later use
|
|
localStorage.setItem('code_verifier', codeVerifier);
|
|
|
|
// Build authorization URL
|
|
const authUrl = new URL('https://accounts.spotify.com/authorize');
|
|
authUrl.searchParams.append('client_id', clientId);
|
|
authUrl.searchParams.append('response_type', 'code');
|
|
authUrl.searchParams.append('redirect_uri', redirectUri);
|
|
authUrl.searchParams.append('scope', scope);
|
|
authUrl.searchParams.append('code_challenge_method', 'S256');
|
|
authUrl.searchParams.append('code_challenge', codeChallenge);
|
|
|
|
// Redirect user
|
|
window.location.href = authUrl.toString();
|
|
```
|
|
|
|
### Step 3: Handle callback
|
|
|
|
```javascript
|
|
// Extract code from URL
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const code = urlParams.get('code');
|
|
|
|
if (code) {
|
|
// Retrieve stored code verifier
|
|
const codeVerifier = localStorage.getItem('code_verifier');
|
|
|
|
// Exchange code for token
|
|
await exchangeCodeForToken(code, codeVerifier);
|
|
}
|
|
```
|
|
|
|
### Step 4: Exchange code for token
|
|
|
|
```javascript
|
|
async function exchangeCodeForToken(code, codeVerifier) {
|
|
const clientId = 'your_client_id';
|
|
const redirectUri = 'https://yourapp.com/callback';
|
|
|
|
const response = await fetch('https://accounts.spotify.com/api/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: new URLSearchParams({
|
|
client_id: clientId,
|
|
grant_type: 'authorization_code',
|
|
code: code,
|
|
redirect_uri: redirectUri,
|
|
code_verifier: codeVerifier
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Store tokens
|
|
localStorage.setItem('access_token', data.access_token);
|
|
localStorage.setItem('refresh_token', data.refresh_token);
|
|
|
|
// Clean up code verifier
|
|
localStorage.removeItem('code_verifier');
|
|
|
|
return data;
|
|
}
|
|
```
|
|
|
|
## Complete React example
|
|
|
|
```javascript
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
// Configuration
|
|
const CLIENT_ID = 'your_client_id';
|
|
const REDIRECT_URI = 'http://localhost:3000/callback';
|
|
const SCOPES = 'user-read-private user-read-email';
|
|
|
|
// PKCE utilities
|
|
function generateCodeVerifier() {
|
|
const array = new Uint8Array(32);
|
|
window.crypto.getRandomValues(array);
|
|
return base64URLEncode(array);
|
|
}
|
|
|
|
async function generateCodeChallenge(verifier) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(verifier);
|
|
const hash = await window.crypto.subtle.digest('SHA-256', data);
|
|
return base64URLEncode(new Uint8Array(hash));
|
|
}
|
|
|
|
function base64URLEncode(buffer) {
|
|
return btoa(String.fromCharCode(...buffer))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=+$/, '');
|
|
}
|
|
|
|
function App() {
|
|
const [token, setToken] = useState(null);
|
|
const [profile, setProfile] = useState(null);
|
|
|
|
useEffect(() => {
|
|
// Check for callback code
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const code = urlParams.get('code');
|
|
|
|
if (code) {
|
|
handleCallback(code);
|
|
} else {
|
|
// Check for existing token
|
|
const storedToken = localStorage.getItem('access_token');
|
|
if (storedToken) {
|
|
setToken(storedToken);
|
|
fetchProfile(storedToken);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
async function handleCallback(code) {
|
|
const codeVerifier = localStorage.getItem('code_verifier');
|
|
|
|
try {
|
|
const response = await fetch('https://accounts.spotify.com/api/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: new URLSearchParams({
|
|
client_id: CLIENT_ID,
|
|
grant_type: 'authorization_code',
|
|
code: code,
|
|
redirect_uri: REDIRECT_URI,
|
|
code_verifier: codeVerifier
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
localStorage.setItem('access_token', data.access_token);
|
|
localStorage.setItem('refresh_token', data.refresh_token);
|
|
localStorage.removeItem('code_verifier');
|
|
|
|
setToken(data.access_token);
|
|
|
|
// Clean URL
|
|
window.history.replaceState({}, document.title, '/');
|
|
|
|
// Fetch profile
|
|
fetchProfile(data.access_token);
|
|
} catch (error) {
|
|
console.error('Error exchanging code:', error);
|
|
}
|
|
}
|
|
|
|
async function login() {
|
|
const codeVerifier = generateCodeVerifier();
|
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
|
|
localStorage.setItem('code_verifier', codeVerifier);
|
|
|
|
const authUrl = new URL('https://accounts.spotify.com/authorize');
|
|
authUrl.searchParams.append('client_id', CLIENT_ID);
|
|
authUrl.searchParams.append('response_type', 'code');
|
|
authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
|
|
authUrl.searchParams.append('scope', SCOPES);
|
|
authUrl.searchParams.append('code_challenge_method', 'S256');
|
|
authUrl.searchParams.append('code_challenge', codeChallenge);
|
|
|
|
window.location.href = authUrl.toString();
|
|
}
|
|
|
|
async function fetchProfile(accessToken) {
|
|
const response = await fetch('https://api.spotify.com/v1/me', {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
setProfile(data);
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
setToken(null);
|
|
setProfile(null);
|
|
}
|
|
|
|
if (!token) {
|
|
return (
|
|
<div>
|
|
<h1>Spotify PKCE Example</h1>
|
|
<button onClick={login}>Login with Spotify</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1>Welcome, {profile?.display_name}</h1>
|
|
<p>Email: {profile?.email}</p>
|
|
<button onClick={logout}>Logout</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|
|
```
|
|
|
|
## Security considerations
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="Generate strong code verifier">
|
|
Use cryptographically secure random generation for the code verifier.
|
|
</Accordion>
|
|
<Accordion title="Store verifier securely">
|
|
Store the code verifier in a secure location. For web apps, sessionStorage is better than localStorage for sensitive data.
|
|
</Accordion>
|
|
<Accordion title="Clean up after exchange">
|
|
Always remove the code verifier after exchanging for a token.
|
|
</Accordion>
|
|
<Accordion title="Use HTTPS">
|
|
Always use HTTPS for your redirect URI.
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
## Next steps
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="Refreshing tokens" href="/tutorials/refreshing-tokens">
|
|
Handle token expiration
|
|
</Card>
|
|
<Card title="Authentication flows" href="/getting-started/authentication-flows">
|
|
Compare different flows
|
|
</Card>
|
|
</CardGroup>
|
|
|