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.

Related Documentation