Skip to Content
AgentPress is finally here! šŸŽ‰
ArchitectureAuthentication

Authentication & Sessions

This document describes the authentication implementation in the AgentPress monorepo.

Overview

AgentPress uses session-based authentication with Lucia Auth:

  • Sessions stored in PostgreSQL
  • HTTP-only cookies for token storage
  • OAuth support (GitHub, Google)
  • External provider integration
  • Role-based access control (Admin, Guest)

Key Libraries

LibraryVersionPurpose
lucia3.2.2Session management
@lucia-auth/adapter-drizzle1.1.0Database adapter
arctic3.6.0OAuth provider management
bcrypt6.0.0Password hashing

Session Configuration

File: packages/api/src/auth/auth.ts

export const lucia = new Lucia(adapter, { sessionCookie: { expires: true, attributes: { secure: true, // HTTPS only in production sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", }, }, getUserAttributes: (attributes) => ({ email: attributes.email, username: attributes.username, firstName: attributes.firstName, lastName: attributes.lastName, githubId: attributes.githubId, googleId: attributes.googleId, role: attributes.role, provider: attributes.provider, }), });

Session Duration

const MAX_AGE = 60 * 60 * 24 * 30; // 30 days

Authentication Flows

Credentials Login

Endpoint: POST /auth/login/credentials

// Request { email: string, password: string } // Flow 1. Validate input with Zod schema 2. Rate limit check (10 requests/60 seconds) 3. Find user by email 4. Verify password: bcrypt.compare(password, user.password) 5. Create session: lucia.createSession(user.id, {}) 6. Set HTTP-only cookie 7. Return user object

GitHub OAuth

Step 1: Initiate

GET /auth/github ā”œā”€ Generate random state ā”œā”€ Store state in cookie (httpOnly) └─ Redirect to GitHub authorization URL

Step 2: Callback

GET /auth/github/callback?code=CODE&state=STATE ā”œā”€ Validate state matches cookie ā”œā”€ Exchange code for access token ā”œā”€ Fetch user from GitHub API ā”œā”€ Create/update user ā”œā”€ Create session └─ Redirect to APP_HOST

Google OAuth

Similar to GitHub with PKCE:

GET /auth/google ā”œā”€ Generate state + code verifier ā”œā”€ Store both in cookies └─ Redirect to Google authorization GET /auth/google/callback ā”œā”€ Validate state ā”œā”€ Exchange code + verifier for token ā”œā”€ Fetch user from Google OpenID └─ Create session

External Provider

Endpoint: POST /auth/external

// Request { authToken: string, providerName?: string } // Flow 1. Get provider: getExternalAuthProvider(providerName) 2. Validate token: provider.validateToken(authToken) 3. Find/create user by externalUserId 4. Encrypt and store authToken 5. Create session

Logout

Endpoint: POST /auth/logout

1. Get session ID from cookie 2. Invalidate: lucia.invalidateSession(sessionId) 3. Clear session cookie (maxAge: 0)

Password Reset

Request Reset:

POST /auth/forgot-password { email } ā”œā”€ Find user by email ā”œā”€ Create reset token with expiration ā”œā”€ Send email with reset link └─ Return generic success (don't reveal user existence)

Complete Reset:

POST /auth/reset-password { token, newPassword } ā”œā”€ Validate token not expired/used ā”œā”€ Hash new password (bcrypt, 12 rounds) ā”œā”€ Update user password └─ Mark token as used

Authentication Middleware

File: packages/api/src/middleware/auth.ts

sessionMiddleware

Default middleware for authenticated routes:

export const sessionMiddleware = (routeRole: EUserRole = EUserRole.GUEST) => { return async (req: Request, res: Response, next: NextFunction) => { const sessionToken = req.cookies?.session; if (!sessionToken) { return res.status(401).json({ error: "Unauthorized: No session found" }); } const { session, user } = await lucia.validateSession(sessionToken); if (!user || !session) { return res.status(401).json({ error: "Unauthorized: Invalid session" }); } // Role-based access control if (routeRole === EUserRole.ADMIN && user.role !== EUserRole.ADMIN) { return res.status(403).json({ error: "Forbidden: Requires Admin role" }); } req.user = user; req.session = session; next(); }; };

flexibleSessionMiddleware

Accepts token from headers OR cookies:

export const flexibleSessionMiddleware = (requiredRole = EUserRole.ADMIN) => { const cookieToken = req.cookies?.session; const headerToken = req.headers.authorization?.replace("Bearer ", "") || req.headers.session; const sessionToken = headerToken || cookieToken; // ... validation };

optionalSessionMiddleware

Extracts session if present, doesn’t fail if missing:

export const optionalSessionMiddleware = async (req, res, next) => { const sessionToken = req.cookies?.session; if (sessionToken) { try { const { session, user } = await lucia.validateSession(sessionToken); if (user && session) { req.user = user; req.session = session; } } catch { /* ignore */ } } next(); };

Security Features

Password Hashing

// On registration/reset const hashedPassword = await bcrypt.hash(newPassword, 12); // On login const isValid = await bcrypt.compare(password, user.password);

Token Encryption

External provider tokens are encrypted using AES-256-GCM:

File: packages/api/src/utils/tokenEncryption.ts

class EncryptionService { private algorithm = "aes-256-gcm"; async encrypt(text: string): Promise<string> { const salt = randomBytes(32); const iv = randomBytes(16); const key = await this.deriveKey(masterKey, salt); const cipher = createCipheriv("aes-256-gcm", key, iv); // ... encryption logic } }

Required: ENCRYPTION_MASTER_KEY environment variable

res.cookie("session", sessionId, { secure: true, // HTTPS only httpOnly: true, // No JS access sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days });

Rate Limiting

const authLimiter = rateLimit({ windowMs: 60000, // 1 minute max: 10, // 10 requests }); // Applied to OAuth and auth endpoints router.use("/github", authLimiter); router.use("/google", authLimiter);

External Auth Provider Interface

File: packages/api/src/auth/externalAuthProvider.ts

export interface IExternalAuthProvider { name: string; validateToken(authToken: string): Promise<{ isValid: boolean; userData?: { externalUserId: string; email: string; firstName?: string; lastName?: string; isAdmin?: boolean; }; error?: string; }>; getUserLimits?(externalUserId: string, authToken: string): Promise<{ maxMessages?: number; resetDate?: Date; }>; } // Registry functions export function registerExternalAuthProvider(provider: IExternalAuthProvider): void; export function getExternalAuthProvider(name: string): IExternalAuthProvider;

Environment Variables

Required

ENCRYPTION_MASTER_KEY=<required> APP_HOST=http://localhost:3000 API_HOST=http://localhost:3001

OAuth (Optional)

GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET=

Usage Examples

Route Protection

// Require any authenticated user (guest or admin) router.use(sessionMiddleware(EUserRole.GUEST)); // Require admin role router.use(sessionMiddleware(EUserRole.ADMIN)); // Optional authentication router.use(optionalSessionMiddleware);

Accessing User in Routes

router.get("/profile", async (req: Request, res: Response) => { const { id, email, role } = req.user; res.json({ id, email, role }); });

Frontend Token Usage

// Token is automatically sent via cookies const response = await fetch("/api/threads", { credentials: "include", }); // Or via header for cross-origin const response = await fetch("/api/threads", { headers: { Authorization: `Bearer ${sessionToken}`, }, });

Key Files

FilePurpose
packages/api/src/auth/auth.tsLucia configuration
packages/api/src/auth/externalAuthProvider.tsExternal provider interface
packages/api/src/routes/auth.tsAll auth endpoints
packages/api/src/middleware/auth.tsSession middleware
packages/api/src/db/schema/sessions.tsSession table
packages/api/src/db/schema/users.tsUser table
Last updated on