Authentication & Authorization
Leadmetrics uses a unified JWT auth system across all three web portals (Dashboard, DM Portal, Manage) and the mobile app. The Fastify API is the single identity issuer. Each portal validates tokens independently using shared middleware from @leadmetrics/middleware.
Related: API Auth Endpoints | Multi-tenancy
How It Works
┌─────────────────────────────────────────────────────────┐
│ Fastify API /auth/v1 │
│ │
│ POST /auth/v1/login → access + refresh tokens │
│ POST /auth/v1/refresh → new access token │
│ POST /auth/v1/forgot-password → send reset email │
│ POST /auth/v1/reset-password → apply new password │
└─────────────────────────────────────────────────────────┘
▲ ▲ ▲
Dashboard :3000 DM Portal :3002 Manage :3001
(Next.js) (Next.js) (Next.js)
Each portal runs its own Next.js middleware that:
1. Reads the portal's httpOnly access-token cookie
2. Verifies the JWT with JWT_SECRET (HS256)
3. On 401: attempts a silent refresh via /auth/v1/refresh
4. On refresh failure: redirects to /loginToken Model
| Token | Lifetime | Storage | Purpose |
|---|---|---|---|
| Access token | 15 minutes | httpOnly cookie per portal | Authorize API requests; verified locally (no DB hit) |
| Refresh token | 7 days | httpOnly cookie per portal | Obtain a new access token via /auth/v1/refresh |
Signing: HS256 (HMAC-SHA256) with JWT_SECRET. All portals and the API share the same secret.
Access token payload:
{
sub: string; // user ID (ULID)
role: string; // 'admin' | 'member' | 'reviewer' | 'super_admin'
appAccess: string[]; // apps this user is allowed to access
tenantId?: string; // absent for super_admin
name?: string;
email?: string;
iat: number;
exp: number; // iat + 15 min
}Cookie Names
Each portal uses its own cookie names so sessions are completely isolated:
| Portal | Access token cookie | Refresh token cookie |
|---|---|---|
| Dashboard | dashboard_access_token | dashboard_refresh_token |
| DM Portal | dm_access_token | dm_refresh_token |
| Manage | manage_access_token | manage_refresh_token |
Cookie names are exported as PORTAL_COOKIES from @leadmetrics/middleware.
Shared Middleware Package
@leadmetrics/middleware provides the shared auth primitives:
| Export | Purpose |
|---|---|
createJwtAuthMiddleware(options) | Factory that returns a Next.js middleware function with JWT verify + silent refresh |
verifyJwt(token) | Verify a JWT and return the payload (used in server components / server actions) |
setPortalAuthCookies(cookieSetter, tokens, cookieNames) | Set access + refresh cookies after login |
clearPortalAuthCookies(cookieDeleter, cookieNames) | Delete both cookies on logout |
PORTAL_COOKIES | Constant with cookie name pairs for each portal |
Each portal’s middleware.ts is a thin wrapper:
// apps/dashboard/src/middleware.ts
import { createJwtAuthMiddleware } from "@leadmetrics/middleware";
export const middleware = createJwtAuthMiddleware({
accessTokenCookie: "dashboard_access_token",
refreshTokenCookie: "dashboard_refresh_token",
apiUrl: process.env.API_URL ?? "http://localhost:3003",
publicPaths: ["/login", "/signup", "/forgot-password", "/reset-password", "/api/auth"],
});
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };Per-Portal Auth Helpers
Each portal has:
| File | Purpose |
|---|---|
src/middleware.ts | Next.js middleware (JWT verify + silent refresh) |
src/lib/auth-cookies.ts | setAuthCookies() / clearAuthCookies() — thin wrappers over shared helpers |
src/lib/server-auth.ts | requireSession() / requireAuth() — server component / action guards |
src/app/actions/auth.ts | loginAction, logoutAction, forgotPasswordAction, resetPasswordAction |
Server Auth Guards
Use these at the top of every server component, layout, and server action that requires authentication.
| Portal | Function | Redirects on failure |
|---|---|---|
| Dashboard | requireSession() from @/lib/server-auth | /login |
| DM Portal | requireAuth() from @/lib/server-auth | /login |
| Manage | requireSuperAdmin() from @/lib/server-auth | /login |
For API routes that must return JSON (not redirect), use getSession() instead — it returns null on failure.
Roles & Permissions
| Role | Portals | Scope |
|---|---|---|
admin | Dashboard, Mobile | Single tenant — full access |
member | Dashboard, Mobile | Single tenant — read + limited write |
reviewer | DM Portal | Assigned tenants — approval + editorial |
super_admin | Manage | All tenants — platform-wide |
Roles are embedded in the JWT. Role checks are enforced per API route in the Fastify API.
Login Flow
1. User submits email + password on /login
2. loginAction() calls POST /auth/v1/login { email, password, app: "dashboard" }
3. API verifies credentials, checks app access, returns { accessToken, refreshToken, user }
4. setAuthCookies() sets both tokens as httpOnly cookies
5. User is redirected to /overviewSilent Refresh
1. Request arrives; middleware reads access token cookie
2. JWT verify fails (expired)
3. Middleware reads refresh token cookie
4. POST /auth/v1/refresh { refreshToken }
5. API returns new accessToken
6. Middleware sets new access token cookie, continues with request
7. If refresh fails → redirect to /loginPassword Reset Flow
1. User submits email on /forgot-password
2. forgotPasswordAction() calls POST /auth/v1/forgot-password { email }
3. API sends reset email with a one-time token (1-hour expiry)
4. User clicks link → /reset-password?token=<token>
5. resetPasswordAction() calls POST /auth/v1/reset-password { token, newPassword }
6. API validates token, updates password, invalidates tokenFeatures Not Yet Implemented
The following are planned but not built:
- Social login (Google, LinkedIn, Microsoft, Apple) — see Social Providers
- OAuth 2.0 / OIDC server (external client integrations) — see OAuth Server
- Two-factor authentication (TOTP)
- Biometric auth (mobile)
- Email verification on registration