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
| Library | Version | Purpose |
|---|---|---|
lucia | 3.2.2 | Session management |
@lucia-auth/adapter-drizzle | 1.1.0 | Database adapter |
arctic | 3.6.0 | OAuth provider management |
bcrypt | 6.0.0 | Password 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 daysAuthentication 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 objectGitHub OAuth
Step 1: Initiate
GET /auth/github
āā Generate random state
āā Store state in cookie (httpOnly)
āā Redirect to GitHub authorization URLStep 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_HOSTGoogle 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 sessionExternal 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 sessionLogout
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 usedAuthentication 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
Cookie Security
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:3001OAuth (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
| File | Purpose |
|---|---|
packages/api/src/auth/auth.ts | Lucia configuration |
packages/api/src/auth/externalAuthProvider.ts | External provider interface |
packages/api/src/routes/auth.ts | All auth endpoints |
packages/api/src/middleware/auth.ts | Session middleware |
packages/api/src/db/schema/sessions.ts | Session table |
packages/api/src/db/schema/users.ts | User table |
Last updated on