Authentication System
JWT-based authentication with typed payloads and realm access control.
Authentication Overview
Autonomy uses JSON Web Tokens (JWT) for stateless authentication with the following characteristics:
- HTTP-only cookies — Tokens stored securely, not accessible to JavaScript
- Typed payloads — TypeScript interfaces enforce token structure
- 7-day expiration — Balance between convenience and security
- Server-side validation — All routes verify tokens before granting access
JWT Token Structure
AuthPayload Interface
// lib/types/auth.ts
import type { JWTPayload } from 'jose'
export interface AuthPayload extends JWTPayload {
user_id: string // User's unique identifier
email: string // User's email address
role: string // User's role (OWNER, SANCTUM, GUEST)
}This typed interface ensures tokens always contain the required fields and TypeScript enforces correct usage throughout the codebase.
Login Flow
Step 1: User Submits Credentials
User enters email and password in login form at /admin/login
// Form submits to API
fetch('/api/admin/auth/login', {
method: 'POST',
body: JSON.stringify({
user_email: email,
user_password: password
})
})Step 2: Server Validates Credentials
API route queries database and verifies password with bcrypt
// lib/queries/user.ts
export async function authenticateUser(data: LoginInput) {
const { user_email, user_password } = data
const user = await prisma.user.findUnique({
where: { user_email }
})
if (!user) return null
const isValid = await bcrypt.compare(
user_password,
user.user_password
)
return isValid ? user : null
}Step 3: Generate JWT Token
If credentials valid, create signed JWT with user data
// app/api/admin/auth/login/route.ts
const payload: AuthPayload = {
user_id: user.user_id,
email: user.user_email,
role: user.user_role,
}
const token = await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(JWT_SECRET)Step 4: Set HTTP-Only Cookie
Token stored in secure cookie, not accessible to client-side JavaScript
response.cookies.set('auth_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
})Step 5: Client Redirects
On successful login, client redirects to /admin/signals
Authentication Utilities
getCurrentUser()
Returns authenticated user or null. Does not throw errors.
// lib/utils/auth.ts
export async function getCurrentUser(): Promise<AuthPayload | null> {
try {
const cookieStore = await cookies()
const token = cookieStore.get('auth_token')?.value
if (!token) return null
const { payload } = await jwtVerify(token, JWT_SECRET)
return payload as AuthPayload
} catch {
return null
}
}Use case: Optional auth checks, displaying user info in UI
requireAuth()
Requires authentication. Redirects to login if not authenticated.
export async function requireAuth(): Promise<AuthPayload> {
const user = await getCurrentUser()
if (!user) {
redirect('/admin/login') // Server-side redirect
}
return user
}Use case: Server components (pages) that require authentication
requireAuthAPI()
Requires authentication for API routes. Throws error if not authenticated.
export async function requireAuthAPI(): Promise<AuthPayload> {
const user = await getCurrentUser()
if (!user) {
throw new Error('Not authenticated') // Returns 401
}
return user
}Use case: API route handlers that return JSON errors
Usage Examples
Example 1: Protected Page
// app/admin/signals/page.tsx
import { requireAuth } from '@/lib/utils/auth'
export default async function SignalsPage() {
const user = await requireAuth() // Redirects if not authenticated
const signals = await querySignals({}, user.user_id)
return <div>...</div>
}Example 2: API Route
// app/api/admin/signals/route.ts
import { requireAuthAPI } from '@/lib/utils/auth'
export async function GET(request: NextRequest) {
const user = await requireAuthAPI() // Throws if not authenticated
const signals = await querySignals({}, user.user_id)
return NextResponse.json(signals)
}Example 3: Optional Auth
// components/SiteNavigation.tsx
const user = await getCurrentUser() // Returns null if not authenticated
return (
<nav>
{user ? (
<Link href="/admin/signals">Admin</Link>
) : (
<Link href="/admin/login">Login</Link>
)}
</nav>
)Logout Flow
Logout is handled by clearing the authentication cookie:
// app/api/admin/auth/logout/route.ts
export async function POST() {
const response = NextResponse.json({ success: true })
response.cookies.set('auth_token', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0, // Expire immediately
path: '/',
})
return response
}Security Considerations
✅ HTTP-Only Cookies
Tokens not accessible to JavaScript, preventing XSS attacks from stealing credentials.
✅ Secure Flag (Production)
Cookies only sent over HTTPS in production, preventing man-in-the-middle attacks.
✅ SameSite Protection
SameSite=lax prevents CSRF attacks while allowing normal navigation.
✅ Bcrypt Password Hashing
Passwords hashed with bcrypt (cost factor 10) before storage.
✅ Server-Side Validation
All auth checks happen server-side. Client cannot bypass security.
✅ Type Safety
TypeScript enforces correct usage of auth payloads throughout codebase.
Required Environment Variables
Set these in your .env file:
JWT_SECRET="your-secure-secret-key-here" NODE_ENV="production" # or "development"
Important: Use a long, random string for JWT_SECRET in production. Never commit it to version control.
Password Requirements
Enforced during user creation (via npm run create:owner):
- • Minimum 8 characters
- • Maximum 72 characters (bcrypt limit)
- • At least one lowercase letter
- • At least one uppercase letter
- • At least one number
- • At least one special character
Future Enhancements
Refresh Tokens
Long-lived refresh tokens for better UX without compromising security.
Two-Factor Authentication
TOTP-based 2FA for enhanced account security.
OAuth Integration
Sign in with GitHub, Google, or other providers.
Session Management
View and revoke active sessions from multiple devices.