OAuth Flows
Implement OAuth 2.0 and OpenID Connect authentication flows.
Overview
Transactional Auth implements OAuth 2.0 and OpenID Connect (OIDC) standards for secure authentication. Choose the flow that matches your application type.
Supported Flows
| Flow | Application Type | Use Case |
|---|---|---|
| Authorization Code + PKCE | SPA, Native | Interactive user login |
| Authorization Code | Server | Server-side web apps |
| Client Credentials | Machine | Service-to-service |
| Refresh Token | All | Token renewal |
Authorization Code Flow with PKCE
The recommended flow for all interactive applications.
Step 1: Generate PKCE Parameters
import crypto from 'crypto';
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier: string): string {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Store codeVerifier securely for later use
sessionStorage.setItem('code_verifier', codeVerifier);Step 2: Redirect to Authorization
const state = crypto.randomBytes(16).toString('hex');
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
client_id: 'your_client_id',
redirect_uri: 'https://yourapp.com/callback',
response_type: 'code',
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
window.location.href = `https://auth.usetransactional.com/auth?${params}`;Step 3: Handle the Callback
// At /callback
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// Verify state
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch - possible CSRF attack');
}
// Get stored code verifier
const codeVerifier = sessionStorage.getItem('code_verifier');Step 4: Exchange Code for Tokens
const response = await fetch('https://auth.usetransactional.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'your_client_id',
code: code,
redirect_uri: 'https://yourapp.com/callback',
code_verifier: codeVerifier,
}),
});
const tokens = await response.json();
// {
// access_token: "eyJ...",
// refresh_token: "eyJ...",
// id_token: "eyJ...",
// token_type: "Bearer",
// expires_in: 3600
// }Authorization Code Flow (Server)
For server-side applications that can securely store a client secret.
Step 1: Redirect to Authorization
const params = new URLSearchParams({
client_id: process.env.CLIENT_ID,
redirect_uri: 'https://yourapp.com/callback',
response_type: 'code',
scope: 'openid profile email',
state: generateState(),
});
res.redirect(`https://auth.usetransactional.com/auth?${params}`);Step 2: Exchange Code (Server-Side)
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (!verifyState(state)) {
return res.status(400).send('Invalid state');
}
const response = await fetch('https://auth.usetransactional.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
code: code,
redirect_uri: 'https://yourapp.com/callback',
}),
});
const tokens = await response.json();
// Store tokens in session
req.session.tokens = tokens;
res.redirect('/dashboard');
});Client Credentials Flow
For machine-to-machine authentication without user context.
const response = await fetch('https://auth.usetransactional.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'read:data write:data',
audience: 'https://api.yourapp.com',
}),
});
const { access_token, expires_in } = await response.json();Refresh Token Flow
Renew access tokens without user interaction.
const response = await fetch('https://auth.usetransactional.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: 'your_client_id',
refresh_token: currentRefreshToken,
}),
});
const tokens = await response.json();
// New access_token and potentially new refresh_token (rotation)Note: Refresh tokens are rotated by default. Always use the new refresh token.
Token Validation
Validate Access Token
const response = await fetch('https://auth.usetransactional.com/token/introspection', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
},
body: new URLSearchParams({
token: accessToken,
}),
});
const { active, sub, scope, exp } = await response.json();Validate ID Token (JWT)
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: 'https://auth.usetransactional.com/.well-known/jwks.json',
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
}
jwt.verify(idToken, getKey, {
algorithms: ['RS256'],
audience: 'your_client_id',
issuer: 'https://auth.usetransactional.com',
}, (err, decoded) => {
if (err) {
console.error('Token validation failed:', err);
} else {
console.log('User:', decoded.sub);
}
});Scopes
Request specific permissions with scopes:
| Scope | Description |
|---|---|
openid | Required for OIDC |
profile | User profile (name, picture) |
email | User email address |
offline_access | Request refresh token |
Custom scopes for your API:
scope: 'openid profile email read:data write:data'OIDC Discovery
Find all endpoints automatically:
curl https://auth.usetransactional.com/.well-known/openid-configuration{
"issuer": "https://auth.usetransactional.com",
"authorization_endpoint": "https://auth.usetransactional.com/auth",
"token_endpoint": "https://auth.usetransactional.com/token",
"userinfo_endpoint": "https://auth.usetransactional.com/userinfo",
"jwks_uri": "https://auth.usetransactional.com/.well-known/jwks.json",
"revocation_endpoint": "https://auth.usetransactional.com/token/revocation",
"introspection_endpoint": "https://auth.usetransactional.com/token/introspection"
}UserInfo Endpoint
Get user information with an access token:
const response = await fetch('https://auth.usetransactional.com/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const user = await response.json();
// { sub: "user_xxx", email: "user@example.com", name: "John Doe" }Token Revocation
Revoke tokens when users log out:
await fetch('https://auth.usetransactional.com/token/revocation', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: 'your_client_id',
token: refreshToken,
token_type_hint: 'refresh_token',
}),
});Error Handling
OAuth errors follow the standard format:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}| Error | Description |
|---|---|
invalid_request | Missing or invalid parameter |
invalid_client | Client authentication failed |
invalid_grant | Invalid code, refresh token, or credentials |
unauthorized_client | Client not authorized for this grant |
unsupported_grant_type | Grant type not supported |
invalid_scope | Invalid or unknown scope |
Next Steps
On This Page
- Overview
- Supported Flows
- Authorization Code Flow with PKCE
- Step 1: Generate PKCE Parameters
- Step 2: Redirect to Authorization
- Step 3: Handle the Callback
- Step 4: Exchange Code for Tokens
- Authorization Code Flow (Server)
- Step 1: Redirect to Authorization
- Step 2: Exchange Code (Server-Side)
- Client Credentials Flow
- Refresh Token Flow
- Token Validation
- Validate Access Token
- Validate ID Token (JWT)
- Scopes
- OIDC Discovery
- UserInfo Endpoint
- Token Revocation
- Error Handling
- Next Steps