Auth0 Infrastructure
Auth0 handles authentication and organisation-based SSO for the FSA Platform.
Key Files
| Type | Location |
|---|---|
| Auth0 Client | packages/core/src/infrastructure/auth0/client.ts |
| Auth Utilities | apps/web/src/lib/auth.ts |
| Repositories | packages/core/src/infrastructure/repositories/auth0-*-repository.ts |
Authentication Flow
Authentication is handled by Next.js middleware (proxy.ts). Unauthenticated users are redirected automatically.
JWT Claims
interface Auth0User {
sub: string; // Auth0 user ID (auth0|abc123)
email: string;
email_verified: boolean;
name?: string;
org_id?: string; // Auth0 organisation ID
}Organisation-Based SSO
Each Account links to an Auth0 Organisation for SSO:
Linking Account to Auth0 Org
const useCase = new CreateAndLinkAuth0Organisation();
await useCase.execute({ accountId, orgName, auth0UserId });Management API
// packages/core/src/infrastructure/auth0/client.ts
import { ManagementClient } from 'auth0';
const management = new ManagementClient({
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_MGMT_CLIENT_ID!,
clientSecret: process.env.AUTH0_MGMT_CLIENT_SECRET!,
});Common Operations
// Create organisation
await management.organizations.create({ name: 'slug', display_name: 'Name' });
// Invite member
await management.organizations.createInvitation(
{ id: orgId },
{ inviter: { name }, invitee: { email }, client_id, send_invitation_email: true }
);
// Get members
await management.organizations.getMembers({ id: orgId });
// Check user exists
const users = await management.users.getByEmail(email);Environment Variables
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=xxx
AUTH0_CLIENT_SECRET=xxx
AUTH0_MGMT_CLIENT_ID=xxx
AUTH0_MGMT_CLIENT_SECRET=xxxServer Action Authorization
Server actions must verify the caller has a platform account before executing account-scoped operations.
Auth Guards
| Guard | Location | Use |
|---|---|---|
requireAuth() | lib/auth.ts | Verifies Auth0 session, returns userId |
requireAccount() | lib/auth.ts | Verifies session + selected account (Redis), returns { userId, accountId } |
requireAdmin() | lib/auth.ts | Verifies FSAdmin role, redirects if not |
getCurrentUserId() | lib/auth.ts | Optional auth — returns userId or null without redirecting |
Which guard to use
| Context | Guard |
|---|---|
| Account-scoped server actions, pages, and layouts | requireAccount() |
Admin server actions (under /admin) | requireAuth() (proxy enforces FSAdmin role) |
Respondent-only actions (/engage) | requireAuth() (no account needed) |
| Optional auth (e.g., admin pages) | getCurrentUserId() |
Example
'use server';
import { requireAccount } from '@/lib/auth';
export const myAction = createServerAction()
.input(z.object({ name: z.string() }))
.handler(async ({ input }) => {
const { userId, accountId } = await requireAccount();
// userId and accountId are guaranteed to exist
});Organisation Security
Auth0 organisations are created with assign_membership_on_login: false and is_signup_enabled: false (defaults). This prevents users from self-joining organisations by crafting direct Auth0 /authorize URLs. Membership is managed exclusively through the invitation flow.
Signup is enabled at the tenant database connection level to allow invitees to create accounts. Users who sign up without an invitation have no org membership and are blocked by the requireAccount() guard.
Best Practices
- Use
requireAccount()in server actions - fast Redis check, blocks users without an account - Rely on middleware for admin routes -
proxy.tschecks FSAdmin role for/adminpaths - Sync with local data - keep User entity updated with Auth0
- Never trust client-provided user IDs - use
userIdfromrequireAccount() - Secure Management API - keep credentials safe
Related Files
| Type | Location |
|---|---|
| Client | packages/core/src/infrastructure/auth0/client.ts |
| Proxy | apps/web/src/proxy.ts |
| Auth Utilities | apps/web/src/lib/auth.ts |
| Repositories | packages/core/src/infrastructure/repositories/auth0-*-repository.ts |