Next.js Integration
Complete Integration Guide
Section titled “Complete Integration Guide”This guide walks you through adding auth to a new Next.js app from scratch.
-
Set up environment variables
Create
.env.local:Terminal window APP_URL=http://localhost:3006JWT_SECRET=<64-char hex, from project signing_key>AUTH_CLIENT_SECRET=<64-char hex, from project creation> -
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',}; -
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 minutesexport const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days -
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;}} -
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 routesif (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 tokenif (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 redirectreturn redirectToLogin(req);}}}// Try refreshif (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 loginreturn 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 URLconst 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).*)'],}; -
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 tokensconst 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 returnTolet safeReturnTo = '/';if (returnTo) {try {const parsed = new URL(returnTo, 'https://yourapp.beshoy.ai');if (parsed.hostname.endsWith('.beshoy.ai')) {safeReturnTo = returnTo;}} catch {}}redirect(safeReturnTo);} -
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-sideif (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 cookiesjar.delete({ name: 'yourapp_access_token', domain, path: '/' });jar.delete({ name: 'yourapp_auth_token', domain, path: '/' });jar.delete({ name: 'yourapp_uid', domain, path: '/' });redirect('/');} -
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;}}
Checklist
Section titled “Checklist”- 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,sameSiteflags