Backend Setup¶
Set up OAuth routes and protected middleware for Express or Next.js.
Express Setup¶
OAuth Routes¶
Create OAuth routes for handling the authentication flow:
// routes/auth.ts
import { Router } from 'express';
import { OAuthFlow } from '@authvital/sdk/server';
import { authvital } from '../lib/authvital';
const router = Router();
// Initialize OAuth flow helper
const oauth = new OAuthFlow({
authVitalHost: process.env.AV_HOST!,
clientId: process.env.AV_CLIENT_ID!,
clientSecret: process.env.AV_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
scope: 'openid profile email',
});
/**
* GET /api/auth/login
* Initiates OAuth 2.0 PKCE flow
*/
router.get('/login', (req, res) => {
const { authorizeUrl, state, codeVerifier } = oauth.startFlow();
// Store state and verifier in session (for CSRF + PKCE validation)
req.session.oauthState = state;
req.session.codeVerifier = codeVerifier;
// Optional: Store where to redirect after login
if (req.query.returnTo) {
req.session.returnTo = req.query.returnTo as string;
}
res.redirect(authorizeUrl);
});
/**
* GET /api/auth/callback
* Handles OAuth callback from AuthVital
*/
router.get('/callback', async (req, res) => {
try {
const { code, state, error } = req.query;
if (error) {
console.error('OAuth error:', error, req.query.error_description);
return res.redirect('/login?error=' + error);
}
const tokens = await oauth.handleCallback(
code as string,
state as string,
req.session.oauthState,
req.session.codeVerifier
);
// Clear OAuth session data
delete req.session.oauthState;
delete req.session.codeVerifier;
// Set access token as httpOnly cookie (XSS-safe!)
res.cookie('access_token', tokens.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 1000, // 1 hour
path: '/',
});
if (tokens.refresh_token) {
res.cookie('refresh_token', tokens.refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/',
});
}
const returnTo = req.session.returnTo || '/dashboard';
delete req.session.returnTo;
res.redirect(returnTo);
} catch (err) {
console.error('OAuth callback error:', err);
res.redirect('/login?error=callback_failed');
}
});
/**
* GET /api/auth/me
* Returns current user data
*/
router.get('/me', async (req, res) => {
const { authenticated, user, error } = await authvital.getCurrentUser(req);
if (!authenticated) {
return res.status(401).json({
authenticated: false,
error: error || 'Not authenticated'
});
}
res.json({
authenticated: true,
user: {
id: user.sub,
email: user.email,
name: user.name || `${user.given_name || ''} ${user.family_name || ''}`.trim(),
picture: user.picture,
tenantId: user.tenant_id,
tenantRoles: user.tenant_roles || [],
appPermissions: user.app_permissions || [],
license: user.license,
},
});
});
/**
* POST /api/auth/logout
*/
router.post('/logout', (req, res) => {
res.clearCookie('access_token', { path: '/' });
res.clearCookie('refresh_token', { path: '/' });
req.session.destroy((err) => {
if (err) console.error('Session destroy error:', err);
res.json({ success: true });
});
});
/**
* POST /api/auth/refresh
*/
router.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
const tokens = await oauth.refreshTokens(refreshToken);
res.cookie('access_token', tokens.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 1000,
path: '/',
});
res.json({ success: true });
} catch (err) {
console.error('Token refresh error:', err);
res.clearCookie('access_token', { path: '/' });
res.clearCookie('refresh_token', { path: '/' });
res.status(401).json({ error: 'Refresh failed' });
}
});
export default router;
Wire Up Express App¶
// app.ts
import express from 'express';
import session from 'express-session';
import cookieParser from 'cookie-parser';
import authRoutes from './routes/auth';
const app = express();
app.use(cookieParser());
app.use(session({
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 10 * 60 * 1000, // 10 minutes (for OAuth flow)
},
}));
app.use('/api/auth', authRoutes);
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Next.js Setup (App Router)¶
Login Route¶
// app/api/auth/login/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
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: process.env.OAUTH_REDIRECT_URI!,
});
export async function GET(request: Request) {
const { authorizeUrl, state, codeVerifier } = oauth.startFlow();
const cookieStore = await cookies();
cookieStore.set('oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 10,
path: '/',
});
cookieStore.set('code_verifier', codeVerifier, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 10,
path: '/',
});
const { searchParams } = new URL(request.url);
const returnTo = searchParams.get('returnTo');
if (returnTo) {
cookieStore.set('auth_return_to', returnTo, {
httpOnly: true,
maxAge: 60 * 10,
path: '/',
});
}
return NextResponse.redirect(authorizeUrl);
}
Callback Route¶
// app/api/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
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: process.env.OAUTH_REDIRECT_URI!,
});
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
if (error) {
return NextResponse.redirect(new URL(`/login?error=${error}`, request.url));
}
const cookieStore = await cookies();
const savedState = cookieStore.get('oauth_state')?.value;
const codeVerifier = cookieStore.get('code_verifier')?.value;
const returnTo = cookieStore.get('auth_return_to')?.value || '/dashboard';
try {
const tokens = await oauth.handleCallback(
code!,
state!,
savedState,
codeVerifier
);
const response = NextResponse.redirect(new URL(returnTo, request.url));
response.cookies.set('access_token', tokens.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60,
path: '/',
});
if (tokens.refresh_token) {
response.cookies.set('refresh_token', tokens.refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60,
path: '/',
});
}
response.cookies.delete('oauth_state');
response.cookies.delete('code_verifier');
response.cookies.delete('auth_return_to');
return response;
} catch (err) {
console.error('OAuth callback error:', err);
return NextResponse.redirect(new URL('/login?error=callback_failed', request.url));
}
}
Me Route¶
// app/api/auth/me/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { authvital } from '@/lib/authvital';
export async function GET(request: NextRequest) {
const { authenticated, user, error } = await authvital.getCurrentUser(request);
if (!authenticated) {
return NextResponse.json(
{ authenticated: false, error: error || 'Not authenticated' },
{ status: 401 }
);
}
return NextResponse.json({
authenticated: true,
user: {
id: user.sub,
email: user.email,
name: user.name || `${user.given_name || ''} ${user.family_name || ''}`.trim(),
picture: user.picture,
tenantId: user.tenant_id,
tenantRoles: user.tenant_roles || [],
appPermissions: user.app_permissions || [],
license: user.license,
},
});
}
Logout Route¶
// app/api/auth/logout/route.ts
import { NextResponse } from 'next/server';
export async function POST() {
const response = NextResponse.json({ success: true });
response.cookies.delete('access_token');
response.cookies.delete('refresh_token');
return response;
}
Protected Middleware¶
See Common Patterns for reusable middleware examples.
Next Steps¶
- Database & Identity Sync - Set up local identity sync
- Frontend Setup - React provider and hooks