Skip to content

Instantly share code, notes, and snippets.

@elliott-w
Created November 3, 2025 03:27
Show Gist options
  • Select an option

  • Save elliott-w/f5da3ee6127da7fbae532520d20ccf7d to your computer and use it in GitHub Desktop.

Select an option

Save elliott-w/f5da3ee6127da7fbae532520d20ccf7d to your computer and use it in GitHub Desktop.
Middleware to Force Payload Login
import { env } from '@env'
import { jwtVerify } from 'jose'
import { NextRequest, NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Skip middleware for static files and assets
if (isStaticFile(pathname)) {
return NextResponse.next()
}
if (env.APP_ENV === 'staging') {
if (await shouldRedirectToAdmin(request)) {
const params = new URLSearchParams()
params.set('redirect', request.nextUrl.pathname)
const adminUrl = new URL(`/admin/login?${params.toString()}`, request.url)
return NextResponse.redirect(adminUrl)
}
}
return NextResponse.next()
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - admin (Payload admin panel)
*/
'/((?!api|_next/static|_next/image|admin).*)',
],
}
/**
* Most performant file detection - uses a single regex pattern
* Pre-compiled regex is faster than array iterations or multiple checks
*/
const STATIC_FILE_REGEX =
/\.(js|css|png|jpe?g|gif|svg|ico|woff2?|ttf|eot|mp4|webm|pdf|zip|json|xml|txt)$/i
function isStaticFile(pathname: string): boolean {
return STATIC_FILE_REGEX.test(pathname)
}
const shouldRedirectToAdmin = async (
request: NextRequest
): Promise<boolean> => {
if (process.env.NODE_ENV === 'production') {
const payloadSecret = process.env.PAYLOAD_SECRET!
// Check if user is authenticated
const { isAuthenticated } = await isUserAuthenticated(request, {
payloadSecret,
cookieName: 'payload-token', // Default Payload cookie name
requiredCollection: 'users', // Optional: require specific collection
})
return !isAuthenticated
}
return false
}
// Types for Payload JWT payload
interface PayloadJWTPayload {
id: string
email: string
collection: string
iat: number
exp: number
[key: string]: any // For additional fields saved to JWT via saveToJWT
}
/**
* Creates the hashed secret that Payload uses internally for JWT signing
* Payload hashes the secret with SHA-256 and takes the first 32 characters
*/
async function createPayloadSecret(originalSecret: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(originalSecret)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
return hashHex.slice(0, 32)
}
/**
* Lightweight JWT verification for Payload tokens without initializing Payload
* Returns the decoded user data or null if invalid/expired
*/
async function verifyPayloadJWT(
token: string,
payloadSecret: string
): Promise<PayloadJWTPayload | null> {
try {
// Hash the secret the same way Payload does internally
const hashedSecret = await createPayloadSecret(payloadSecret)
// Convert to Uint8Array for jose library
const secretKey = new TextEncoder().encode(hashedSecret)
// Verify and decode the JWT
const { payload } = await jwtVerify(token, secretKey, {
algorithms: ['HS256'], // Payload uses HS256 by default
})
return payload as PayloadJWTPayload
} catch (error) {
// Token is invalid, expired, or malformed
console.error('JWT verification failed:', error)
return null
}
}
/**
* Extract JWT token from various sources (cookie, Authorization header)
*/
function extractToken(
request: NextRequest,
cookieName: string = 'payload-token'
): string | null {
// Try to get from cookie first (most common for Payload)
const cookieToken = request.cookies.get(cookieName)?.value
if (cookieToken) {
return cookieToken
}
// Try Authorization header as fallback
const authHeader = request.headers.get('authorization')
if (authHeader?.startsWith('Bearer ')) {
return authHeader.substring(7)
}
return null
}
/**
* Check if user is authenticated with optional role/permission checks
*/
async function isUserAuthenticated(
request: NextRequest,
options: {
payloadSecret: string
cookieName?: string
requiredCollection?: string
}
): Promise<{ isAuthenticated: boolean; user: PayloadJWTPayload | null }> {
const {
payloadSecret,
cookieName = 'payload-token',
requiredCollection,
} = options
// Extract token
const token = extractToken(request, cookieName)
if (!token) {
return { isAuthenticated: false, user: null }
}
// Verify token
const user = await verifyPayloadJWT(token, payloadSecret)
if (!user) {
return { isAuthenticated: false, user: null }
}
// Check collection requirement
if (requiredCollection && user.collection !== requiredCollection) {
return { isAuthenticated: false, user: null }
}
return { isAuthenticated: true, user }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment