The most secure flow for any app that has a browser. PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks.
Your App Your Browser OAuth Server
| | |
|── generateAuthUrl ─► |
| state (CSRF) | |
| code_challenge | |
| │── GET /authorize ─►|
| | | Show login form
| |◄── 302 /callback ─|
|◄── exchangeCode ──| |
| validates state | |
| sends verifier │── POST /token ───►|
| | | Verify PKCE ✓
|◄── access_token ──────────────────────|
=== “Node.js”
```js
const SCGAuth = require('scg-auth');
const client = new SCGAuth({
clientId: 'my-app',
clientSecret: 'optional-for-public-clients',
authorizationUrl: 'https://your-server.com/authorize',
tokenUrl: 'https://your-server.com/token',
redirectUri: 'http://localhost:3000/callback',
scopes: ['openid', 'email', 'profile'],
});
// ── Step 1: Build the auth URL ──────────────────────────────────────────
const { url, state, codeVerifier } = client.generateAuthUrl({
pkce: true, // enable PKCE S256 (recommended)
additionalParams: { // any extra params your server needs
prompt: 'login',
acr_values: 'urn:mace:incommon:iap:silver',
},
});
// Redirect the user:
// res.redirect(url);
// ── Step 2: Handle callback at /callback ────────────────────────────────
// req.query = { code: '...', state: '...' }
const tokens = await client.exchangeCode(req.query.code, {
state: req.query.state, // scg-auth verifies this matches what was sent
// codeVerifier is stored internally — no need to pass it
});
// tokens =
// {
// access_token: 'eyJ...',
// token_type: 'Bearer',
// expires_in: 3600,
// refresh_token: 'def...',
// scope: 'openid email profile',
// }
// ── Step 3: Check token status ──────────────────────────────────────────
console.log(client.isTokenExpired()); // false — not expired
console.log(client.isTokenExpired(3600)); // true — expires within 1 hour
console.log(client.getStoredTokens()); // full token object
```
=== “Python”
```python
from scg_auth import SCGAuth, OAuthError
client = SCGAuth(
client_id='my-app',
client_secret='optional',
authorization_url='https://your-server.com/authorize',
token_url='https://your-server.com/token',
redirect_uri='http://localhost:3000/callback',
scopes=['openid', 'email', 'profile'],
)
# Step 1: Build auth URL
result = client.generate_auth_url(pkce=True)
# result = { 'url': '...', 'state': '...', 'code_verifier': '...' }
# Step 2: Exchange code after redirect
try:
tokens = client.exchange_code(code, state=result['state'])
except OAuthError as e:
print(e.oauth_error) # e.g. 'invalid_grant'
print(e.status_code)
# Step 3: Check status
print(client.is_token_expired()) # False
print(client.is_token_expired(3600)) # True (expires within 1hr)
print(client.get_stored_tokens()) # full token dict
```
!!! tip “PKCE replaces client_secret for public clients”
For browser apps and mobile apps, PKCE provides the same protection as a client_secret without embedding a secret in client-side code.
!!! warning “Always validate state”
Pass state to exchangeCode / exchange_code. scg-auth will throw if it doesn’t match, preventing CSRF attacks.