Skip to content

Next.js Integration

This guide walks you through adding auth to a new Next.js app from scratch.

  1. Set up environment variables

    Create .env.local:

    Terminal window
    APP_URL=http://localhost:3006
    JWT_SECRET=<64-char hex, from project signing_key>
    AUTH_CLIENT_SECRET=<64-char hex, from project creation>
  2. Create environment validation

    lib/env.ts
    function required(name: string): string {
    const val = process.env[name];
    if (!val) throw new Error(`Missing env: ${name}`);
    return val;
    }
    export const env = {
    APP_URL: required('APP_URL'),
    JWT_SECRET: required('JWT_SECRET'),
    AUTH_CLIENT_SECRET: required('AUTH_CLIENT_SECRET'),
    IS_PRODUCTION: process.env.NODE_ENV === 'production',
    };
  3. Define constants

    lib/constants.ts
    export const CLIENT_ID = 'proj_yourapp';
    export const AUTH_ISSUER = 'https://auth.beshoy.ai';
    export const ACCESS_TOKEN_COOKIE = 'yourapp_access_token';
    export const REFRESH_TOKEN_COOKIE = 'yourapp_auth_token';
    export const USER_ID_COOKIE = 'yourapp_uid';
    export const ACCESS_TOKEN_EXPIRY = 300; // 5 minutes
    export const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days
  4. Create state encryption helpers

    lib/auth/crypto.ts
    const secret = new TextEncoder().encode(env.JWT_SECRET);
    export async function encryptState(data: { v: string; r: string }): Promise<string> {
    const key = await crypto.subtle.importKey(
    'raw',
    secret.slice(0, 32),
    'AES-GCM',
    false,
    ['encrypt'],
    );
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const plaintext = new TextEncoder().encode(JSON.stringify(data));
    const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    plaintext,
    );
    const combined = new Uint8Array(iv.length + new Uint8Array(ciphertext).length);
    combined.set(iv);
    combined.set(new Uint8Array(ciphertext), iv.length);
    return btoa(String.fromCharCode(...combined))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
    }
    export async function decryptState(
    state: string,
    ): Promise<{ v: string; r: string } | null> {
    try {
    const key = await crypto.subtle.importKey(
    'raw',
    secret.slice(0, 32),
    'AES-GCM',
    false,
    ['decrypt'],
    );
    let b64 = state.replace(/-/g, '+').replace(/_/g, '/');
    while (b64.length % 4) b64 += '=';
    const raw = atob(b64);
    const bytes = Uint8Array.from(raw, (c) => c.charCodeAt(0));
    const iv = bytes.slice(0, 12);
    const ciphertext = bytes.slice(12);
    const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv },
    key,
    ciphertext,
    );
    return JSON.parse(new TextDecoder().decode(plaintext));
    } catch {
    return null;
    }
    }
  5. Create the auth proxy (middleware)

    // proxy.ts (Next.js middleware)
    import { NextRequest, NextResponse } from 'next/server';
    import { jwtVerify, errors } from 'jose';
    const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
    export async function middleware(req: NextRequest) {
    // Skip public routes
    if (req.nextUrl.pathname.startsWith('/api/auth/')) {
    return NextResponse.next();
    }
    const accessToken = req.cookies.get('yourapp_access_token')?.value;
    const refreshToken = req.cookies.get('yourapp_auth_token')?.value;
    // Try access token
    if (accessToken) {
    try {
    const { payload } = await jwtVerify(accessToken, secret);
    if (payload.status === 'blocked') {
    const res = NextResponse.json(
    { error: 'Account unavailable' },
    { status: 403 },
    );
    res.cookies.delete('yourapp_access_token');
    res.cookies.delete('yourapp_auth_token');
    res.cookies.delete('yourapp_uid');
    return res;
    }
    return NextResponse.next();
    } catch (e) {
    if (!(e instanceof errors.JWTExpired)) {
    // Token is invalid (not just expired) — clear and redirect
    return redirectToLogin(req);
    }
    }
    }
    // Try refresh
    if (refreshToken) {
    const res = await fetch('https://auth.beshoy.ai/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: 'proj_yourapp',
    client_secret: process.env.AUTH_CLIENT_SECRET,
    }),
    });
    if (res.ok) {
    const tokens = await res.json();
    const { payload } = await jwtVerify(tokens.access_token, secret);
    const response = NextResponse.next();
    const secure = process.env.NODE_ENV === 'production';
    const domain = secure ? '.beshoy.ai' : undefined;
    response.cookies.set('yourapp_access_token', tokens.access_token, {
    httpOnly: true, secure, sameSite: 'lax', path: '/', domain,
    maxAge: 300,
    });
    response.cookies.set('yourapp_auth_token', tokens.refresh_token, {
    httpOnly: true, secure, sameSite: 'lax', path: '/', domain,
    maxAge: 7 * 24 * 60 * 60,
    });
    response.cookies.set('yourapp_uid', payload.sub as string, {
    httpOnly: false, secure, sameSite: 'lax', path: '/', domain,
    maxAge: 7 * 24 * 60 * 60,
    });
    return response;
    }
    }
    // No valid auth — redirect to login
    return redirectToLogin(req);
    }
    async function redirectToLogin(req: NextRequest) {
    const codeVerifier = Buffer.from(
    crypto.getRandomValues(new Uint8Array(32)),
    ).toString('base64url');
    const codeChallenge = Buffer.from(
    await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(codeVerifier),
    ),
    ).toString('base64url');
    // Encrypt state with verifier + return URL
    const state = await encryptState({
    v: codeVerifier,
    r: req.nextUrl.pathname + req.nextUrl.search,
    });
    const authUrl = new URL('https://auth.beshoy.ai/oauth/authorize');
    authUrl.searchParams.set('client_id', 'proj_yourapp');
    authUrl.searchParams.set('redirect_uri', `${process.env.APP_URL}/api/auth/callback`);
    authUrl.searchParams.set('response_type', 'code');
    authUrl.searchParams.set('code_challenge', codeChallenge);
    authUrl.searchParams.set('code_challenge_method', 'S256');
    authUrl.searchParams.set('state', state);
    return NextResponse.redirect(authUrl);
    }
    export const config = {
    matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
    };
  6. Create the callback route

    app/api/auth/callback/route.ts
    import { jwtVerify } from 'jose';
    import { cookies } from 'next/headers';
    import { redirect } from 'next/navigation';
    import { decryptState } from '@/lib/auth/crypto';
    import { env } from '@/lib/env';
    const secret = new TextEncoder().encode(env.JWT_SECRET);
    export async function GET(req: Request) {
    const url = new URL(req.url);
    const code = url.searchParams.get('code');
    const state = url.searchParams.get('state');
    if (!code || !state) redirect('/');
    const stateData = await decryptState(state);
    if (!stateData) redirect('/');
    const { v: codeVerifier, r: returnTo } = stateData;
    // Exchange code for tokens
    const tokenRes = await fetch('https://auth.beshoy.ai/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    code_verifier: codeVerifier,
    client_id: 'proj_yourapp',
    client_secret: env.AUTH_CLIENT_SECRET,
    redirect_uri: `${env.APP_URL}/api/auth/callback`,
    }),
    });
    if (!tokenRes.ok) redirect('/');
    const tokens = await tokenRes.json();
    const { payload } = await jwtVerify(tokens.access_token, secret);
    const userId = payload.sub as string;
    const secure = env.IS_PRODUCTION;
    const domain = secure ? '.beshoy.ai' : undefined;
    const jar = await cookies();
    jar.set('yourapp_access_token', tokens.access_token, {
    httpOnly: true, secure, sameSite: 'lax', path: '/', domain, maxAge: 300,
    });
    jar.set('yourapp_auth_token', tokens.refresh_token, {
    httpOnly: true, secure, sameSite: 'lax', path: '/', domain,
    maxAge: 7 * 24 * 60 * 60,
    });
    jar.set('yourapp_uid', userId, {
    httpOnly: false, secure, sameSite: 'lax', path: '/', domain,
    maxAge: 7 * 24 * 60 * 60,
    });
    // Validate returnTo
    let safeReturnTo = '/';
    if (returnTo) {
    try {
    const parsed = new URL(returnTo, 'https://yourapp.beshoy.ai');
    if (parsed.hostname.endsWith('.beshoy.ai')) {
    safeReturnTo = returnTo;
    }
    } catch {}
    }
    redirect(safeReturnTo);
    }
  7. Create the logout route

    app/api/auth/logout/route.ts
    import { cookies } from 'next/headers';
    import { redirect } from 'next/navigation';
    import { env } from '@/lib/env';
    export async function POST() {
    const jar = await cookies();
    const refreshToken = jar.get('yourapp_auth_token')?.value;
    const secure = env.IS_PRODUCTION;
    const domain = secure ? '.beshoy.ai' : undefined;
    // Revoke server-side
    if (refreshToken) {
    fetch('https://auth.beshoy.ai/oauth/revoke', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
    token: refreshToken,
    client_id: 'proj_yourapp',
    }),
    });
    }
    // Clear cookies
    jar.delete({ name: 'yourapp_access_token', domain, path: '/' });
    jar.delete({ name: 'yourapp_auth_token', domain, path: '/' });
    jar.delete({ name: 'yourapp_uid', domain, path: '/' });
    redirect('/');
    }
  8. Read user info in server components

    lib/auth/session.ts
    import { jwtVerify } from 'jose';
    import { cookies } from 'next/headers';
    const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
    export async function getSession() {
    const jar = await cookies();
    const token = jar.get('yourapp_access_token')?.value;
    if (!token) return null;
    try {
    const { payload } = await jwtVerify(token, secret);
    return {
    userId: payload.sub as string,
    email: payload.email as string,
    role: payload.role as string,
    status: payload.status as string,
    };
    } catch {
    return null;
    }
    }
  • Environment variables set (JWT_SECRET, AUTH_CLIENT_SECRET, APP_URL)
  • Project created in auth service with correct redirect URIs
  • Middleware redirects unauthenticated users
  • Callback route exchanges code and sets cookies
  • Logout route revokes token and clears cookies
  • Return URLs validated with hostname.endsWith('.beshoy.ai')
  • Cookies set with correct domain, httpOnly, secure, sameSite flags