@artatol-acp/auth-nextjs
v0.7.0
Published
Next.js SDK for Artatol Cloud Platform Authentication with support for App Router, Server Actions, and Middleware
Downloads
156
Maintainers
Readme
@artatol-acp/auth-nextjs
Next.js SDK for Artatol Cloud Platform Authentication with support for App Router, Server Actions, and automatic token refresh.
Installation
pnpm add @artatol-acp/auth-nextjsPrerequisites
Before using this SDK, you need from the ACP AUTH service:
- Base URL - The auth service URL (e.g.,
https://sso.artatol.net) - API Key - Required for token refresh in proxy/middleware
- JWT Public Key - For local JWT verification. Get it from:
curl https://sso.artatol.net/public-key > public.pem
How Authentication Works
The SDK uses httpOnly cookies for secure token storage:
access_token- Short-lived JWT (5 minutes), used for API callsrefresh_token- Long-lived token (7 days), used to get new access tokens
Important: The auth server rotates refresh tokens on each use. After a refresh, the old refresh token is invalidated and a new one is issued.
Quick Start
1. Set Up Proxy/Middleware (REQUIRED)
CRITICAL: The proxy is required for automatic token refresh. Without it, users will be logged out when their access token expires (every 5 minutes).
Why is proxy required?
In Next.js App Router, cookies can only be modified in:
- Route Handlers (
app/api/...) - Server Actions
- Middleware/Proxy
Server Components cannot set cookies. When a user visits a protected page with an expired access token, the page render cannot refresh the token. The proxy intercepts the request before the page renders and handles the refresh.
Create the proxy file:
| Next.js Version | File Location |
|-----------------|---------------|
| 16+ | src/proxy.ts or proxy.ts (root) |
| 14-15 | src/middleware.ts or middleware.ts (root) |
Note: If your project uses a
src/directory, the file must be insidesrc/.
// src/proxy.ts (Next.js 16+)
// or src/middleware.ts (Next.js 14-15)
import { NextRequest, NextResponse } from 'next/server'
import { createACPAuthMiddleware } from '@artatol-acp/auth-nextjs/proxy'
// For Next.js 14-15: import { createACPAuthMiddleware } from '@artatol-acp/auth-nextjs/middleware'
const acpMiddleware = createACPAuthMiddleware({
baseUrl: process.env.ACP_AUTH_URL!,
apiKey: process.env.ACP_AUTH_API_KEY!,
jwtPublicKey: process.env.ACP_AUTH_JWT_PUBLIC_KEY!,
publicPaths: ['/login', '/register', '/forgot-password', '/reset-password', '/verify-email'],
loginPath: '/login',
cookies: {
domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : undefined,
},
})
export default async function middleware(request: NextRequest) {
// Homepage is public - handle with exact match
if (request.nextUrl.pathname === '/') {
return NextResponse.next()
}
return acpMiddleware(request)
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|icon).*)'],
}WARNING about publicPaths:
The publicPaths option uses prefix matching (pathname.startsWith(path)). This means:
/loginmatches/login,/login/,/login/callback, etc.- NEVER include just
/in publicPaths - it would match ALL paths!
Always handle the homepage (/) with an exact match check as shown above.
2. Create API Route Handlers
Create app/api/auth/[action]/route.ts:
import { createAuthHandlers } from '@artatol-acp/auth-nextjs/handlers'
const { authHandler } = createAuthHandlers({
baseUrl: process.env.ACP_AUTH_URL!,
apiKey: process.env.ACP_AUTH_API_KEY,
cookies: {
domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : undefined,
},
})
export const POST = authHandlerThis creates these endpoints:
POST /api/auth/login- LoginPOST /api/auth/logout- LogoutPOST /api/auth/session- Refresh session & get user (used by client-side refresh)POST /api/auth/register- RegisterPOST /api/auth/verify-2fa- Complete 2FA loginPOST /api/auth/resend-verification- Resend verification emailPOST /api/auth/forgot-password- Request password resetPOST /api/auth/reset-password- Reset password
3. Initialize Server-side Auth
Create lib/auth.ts:
import { initACPAuth } from '@artatol-acp/auth-nextjs/server'
// Initialize once at module load
initACPAuth({
baseUrl: process.env.ACP_AUTH_URL!,
apiKey: process.env.ACP_AUTH_API_KEY,
jwtPublicKey: process.env.ACP_AUTH_JWT_PUBLIC_KEY!,
cookies: {
domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : undefined,
},
})
// Re-export server functions
export {
getUser,
me,
login,
logout,
register,
verify2FALogin,
verifyEmail,
resendVerificationEmail,
forgotPassword,
resetPassword,
deleteAccount,
refreshAccessToken,
} from '@artatol-acp/auth-nextjs/server'4. Add Client Provider
In your root layout (app/layout.tsx):
import { ACPAuthProvider } from '@artatol-acp/auth-nextjs/client'
import { getUser } from '@/lib/auth'
export default async function RootLayout({ children }) {
// getUser() is fast - just verifies JWT locally, no API call
// Returns { id, email } or null
const user = await getUser()
return (
<html>
<body>
<ACPAuthProvider initialUser={user}>
{children}
</ACPAuthProvider>
</body>
</html>
)
}How client-side refresh works:
When initialUser is null (no access_token or expired), ACPAuthProvider automatically calls /api/auth/session to attempt a refresh using the refresh_token. If successful, the user state updates and new cookies are set. This enables seamless client-side navigation even when access_token has expired.
Usage
Server Components
import { getUser, me } from '@/lib/auth'
export default async function ProfilePage() {
// Fast local JWT verification (returns { id, email } or null)
const user = await getUser()
// Or fetch full user from API (returns { id, email, twoFactorEnabled } or null)
const fullUser = await me()
if (!user) {
return <div>Not logged in</div>
}
return <div>Welcome {user.email}</div>
}When to use getUser() vs me():
| Function | Speed | Returns | Use when |
|----------|-------|---------|----------|
| getUser() | Fast (local JWT) | { id, email } | You only need basic user info |
| me() | Slower (API call) | { id, email, twoFactorEnabled } | You need twoFactorEnabled status |
Server Actions
'use server'
import { login, logout, verify2FALogin } from '@/lib/auth'
import { redirect } from 'next/navigation'
export async function loginAction(formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
const result = await login(email, password)
if ('requiresTwoFactor' in result) {
return { requires2FA: true, tempToken: result.tempToken }
}
redirect('/dashboard')
}
export async function logoutAction() {
await logout()
redirect('/login')
}Client Components
'use client'
import { useAuth } from '@artatol-acp/auth-nextjs/client'
export function UserMenu() {
const { user, logout, isLoading } = useAuth()
if (isLoading) return <div>Loading...</div>
if (!user) return <a href="/login">Sign In</a>
return (
<div>
<span>{user.email}</span>
<button onClick={logout}>Logout</button>
</div>
)
}Environment Variables
ACP_AUTH_URL=https://sso.artatol.net
ACP_AUTH_API_KEY=your-api-key
ACP_AUTH_JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"API Reference
Proxy Options
createACPAuthMiddleware({
baseUrl: string, // Auth server URL (required)
apiKey: string, // API key for refresh calls (required)
jwtPublicKey: string, // PEM public key for JWT verification (required)
publicPaths?: string[], // Paths that don't require auth (default: ['/login', '/register', ...])
loginPath?: string, // Redirect path for unauthenticated users (default: '/login')
cookies?: {
domain?: string, // Cookie domain for SSO (e.g., '.yourdomain.com')
path?: string, // Cookie path (default: '/')
secure?: boolean, // HTTPS only (default: NODE_ENV === 'production')
sameSite?: 'strict' | 'lax' | 'none', // (default: 'lax')
},
})Server Functions
| Function | Description |
|----------|-------------|
| initACPAuth(options) | Initialize auth configuration (call once) |
| getUser() | Get user from JWT (local, fast) -> { id, email } or null |
| me() | Get user from API (full data) -> { id, email, twoFactorEnabled } or null |
| login(email, password) | Login, sets cookies -> { accessToken, user } or { requiresTwoFactor, tempToken } |
| verify2FALogin(tempToken, code) | Complete 2FA login |
| logout() | Logout, clears cookies |
| register(email, password) | Register new user |
| verifyEmail(token) | Verify email address |
| resendVerificationEmail(email) | Resend verification email |
| forgotPassword(email) | Request password reset |
| resetPassword(token, password) | Reset password |
| deleteAccount(password, confirmation) | Delete account |
| refreshAccessToken() | Manually refresh access token |
Client Hook
const {
user, // Current user or null
isLoading, // True during initial load or refresh
login, // (email, password) => Promise<{ requiresTwoFactor?, tempToken? }>
verify2FA, // (tempToken, code) => Promise<void>
logout, // () => Promise<void>
refresh, // () => Promise<boolean> - manually trigger session refresh
resendVerification, // (email) => Promise<void>
} = useAuth()ACPAuthProvider Props
<ACPAuthProvider
apiBasePath="/api/auth" // Optional, default: "/api/auth"
initialUser={user} // Optional, from getUser() - { id, email } or null
>
{children}
</ACPAuthProvider>Token Refresh Flow
Server-side (hard refresh, direct URL access)
- User visits protected page with expired
access_token - Proxy intercepts the request before page renders
- Proxy calls auth server
/refreshwithrefresh_token - Auth server validates, rotates refresh token, returns new tokens
- Proxy sets new
access_tokenandrefresh_tokencookies - Page renders with valid tokens
Client-side (navigation within app)
- User clicks link to protected page (client-side navigation)
- Proxy does NOT run (client navigation doesn't hit server)
ACPAuthProviderdetectsinitialUserisnull- Calls
/api/auth/sessionwhich refreshes tokens and sets cookies - User state updates, navigation proceeds
If API handlers are missing: Client-side refresh won't work, user gets logged out on navigation when access_token expires.
If proxy is missing: Server-side refresh won't work, user gets logged out on hard refresh when access_token expires.
Error Handling
import { ACPAuthError } from '@artatol-acp/auth-nextjs/server'
try {
await login(email, password)
} catch (error) {
if (error instanceof ACPAuthError) {
console.error('Status:', error.statusCode)
console.error('Message:', error.message)
console.error('Code:', error.code)
}
}| Status | Meaning | |--------|---------| | 400 | Validation error (bad input) | | 401 | Unauthorized (invalid credentials/token) | | 403 | Forbidden (email not verified, account locked) | | 429 | Too Many Requests (rate limited) |
Password Requirements
- Minimum 10 characters
- At least one lowercase letter (a-z)
- At least one uppercase letter (A-Z)
- At least one number (0-9)
SSO Configuration
For single sign-on across subdomains (e.g., app.example.com and admin.example.com), set the cookie domain:
// In proxy/middleware, handlers, and server init:
cookies: {
domain: '.example.com', // Note the leading dot
}License
MIT
