Skip to content

OAuth Flow

PKCE utilities, URL builders, and token exchange helpers.

Overview

The SDK provides comprehensive OAuth 2.0 utilities for implementing server-side authentication flows with PKCE.


For complete server-side OAuth with PKCE:

import { OAuthFlow } from '@authvital/sdk/server';

const oauth = new OAuthFlow({
  authVitalHost: process.env.AV_HOST!,
  clientId: process.env.AV_CLIENT_ID!,
  clientSecret: process.env.AV_CLIENT_SECRET!,
  redirectUri: 'https://myapp.com/api/auth/callback',
});

startFlow()

Start the OAuth authorization flow.

app.get('/api/auth/login', (req, res) => {
  const { authorizeUrl, state, codeVerifier } = oauth.startFlow({
    appState: req.query.returnTo, // Optional - gets passed through OAuth
  });

  // Store for callback verification
  req.session.oauthState = state;
  req.session.codeVerifier = codeVerifier;

  res.redirect(authorizeUrl);
});

handleCallback()

Complete the OAuth flow by exchanging the authorization code for tokens.

app.get('/api/auth/callback', async (req, res) => {
  const tokens = await oauth.handleCallback(
    req.query.code as string,
    req.query.state as string,
    req.session.oauthState,
    req.session.codeVerifier
  );

  // tokens includes: access_token, refresh_token, id_token, appState

  // Set cookies
  res.cookie('access_token', tokens.access_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
  });

  // Redirect to app state or dashboard
  res.redirect(tokens.appState || '/dashboard');
});

URL Builders

For landing pages, emails, or simple redirects (no PKCE ceremony):

getLoginUrl()

import { getLoginUrl } from '@authvital/sdk/server';

const loginUrl = getLoginUrl({
  authVitalHost: 'https://auth.myapp.com',
  clientId: 'my-app',
  redirectUri: 'https://app.myapp.com/dashboard',
  tenantHint: 'acme-corp', // Optional
});

getSignupUrl()

import { getSignupUrl } from '@authvital/sdk/server';

const signupUrl = getSignupUrl({
  authVitalHost: 'https://auth.myapp.com',
  clientId: 'my-app',
  redirectUri: 'https://app.myapp.com/onboarding',
  email: 'user@example.com', // Optional - pre-fill
});

getLogoutUrl()

import { getLogoutUrl } from '@authvital/sdk/server';

const logoutUrl = getLogoutUrl({
  authVitalHost: 'https://auth.myapp.com',
  postLogoutRedirectUri: 'https://myapp.com',
});

getInviteAcceptUrl()

import { getInviteAcceptUrl } from '@authvital/sdk/server';

const inviteUrl = getInviteAcceptUrl({
  authVitalHost: 'https://auth.myapp.com',
  clientId: 'my-app',
  inviteToken: 'abc123xyz',
});

getPasswordResetUrl()

import { getPasswordResetUrl } from '@authvital/sdk/server';

const resetUrl = getPasswordResetUrl({
  authVitalHost: 'https://auth.myapp.com',
  token: 'reset-token',
});

getAccountSettingsUrl()

import { getAccountSettingsUrl } from '@authvital/sdk/server';

const settingsUrl = getAccountSettingsUrl({
  authVitalHost: 'https://auth.myapp.com',
});

State Management

encodeState() / decodeState()

Encode CSRF token and app state into the OAuth state parameter:

import { encodeState, decodeState } from '@authvital/sdk/server';

// Encode
const state = encodeState(csrfNonce, '/dashboard?tab=settings');

// Later, decode
const payload = decodeState(state);
// { csrf: 'abc123', appState: '/dashboard?tab=settings' }

encodeStateWithVerifier() / decodeStateWithVerifier()

Include the PKCE code verifier in the state (for stateless auth):

import { encodeStateWithVerifier, decodeStateWithVerifier } from '@authvital/sdk/server';

// Encode (with encryption)
const state = encodeStateWithVerifier(csrf, codeVerifier, appState, encryptionKey);

// Decode
const { csrf, codeVerifier, appState } = decodeStateWithVerifier(state, encryptionKey);

Low-Level PKCE Utilities

For custom OAuth implementations:

generatePKCE()

import { generatePKCE } from '@authvital/sdk/server';

const { codeVerifier, codeChallenge } = await generatePKCE();

buildAuthorizeUrl()

import { buildAuthorizeUrl } from '@authvital/sdk/server';

const authorizeUrl = buildAuthorizeUrl({
  authVitalHost: 'https://auth.yourapp.com',
  clientId: 'your-client-id',
  redirectUri: 'https://yourapp.com/callback',
  codeChallenge,
  state: 'random-state',
});

exchangeCodeForTokens()

import { exchangeCodeForTokens } from '@authvital/sdk/server';

const tokens = await exchangeCodeForTokens({
  authVitalHost: 'https://auth.yourapp.com',
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  code: 'authorization-code',
  codeVerifier,
  redirectUri: 'https://yourapp.com/callback',
});

refreshAccessToken()

import { refreshAccessToken } from '@authvital/sdk/server';

const newTokens = await refreshAccessToken({
  authVitalHost: 'https://auth.yourapp.com',
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  refreshToken: 'current-refresh-token',
});

Complete Example: Express OAuth Routes

import { OAuthFlow, getLogoutUrl } from '@authvital/sdk/server';
import express from 'express';
import session from 'express-session';

const app = express();
app.use(session({ /* config */ }));

const oauth = new OAuthFlow({
  authVitalHost: process.env.AV_HOST!,
  clientId: process.env.AV_CLIENT_ID!,
  clientSecret: process.env.AV_CLIENT_SECRET!,
  redirectUri: `${process.env.APP_URL}/api/auth/callback`,
});

// Start login
app.get('/api/auth/login', (req, res) => {
  const { authorizeUrl, state, codeVerifier } = oauth.startFlow({
    appState: req.query.returnTo as string,
  });

  req.session.oauthState = state;
  req.session.codeVerifier = codeVerifier;

  res.redirect(authorizeUrl);
});

// OAuth callback
app.get('/api/auth/callback', async (req, res) => {
  try {
    const tokens = await oauth.handleCallback(
      req.query.code as string,
      req.query.state as string,
      req.session.oauthState!,
      req.session.codeVerifier!
    );

    // Clear session state
    delete req.session.oauthState;
    delete req.session.codeVerifier;

    // Set httpOnly cookies
    res.cookie('access_token', tokens.access_token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: tokens.expires_in * 1000,
    });

    if (tokens.refresh_token) {
      res.cookie('refresh_token', tokens.refresh_token, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax',
        maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
      });
    }

    res.redirect(tokens.appState || '/dashboard');
  } catch (error) {
    console.error('OAuth callback error:', error);
    res.redirect('/login?error=auth_failed');
  }
});

// Logout
app.get('/api/auth/logout', (req, res) => {
  res.clearCookie('access_token');
  res.clearCookie('refresh_token');

  const logoutUrl = getLogoutUrl({
    authVitalHost: process.env.AV_HOST!,
    postLogoutRedirectUri: process.env.APP_URL!,
  });

  res.redirect(logoutUrl);
});