| name | authentication |
| description | This skill should be used when the user wants to "add authentication to Directus + Next.js", "implement Directus login in Next.js", "use NextAuth with Directus", "protect Next.js pages with Directus auth", "set up cookie-based auth for Directus SSR", or needs patterns for authenticating users across Directus and Next.js. |
Authentication: Directus + NextAuth
Patterns for user authentication combining Directus as the identity provider with NextAuth.js for session management in Next.js.
Two Authentication Modes
1. Admin token (server-only) — For backend operations, content fetching, and management tasks. Uses staticToken() on the Directus client. Already configured in lib/directus.ts.
2. User authentication (cookie-based) — For end-user login/registration. Uses NextAuth.js with a Directus CredentialsProvider. Described below.
Install NextAuth
npm install next-auth
NextAuth Configuration
Create lib/auth.ts:
import type { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'Directus',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const res = await fetch(`${process.env.NEXT_PUBLIC_DIRECTUS_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
});
if (!res.ok) return null;
const { data } = await res.json();
const userRes = await fetch(`${process.env.NEXT_PUBLIC_DIRECTUS_URL}/users/me`, {
headers: { Authorization: `Bearer ${data.access_token}` },
});
const { data: user } = await userRes.json();
return {
id: user.id,
email: user.email,
name: `${user.first_name ?? ''} ${user.last_name ?? ''}`.trim(),
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires,
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken;
token.expiresAt = user.expiresAt;
}
if (Date.now() > (token.expiresAt as number)) {
const res = await fetch(`${process.env.NEXT_PUBLIC_DIRECTUS_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: token.refreshToken }),
});
if (res.ok) {
const { data } = await res.json();
token.accessToken = data.access_token;
token.refreshToken = data.refresh_token;
token.expiresAt = Date.now() + data.expires;
}
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
return session;
},
},
pages: {
signIn: '/login',
},
secret: process.env.NEXTAUTH_SECRET,
};
API Route Handler
Create app/api/auth/[...nextauth]/route.ts:
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Session Provider
Create app/providers.tsx (Client Component):
'use client';
import { SessionProvider } from 'next-auth/react';
export default function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
Wrap in app/layout.tsx:
import Providers from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Middleware Protection
Create middleware.ts in the project root:
import { getToken } from 'next-auth/jwt';
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(request: NextRequest) {
const token = await getToken({ req: request });
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', request.url);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
Per-User Directus Client
Create a request-scoped Directus client using the authenticated user's token:
import { createDirectus, rest, staticToken } from '@directus/sdk';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import type { Schema } from '@/types/directus';
export async function getUserDirectus() {
const session = await getServerSession(authOptions);
if (!session?.accessToken) throw new Error('Not authenticated');
return createDirectus<Schema>(process.env.NEXT_PUBLIC_DIRECTUS_URL!)
.with(staticToken(session.accessToken))
.with(rest({ cache: 'no-store' }));
}
Use in Server Components for user-scoped data:
const directus = await getUserDirectus();
const myPosts = await directus.request(readItems('posts'));
Login Page
Create app/login/page.tsx:
'use client';
import { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [error, setError] = useState('');
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const result = await signIn('credentials', {
email: formData.get('email'),
password: formData.get('password'),
redirect: false,
});
if (result?.error) {
setError('Invalid email or password');
} {
router.(searchParams.() || );
}
}
(
);
}
TypeScript Augmentation
Extend NextAuth types to include Directus tokens. Create types/next-auth.d.ts:
import 'next-auth';
declare module 'next-auth' {
interface User {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
interface Session {
accessToken: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
}
CORS Configuration
Directus must allow requests from the Next.js domain. Set in Directus environment:
CORS_ENABLED=true
CORS_ORIGIN=http://localhost:3000,https://your-production-domain.com
For Docker local dev, this is already configured in docker-compose.yml.