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 /clerk surface is a read-only list of firm-assigned Projects whose disclosed is_lawyer_dri row is a lawyer. Clerk has no legal-advice, approval, Git, MCP, or /lawyer authority 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.