Authentication & RBAC
Workloom uses Auth.js v5 (NextAuth) with JWT sessions. Role-based access control enforces what each user can do within an organization.
Authentication Providers
| Provider | Auto-provision | Role sync |
|---|---|---|
| OIDC (Zitadel, Okta, etc.) | Yes | Yes |
| GitHub | No | No |
| No | No |
OIDC is the primary provider for enterprise deployments. GitHub and Google are convenience providers — users must be manually added to organizations.
Role Hierarchy
Roles are ordered by privilege (highest first):
| Role | What they can do |
|---|---|
| owner | Full org control, billing, SSO configuration |
| admin | Manage members, all packages, all workspaces |
| maintainer | Publish packages, approve versions, manage workspace assignments |
| developer | Publish packages, edit workspaces |
| viewer | Read-only access |
A user's effective role is the highest privilege across all resolution sources.
Role Mapping
Most organizations have IdP groups that don't directly match Workloom's five roles. The role mapping system translates external groups to internal roles.
Environment Variable Mapping
Set default mappings via environment variables:
ROLE_MAP_ADMIN=platform-admin,infra-admin,dev-lead-*
ROLE_MAP_DEVELOPER=backend-eng,frontend-eng,platform-eng
ROLE_MAP_VIEWER=contractor-*,intern-*
Patterns support * glob matching — dev-* matches dev-frontend, dev-backend, etc.
Database Mapping (Per-Org)
Organizations can override the defaults via the role_mappings table. Database entries take precedence over environment variables.
This allows different organizations to map the same IdP group to different internal roles.
Provider Plugin System
Role resolution uses a plugin architecture. Each provider implements a simple interface:
interface RoleProvider {
name: string;
enabled(): boolean;
resolveRoles(context: RoleResolutionContext): Promise<Role[]>;
}
Built-in Providers
- OIDC — extracts groups from IdP token, runs through role mapping
- DB — reads existing membership from the database
Merge-All Strategy
When multiple providers are enabled, Workloom collects roles from all of them and picks the highest privilege:
OIDC provider returns: ['maintainer']
DB provider returns: ['admin']
Effective role: 'admin' (highest privilege wins)
Extending with Custom Providers
Add new providers (LDAP, custom APIs) by implementing the RoleProvider interface and registering in the provider registry. Each provider manages its own caching — OIDC uses the token data, DB uses direct queries, LDAP could use Redis TTL.
How Login Works
- User authenticates via OIDC provider
- IdP returns token with group claims
- Groups extracted from the configured claim (
AUTH_OIDC_ROLE_CLAIM) - Provider registry resolves effective role (mapping + merge-all)
- Role persisted to
organization_membershipstable - Raw IdP groups stored in
users.idp_groupsfor audit - JWT cookie contains only identity (id, name, email, image) — no roles
Retrieving Roles
Roles are not stored in the session cookie (prevents bloat with many IdP groups).
Server-side:
import { getUserRoleAction } from '@/server/get-user-role-action';
const { role, orgId } = await getUserRoleAction();
Client-side:
import { useUserRole } from '@/hooks/use-user-role';
const { role, orgId, loading } = useUserRole();
API routes use requireRole() which queries the database directly and throws 403 Forbidden if the user lacks the required role.
Configuration
| Variable | Purpose |
|---|---|
AUTH_OIDC_ROLE_CLAIM | Claim name containing groups in the IdP token |
AUTH_OIDC_DEFAULT_ROLE | Fallback when no mapping matches (default: developer) |
AUTH_OIDC_AUTO_PROVISION_ORG | Org slug for automatic membership creation |
ROLE_MAP_OWNER | Patterns mapping to owner role |
ROLE_MAP_ADMIN | Patterns mapping to admin role |
ROLE_MAP_MAINTAINER | Patterns mapping to maintainer role |
ROLE_MAP_DEVELOPER | Patterns mapping to developer role |
ROLE_MAP_VIEWER | Patterns mapping to viewer role |