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

FlowApplication TypeUse Case
Authorization Code + PKCESPA, NativeInteractive user login
Authorization CodeServerServer-side web apps
Client CredentialsMachineService-to-service
Refresh TokenAllToken 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:

ScopeDescription
openidRequired for OIDC
profileUser profile (name, picture)
emailUser email address
offline_accessRequest 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"
}
ErrorDescription
invalid_requestMissing or invalid parameter
invalid_clientClient authentication failed
invalid_grantInvalid code, refresh token, or credentials
unauthorized_clientClient not authorized for this grant
unsupported_grant_typeGrant type not supported
invalid_scopeInvalid or unknown scope

Next Steps

  • Users - Manage user accounts
  • Sessions - Handle sessions and tokens
  • MFA - Add multi-factor authentication