Authentication & Authorization
Overview
CaseSolo uses JWT-based authentication with HTTP-only refresh token rotation.
Auth Flow
1. POST /api/auth/login → { accessToken, user } + Set-Cookie: refreshToken
2. GET /api/auth/me → { user } (using accessToken in Authorization header)
3. POST /api/auth/refresh → { accessToken } (using refreshToken cookie)
4. POST /api/auth/logout → Clear refreshToken cookie
Access Tokens
- Algorithm: HS256
- Expiry: 15 minutes
- Payload:
{ userId, firmId, role, email } - Sent via:
Authorization: Bearer <token>header
Refresh Tokens
- Stored as HTTP-only, Secure, SameSite=Strict cookie
- Rotated on each use (old token invalidated)
- Stored hashed in
User.refreshTokencolumn
Route Protection
Server-side (API routes)
import { getCurrentUser } from '@/lib/auth/get-user'
export async function GET(req: Request) {
const user = await getCurrentUser(req)
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 })
// user.firmId, user.role available
}
Middleware
src/proxy.ts intercepts all /api/ and dashboard routes, verifying the JWT and injecting user context.
RBAC (Role-Based Access Control)
Roles (ordered by privilege)
| Role | Level | |------|-------| | SUPER_ADMIN | Platform-wide admin | | ADMIN | Firm admin — settings, billing, members | | ATTORNEY | Full case access, AI tools | | PARALEGAL | Case access, limited settings | | INTAKE_SPECIALIST | Leads, intake, case creation | | CLIENT | Portal access only | | READONLY | View-only dashboard access |
Permission Check
import { requireRole } from '@/lib/auth/rbac'
// In route handler:
requireRole(user, ['ADMIN', 'ATTORNEY']) // throws 403 if unauthorized
MFA, Passkeys, and Security Policy
Main-app MFA is implemented through /api/auth/mfa/setup, /api/auth/mfa/verify, and login-time MFA action tokens.
- TOTP setup writes a pending encrypted secret to
User.metadata.mfaPendingSecretEncrypted, returns the plaintext secret only once for authenticator enrollment, and stores recovery codes assha256:hashes. - Setup verification promotes the pending secret to
User.metadata.mfaSecretEncrypted, clears pending setup fields, and emits immutable audit events throughauditLog. - Login checks encrypted and legacy TOTP metadata. If MFA is enabled,
/api/auth/loginreturns a short-livedmfa_loginaction token instead of creating a session. /api/auth/mfa/verifyvalidates the action token, verifies a TOTP or recovery code, consumes hashed recovery codes, then creates the normal access/refresh session.- WebAuthn/passkey enrollment metadata is captured through
webauthn_beginandwebauthn_register; password-flow WebAuthn assertion verification is intentionally not treated as a completed login method until full cryptographic assertion support is added.
Firm-level security policy is exposed at /api/settings/security for ADMIN and SUPER_ADMIN users:
update_policycontrolsmfaRequired, allowed MFA methods, and recovery-code rotation days.configure_ssostores enterprise SAML/OIDC policy inFirm.settings.securityPolicy.sso; OIDC client secrets are encrypted at rest and never returned by the API.set_sso_enforcementblocks password login for matching firm domains unless the user is on the SSO bypass list.- Login emits audit events for SSO password blocks, MFA enrollment blocks, MFA failures, MFA verification, enrollment, recovery-code rotation, and passkey registration.
Store MFA encryption keys in MFA_SECRET_ENCRYPTION_KEY. If unset, the app falls back to JWT_SECRET for local/dev compatibility.
Client Portal Auth
The client portal uses a separate JWT flow:
- Clients receive a magic link or set a portal password
- Portal tokens are scoped to specific cases
- No access to firm dashboard routes
Security Headers
Applied via next.config.ts:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-origin- CSRF protection via double-submit cookie pattern
Environment Variables
JWT_SECRET= # HS256 signing key (min 32 chars)
JWT_REFRESH_SECRET= # Refresh token signing key
MFA_SECRET_ENCRYPTION_KEY= # 32+ char secret for encrypted TOTP and SSO secret envelopes