Authorization Code Grant¶
The Authorization Code grant is the recommended flow for user-facing applications.
Flow Overview¶
┌──────────┐ ┌──────────┐
│ │ 1. Authorization Request │ │
│ Client │ ────────────────────────────▶│ AuthZ │
│ App │ │ Endpoint │
│ │◀─────────────────────────────│ │
└──────────┘ 2. Authorization Code └──────────┘
│ │
│ │
│ 3. Token Request │
│ (code + verifier) │
▼ │
┌──────────┐ ┌──────────┐
│ Token │◀─────────────────────────────│ User │
│ Endpoint │ 4. Access + Refresh Token │ Login │
└──────────┘ └──────────┘
PKCE (Proof Key for Code Exchange)¶
PKCE is required for public clients and recommended for all clients.
Generate Code Verifier¶
// Client-side (JavaScript)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
Generate Code Challenge¶
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));
}
Step 1: Authorization Request¶
Redirect user to authorization endpoint:
GET /oauth/authorize?
response_type=code&
client_id=my-app&
redirect_uri=https://app.example.com/callback&
scope=openid+profile+email&
state=xyz123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Parameters¶
| Parameter | Required | Description |
|---|---|---|
response_type |
Yes | Must be code |
client_id |
Yes | Your app's client ID |
redirect_uri |
Yes | Must match registered URI |
scope |
Yes | Space-separated scopes |
state |
Recommended | CSRF protection |
code_challenge |
Public clients | PKCE challenge |
code_challenge_method |
With challenge | Must be S256 |
Step 2: User Authentication¶
CoreForge redirects to your login page if the user isn't authenticated:
// In AuthorizeEndpoint
userID := getUserIDFromSession(r)
if userID == "" {
http.Redirect(w, r, "/login?redirect="+url.QueryEscape(r.URL.String()), http.StatusFound)
return
}
Step 3: Authorization Response¶
After user authorizes, they're redirected back with a code:
Error Response¶
https://app.example.com/callback?
error=access_denied&
error_description=The+user+denied+the+request&
state=xyz123
Step 4: Token Exchange¶
Exchange the code for tokens:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
client_id=my-app&
redirect_uri=https://app.example.com/callback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Confidential Clients¶
Add client authentication:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://app.example.com/callback
Token Response¶
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "8xLOxBtZp8",
"scope": "openid profile email"
}
Complete Example (JavaScript)¶
class OAuthClient {
constructor(config) {
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.authEndpoint = config.authEndpoint;
this.tokenEndpoint = config.tokenEndpoint;
}
async startAuth(scopes) {
const verifier = this.generateVerifier();
const challenge = await this.generateChallenge(verifier);
const state = this.generateState();
// Store for later
sessionStorage.setItem('pkce_verifier', verifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: scopes.join(' '),
state: state,
code_challenge: challenge,
code_challenge_method: 'S256'
});
window.location.href = `${this.authEndpoint}?${params}`;
}
async handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
// Verify state
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch');
}
const verifier = sessionStorage.getItem('pkce_verifier');
// Exchange code for tokens
const response = await fetch(this.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: this.clientId,
redirect_uri: this.redirectUri,
code_verifier: verifier
})
});
return response.json();
}
}
Server-Side Implementation¶
// Login handler - after successful authentication
func handleLogin(w http.ResponseWriter, r *http.Request) {
// Validate credentials
user, err := validateCredentials(r)
if err != nil {
// Show login error
return
}
// Create session
session := createSession(user.ID)
setSessionCookie(w, session)
// Redirect back to OAuth flow
if redirect := r.URL.Query().Get("redirect"); redirect != "" {
http.Redirect(w, r, redirect, http.StatusFound)
return
}
// Default redirect
http.Redirect(w, r, "/", http.StatusFound)
}