Sign-in: bring an OIDC provider; passwords live there, not here
Passwords matter — Neon Law Navigator's own database never stores a password. There is no password column and no hashing crate; the credential lives with an OIDC provider you bring, never in our database. Identity is delegated via the standard Authorization Code + PKCE flow. Four env vars wire it:
OAUTH_ISSUER_URL=... # the provider's issuer; discovery hangs off /.well-known/openid-configuration
OAUTH_CLIENT_ID=...
OAUTH_CLIENT_SECRET=...
OAUTH_REDIRECT_URI=https://www.your-domain.example/auth/callback
Navigator speaks that flow against the provider (/auth/login → /auth/callback) and discovers every endpoint from
<issuer>/.well-known/openid-configuration, so no provider URL is hard-coded. Worked examples for Rauthy, Google,
Auth0, and Okta live in .env.example. The provider asserts only who you are (a stable sub and an email); your
persons row owns what you can do (the single role), so granting or revoking access is one SQL statement — see
docs/oidc.md for the full model.
For this workshop, that row should make you the owner. Navigator has five stored authorization roles, plus the
anonymous public visitor:
owner— the system owner, highest in authority, inheriting every Admin and Lawyer capability. Only Owner may govern another Owner identity.admin— a licensed lawyer with installation-wide administration authority. Admin cannot manage an Owner.lawyer— a licensed lawyer working assigned matters and supervising any Clerk capability the application grants.clerk— a supervised non-lawyer worker. Clerk's/clerksurface is a read-only list of firm-assigned Projects whose disclosedis_lawyer_drirow is a lawyer. Clerk has no legal-advice, approval, Git, MCP, or/lawyerauthority by inheritance; upload and preparation work still need their own narrow, supervised routes.client— represented people using the portal for their own matters.
Owner is the deployer's role because this class touches billing, secrets, OIDC, release state, and every Project. The
role still lives in the database, not the IdP token; your OIDC provider proves identity, then Navigator reads the
persons.role value to decide the tier.
One environment variable answers which person is the protected bootstrap Owner:
NAVIGATOR_BOOTSTRAP_OWNER_EMAIL=owner@example.com
Read NAVIGATOR_BOOTSTRAP_OWNER_EMAIL from the environment or secret source that supplies your deployment to determine
the protected identity. Do not copy the deployed value into Git. An unset, empty, or whitespace-only value disables the
bootstrap carve-out, so every person must already have a row before signing in. On a fresh installation, the first
successful OIDC login with the configured email JIT-creates its persons row as owner; later sign-ins restore that
role if the database has drifted. Its entire Person record is immutable in Navigator so an administrator cannot rename,
demote, or delete the installation's recovery identity by accident.
After signing in as Owner or Admin, open /admin/people to manage the directory and change another Person's system-wide
role among owner, admin, lawyer, clerk, and client. Owner appears first because it owns the deployed system.
Only an Owner can assign or modify Owner. Admin can manage Admin and every lower tier. The bootstrap-Owner row remains
read-only, and the command boundary rejects a hand-written update or delete as well. The Lawyer workbench does not grant
role-management power. These values are persons.role; Project assignments such as attorney, paralegal, client, and
co-counsel are Participation records and do not create another authorization tier.
We recommend Google. Verifying that a person is who they claim is real work — risk signals, step-up challenges, and
hardware-key (passkey / security-key) verification — and Google invests far more in it than we could. A Google-backed
sign-in is stronger than any password we would host ourselves. That is exactly the path NeonLaw's own prod takes: Sign
in with Google, wired by the four env vars above with OAUTH_ISSUER_URL=https://accounts.google.com — the same
standard redirect flow, no password anywhere. (Google-hosted email/password is a separate, opt-in door: set
NAVIGATOR_IDENTITY_PLATFORM_API_KEY and /auth/login also renders a password form whose POST /auth/password checks
the credential against GCP Identity Platform — Google still owns the password and lockout, never us. Reset and email
confirmation need the admin door too: set NAVIGATOR_GCP_PROJECT_ID, run web where the GCE metadata server can mint a
service-account bearer token, and grant that service account roles/identitytoolkit.admin. Leave the key unset and
sign-in stays a pure Google redirect. Config lives in .env.example, not the redirect swap above.)
No-Google path. "Sign in with Google" cannot be the only front door of a public legal-services portal — the person a clinic serves may have no Google account. Any standards-compliant OIDC provider — discovery, Authorization Code + PKCE, RS256-signed ID tokens with RSA keys in JWKS — that hosts its own email/password login works with the env-swap above and zero Neon Law Navigator code changes. Rauthy — the same open-source IdP the local KIND loop already runs — serves email/password, self-registration, reset, and verification from its own pages, in your cluster with no per-user fee. Auth0 / Okta are hosted SaaS equivalents: same four env vars, same redirect flow.