Unphish v2 Docs

Auth and RBAC

Identity, the role and capability model, route guards, and tenant isolation.

Two layers: identity vs. authorization

Unphish v2 separates identity (who you are) from authorization (what you can do):

  • Identity is owned by Authentik. Authentik holds passwords, MFA secrets, recovery flows, SSO federation.
  • Authorization is owned by the Unphish application. The app database holds memberships, roles, capabilities, and scope restrictions.

This separation matters: rotating a password, resetting MFA, or federating to a customer SSO provider is an Authentik operation. Granting a user access to a client, promoting them to admin, or revoking their analyst role is a Unphish-app operation.

Identity (Authentik)

Authentik is the OIDC provider. Configuration:

  • Separate Authentik application / provider pair per environment (local, staging, production).
  • Redirect URI: ${NEXT_PUBLIC_APP_URL}/api/auth/callback/authentik.
  • Logout / end-session return URL: ${NEXT_PUBLIC_APP_URL}/signin.
  • Issuer: no trailing slash; includes the app slug, e.g., https://auth.example.com/application/o/unphish.
  • Required scopes: openid, email, profile. Authentik group claims may be captured as identity metadata, but app roles come from app-owned tables.
  • Branded login, recovery, MFA, and email templates.
  • SMTP, optional MFA policy, admin recovery, backups, monitoring, and TLS configured on the Authentik host.

Authentik runs outside Vercel with its own Postgres / Redis / TLS / SMTP / backups / monitoring / MFA policy. This is non-negotiable; Authentik is not a Vercel function.

App-owned authorization

App tables hold all authorization state:

  • public.users — Authentik subject (sub) plus profile (email, name, image, last login, disabled state, system_role).
  • public.user_identities — provider mappings; idempotent upsert by email on login.
  • public.user_sessions — product session created after OIDC callback. Signed cookie; HttpOnly; Secure in production.
  • public.organizations and public.clients — tenant containers.
  • public.user_organizations — membership in an organization with a role.
  • public.user_client_access — fine-grained client access (optional; if missing, user can see all org clients).
  • public.user_brand_access — brand-level grants (e.g., a reviewer can approve only Brand A and Brand B).
  • public.team_members — mirror of org/role for dashboard team views.
  • public.team_invitations — app-owned invite tokens (14-day expiry, sent via Postmark).
  • public.audit_log (or activity_log) — auth-sensitive events.

What app tables explicitly do not hold:

  • Password hashes.
  • MFA secrets.
  • OAuth client secrets.
  • Recovery tokens.

There is no "credential account" table in the app database. A SQL injection on the app database does not yield credentials.

Roles

The platform uses normalized, lowercase role identifiers in storage and APIs:

RoleScopeCapabilities
ownerOrganization or partnerFull tenant control: billing, configuration, team, destructive actions.
adminOrganization, partner, or clientManage users, clients, policies, integrations, reports, and most operational settings.
analystPartner or client operationsReview, triage, enrich, enforce, verify, escalate, and close cases.
viewerPartner or clientRead dashboards, cases, reports, intelligence, and audit records allowed by scope.
client_approverClientApprove or reject enforcement requests; provide requested information.
apiClient or integrationProgrammatic access limited by API key scopes.

Legacy v1 labels (Admin, Analyst, Viewer, Senior Analyst) are mapped to canonical roles before persistence. Senior Analyst maps to analyst unless we deliberately introduce a future role.

System role: separate from organization role

In addition to organization role, every user has a system_role:

system_roleMeaning
userCustomer or partner user. No platform-level access.
staffUnphish staff. Hub access for read-only operational visibility.
adminPlatform operator (Engram or Unphish admin). Hub write access, secrets, audit, support preview.

system_role is distinct from organization role. A user can be admin of their organization and user at the system level (typical for a partner or client admin). Conversely, a user can be analyst in the platform organization and admin at the system level (an Engram engineer). The two values combine in route guards.

Internal-only auto-promotion

Users invited with internal email domains (@unphish.com, @engram.org, @engram.com by default) and an organization role of admin or owner are automatically promoted to system_role: admin. Partner and client admins do not auto-promote.

Capabilities

For finer-grained authorization than role-based, the platform uses named capabilities:

CapabilityWhat it gates
hub.viewHub access.
client.onboardCreate / edit clients.
impersonation.startSupport preview.
provider_secrets.manageHub secrets writes.
enforcement.submit_liveProduction enforcement (gated separately from staging).
workbench.viewWorkbench access.
workbench.mutate_stagingStaging sandbox mutations.
delivery_board.manageCustomer-visible Kanban / requests.
status.manageExternal status page.
architecture.manageArchitecture diagram.

Capabilities are derived from system_role plus organization role plus explicit grants. The combination keeps route guards readable: the dashboard checks analyst or higher in the active organization; Hub checks system_role >= staff; live enforcement checks enforcement.submit_live.

Route guards

Each route family enforces guards consistently at the layout level:

RouteGuard
/hub/*system_role >= staff; sections may have additional capability requirements.
/admin/*system_role >= staff plus admin capabilities.
/dashboard/*analyst or higher in the active organization.
/partner/*Partner membership (organization is a partner; user has role in that organization).
/client/*Client membership; further filtered by brand access where configured.
/workbench/*system_role >= staff plus workbench.view.
/api/*Authenticated session or valid API key with appropriate scope.

Unauthenticated requests redirect to /signin. Authenticated-but-unauthorized requests render an explicit 403 with no data leak; they do not redirect to /dashboard and silently scrub.

Tenant isolation

Tenant isolation is structural:

  • Every tenant-scoped record carries organization_id.
  • Every client-scoped record additionally carries client_id.
  • Query filters always include the active organization (and client where applicable) at the data-access layer.
  • API keys are scoped to their issuing client/organization; their requests are filtered the same way.
  • Cross-tenant access is impossible by construction. There is no global "see all cases" query path.

Staff/global access is the only exception, and it is explicit:

  • An Engram or Unphish admin can use support preview (impersonation) to act inside a target tenant's scope.
  • Support preview is gated by the impersonation.start capability.
  • Preview sessions show a persistent banner with the real actor, target user, and time remaining.
  • Every action in a preview is audited under both the real actor and the target.
  • Preview sessions have a max 60-minute expiry and require an explicit reason.

There is no "act as customer" path that does not produce an audit trail.

API keys

Programmatic access uses API keys:

  • Issued per api_application (per client or organization).
  • Scoped to specific resource families (cases, evidence, enforcements, watchlist, reports).
  • Rate-limited.
  • Revocable; revocation is immediate.
  • Last-used timestamp tracked.
  • Created via the appropriate admin surface; the secret is shown once and never retrievable again. If lost, issue a new one.

API keys do not have personal-user identity; their requests are tagged with the application and scope they belong to. Audit entries record the application, not a synthetic user.

SSO federation

Clients can configure SSO via verified email domains. Authentik handles the federation; the app records:

  • The verified domain.
  • The provider (Auth0, Okta, Microsoft Entra ID, Google, custom OIDC, custom SAML).
  • The verification state (pending, verified, disabled).
  • The activation timestamp.
  • Audit history.

Domain verification typically requires a DNS record (TXT) before SSO is enabled. Unverified domains cannot route logins.

MFA

MFA is enforced at the Authentik level. Organizations and clients can require MFA via Authentik policies; the application surfaces the requirement but does not enforce it directly. Users without MFA enrollment cannot complete sign-in when their organization requires it.

v1 TOTP secrets are not migrated. Users re-enroll MFA in Authentik after migration.

Session model

  • Sign-in completes at the Authentik OIDC callback (/api/auth/callback/authentik).
  • The app creates a user_sessions row and issues a signed cookie.
  • Cookies are HttpOnly and Secure (in production), with a configured expiry.
  • Sign-out terminates the session row, clears the cookie, and routes the user to Authentik's end-session URL, which returns them to /signin.
  • Revoked memberships, sessions, and API keys cannot access protected routes — checked at every request.

What deliberately does not exist

  • Local password authentication. No "username + password against the app database" path. All authentication routes through Authentik.
  • A fallback auth path. If Authentik is unavailable, sign-in is unavailable. Readiness goes red and operators are paged.
  • A role slider for previewing other personas. v1 had this on Hub. v2 has support preview, which is capability-gated, banner-visible, time-bounded, and audited.

On this page