tokyo3-auth

module
v0.0.0-...-6b79937 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: Apache-2.0

README

auth

Release Test Go Reference Go Report Card codecov

A minimal self-hosted Identity Provider (IdP) for internal applications.

Overview

auth acts as an OAuth2/OIDC authorization server with four pillars:

  1. OAuth2/OIDC — Authorization Code + PKCE (S256), ID tokens (RS256), JWKS rotation, UserInfo, token revocation.
  2. GitHub OAuth compatibility — Drop-in replacement for GitHub OAuth so existing integrations work without code changes.
  3. Outbound provisioning — auth pushes user/group lifecycle events to downstream systems via SCIM 2.0 (Vault, Okta-as-target, custom REST) and the AWS IAM SDK. Per-integration auth is bearer token or mTLS (mutually exclusive).
  4. PCI-DSS v4.0.1 policy engine — Pluggable rule engine enforcing password complexity, MFA, lockout, session timeout, and audit logging.

Design Concepts

Token model
  • Access tokens: Opaque random 32-byte hex strings. SHA-256 hashes stored in the database. Never stored in plain text.
  • Refresh tokens: Same opaque model; rotated on each use.
  • ID tokens: RS256 JWT with OIDC Core 1.0 claims (sub, iss, aud, exp, iat, email, name, nonce, auth_time, acr, amr).
  • Code grant: 10-minute authorization codes; single-use; PKCE S256 verified at exchange.
Policy engine

The internal/policy package provides a pluggable Rule interface. PCI-DSS v4.0.1 rules are loaded by default:

Rule Requirement Enforcement
PasswordComplexityRule 8.3.6 ≥12 chars, upper+lower+digit+special
PasswordAgeRule 8.3.9 Password expires after 90 days when MFA is disabled
AccountLockoutRule 8.3.4 Lock after 10 consecutive failures for 30 minutes
MFARequiredRule 8.4.2 Block token issuance without MFA verification
SessionIdleTimeoutRule 8.2.8 15-minute idle timeout for cde scoped sessions
TokenLifetimeRule 8.2.8 Access tokens: 1h max; refresh tokens: 24h max
ClientSecretAgeRule 8.6.1 Machine client secrets must be rotated every 12 months
MFA
  • TOTP: RFC 6238 (SHA1, 6-digit, 30s), ±1 window skew. Secret encrypted with AES-256-GCM DEK+KEK.
  • WebAuthn/FIDO2: go-webauthn/webauthn library. Supports biometric devices and YubiKeys. Session data stored in DB with 5-minute TTL.
Audit

Every authentication event is published synchronously to a NATS JetStream stream (auth_audit on subject auth.audit.events). The publish is fail-closed: if the journal is unreachable (NATS down, ack timeout, etc.) every handler that emits an audit event returns 503 and the originating action is refused. The JetStream stream is the sole authoritative store — there is no projection database, no local DB mirror. FileStorage + DenyDelete + DenyPurge + 13-month retention satisfy PCI-DSS 10.5.

The same stream is read back by:

  • /portal/admin/audit — live tail in the browser via SSE (last 100 + tail forward, with Last-Event-ID reconnect resume).
  • authd audit query — terminal viewer; prints the most recent N events as one JSON object per line, then exits.

Both readers use journal/jetstream.Source from tokyo3-base; same primitive, different framing.

Identity Subject perms
authd-nats-client PUBLISH + CONSUME on auth.audit.events

Operating the journal:

make docker-up                              # postgres + nats + natsbox + auth all wired up
docker compose exec natsbox nats stream info auth_audit
docker compose exec natsbox nats sub 'auth.audit.events'
docker compose exec auth authd audit query --limit 20

Required env vars on authd to enable JetStream publish + read: AUTH_NATS_URL, plus AUTH_NATS_CERT/KEY/CA for mTLS. With AUTH_NATS_URL unset, audit publishing is a no-op (dev only) and the live tail page renders empty.

Outbound provisioning

Authoritative user/group mutations (admin API, portal admin actions, self-registration) fan out to every enabled integration. SCIM targets receive standards-compliant SCIM 2.0 calls; AWS IAM targets translate group display names to IAM groups via a configurable map. The OIDC discovery endpoint enables AssumeRoleWithWebIdentity federation.

Crypto
  • Passwords: bcrypt cost 12.
  • Opaque tokens: 32-byte random hex, SHA-256 hash stored.
  • TOTP secrets + JWT private keys: AES-256-GCM with DEK+KEK envelope. DEK per value, KEK from AUTH_MASTER_KEY.
  • Auth state cookies (login→MFA flow): AES-256-GCM with master key directly (short-lived, no rotation needed).

Requirements

  • Go 1.22+
  • PostgreSQL 15+
  • (Optional) AWS credentials for IAM provisioning

Installation

go install github.com/abagile/tokyo3-auth/cmd/authd@latest

Or build from source:

git clone <repo>
cd auth
go build -o authd ./cmd/authd
Database setup
# Generate a master key (save this securely)
./authd keygen

# Run migrations
AUTH_ADMIN_DATABASE_URL="postgres://admin:pass@localhost/authdb" ./authd migrate
Bootstrap first admin user
AUTH_DATABASE_URL="postgres://app:pass@localhost/authdb" \
  ./authd admin user create \
    --email admin@example.com \
    --password "S3cur3P@ssw0rd!" \
    --name "Admin"

Configuration

Variable Required Default Description
AUTH_ISSUER Yes IdP base URL, e.g. https://id.example.com
AUTH_ADDR No :8443 HTTPS listen address (host:port; empty host = all interfaces)
AUTH_DATABASE_URL Yes PostgreSQL DSN (app role, DML only)
AUTH_ADMIN_DATABASE_URL No AUTH_DATABASE_URL PostgreSQL DSN for migrations (DDL)
AUTH_MASTER_KEY Yes 64-hex-char KEK for TOTP secrets + JWT key encryption
AUTH_ALLOW_REGISTRATION No false Set to true to enable self-registration at /register
AUTH_PROVISION_SYNC_INTERVAL No 1h Period for the background full-sync goroutine that re-pushes every user/group to every enabled integration. Belt-and-suspenders for the event-driven push path; idempotent per tick. Set to 0 (or any negative duration) to disable.
AUTH_AWS_IAM_ENABLED No false Deprecated. Configure AWS IAM via /portal/admin/integrations instead.
AUTH_VAULT_SCIM_ENABLED No false Deprecated. Configure Vault SCIM via /portal/admin/integrations instead. Auto-imported into app_integrations once on first boot when set (always as bearer-mode).
AUTH_VAULT_SCIM_URL No Deprecated; auto-imported on first boot.
AUTH_VAULT_SCIM_TOKEN No Deprecated; auto-imported on first boot.
AUTH_VAULT_SCIM_TIMEOUT No 10s Deprecated; auto-imported on first boot.
AUTH_SCIM_CERT If any mTLS integration Client cert PEM path for mTLS-mode integrations. Hot-reloaded (mtime polled at most once per second across SCIM requests).
AUTH_SCIM_KEY If any mTLS integration Client key PEM. Required iff AUTH_SCIM_CERT is set.
AUTH_SCIM_CA No system roots CA bundle PEM for verifying downstream SCIM servers.
AUTH_WEBAUTHN_ORIGINS No Derived from AUTH_ISSUER Space-separated additional WebAuthn origins
AWS_REGION If IAM enabled AWS region
AWS_ACCESS_KEY_ID If IAM enabled AWS credentials (or use instance role)
AWS_SECRET_ACCESS_KEY If IAM enabled AWS credentials

Running

AUTH_ISSUER=https://id.example.com \
AUTH_DATABASE_URL="postgres://app:pass@localhost/authdb" \
AUTH_MASTER_KEY="$(./authd keygen)" \
./authd serve
CLI commands
Command Description
authd serve Start the HTTP server
authd migrate Apply pending database migrations
authd keygen Generate a random 32-byte master key
authd admin user create Create an admin user
authd admin sync --target=<name|all> Backfill an integration (configured via /portal/admin/integrations) from auth's tables
authd audit query [--limit N] Print the most recent N audit events from the JetStream journal as JSON (default 100)

Endpoint Reference

OAuth2/OIDC
Method Path Description
GET /.well-known/openid-configuration OIDC discovery document
GET /.well-known/jwks.json Public key set (RS256)
GET /authorize Show login form
POST /authorize Submit credentials
POST /authorize/mfa/totp Submit TOTP code during login
GET /authorize/mfa/webauthn WebAuthn MFA page during OAuth2 flow
POST /authorize/mfa/webauthn/begin Begin WebAuthn assertion (SSO MFA step)
POST /authorize/mfa/webauthn/finish Complete WebAuthn assertion, issue code
GET/POST /register Self-registration form (requires AUTH_ALLOW_REGISTRATION=true)
POST /token Token exchange (authorization_code, refresh_token, client_credentials)
GET /userinfo UserInfo endpoint (Bearer token required)
POST /revoke Token revocation (RFC 7009)
GitHub OAuth Compatibility
Method Path Description
GET /login/oauth/authorize GitHub-style authorization redirect
POST /login/oauth/access_token Token exchange (JSON or form-encoded, per Accept header)
GET /api/v3/user User info in GitHub API v3 shape
GET /api/v3/user/emails User emails list

To use an existing GitHub OAuth app with this IdP:

  1. Set the app's Authorization callback URL to https://your-app/github/callback
  2. Set the app's Homepage URL to your application
  3. Point GITHUB_API_URL / equivalent in your app to this IdP's base URL
MFA
Method Path Auth Description
POST /mfa/totp/enroll Bearer Generate TOTP secret + QR URI
POST /mfa/totp/confirm Bearer Verify first code to activate TOTP
POST /mfa/totp/verify Bearer Verify TOTP code
DELETE /mfa/totp Bearer Remove TOTP credential
POST /mfa/webauthn/register/begin Bearer Start WebAuthn credential registration
POST /mfa/webauthn/register/finish?session_id=...&device_name=... Bearer Complete registration
POST /mfa/webauthn/login/begin None Start WebAuthn assertion (body: {"email":"..."})
POST /mfa/webauthn/login/finish?session_id=...&user_id=... None Complete assertion
DELETE /mfa/webauthn/{id} Bearer Remove a WebAuthn credential
Admin API

All admin endpoints require a Bearer token belonging to a session with the admin scope.

Method Path Description
GET/POST /admin/users List / create users
GET/PUT/DELETE /admin/users/{id} Get / update / delete user
GET/POST /admin/clients List / create OAuth2 clients
GET/DELETE /admin/clients/{id} Get / delete client
POST /admin/clients/{id}/rotate-secret Rotate client secret
Portal (Web UI)

The portal is a server-rendered web UI for user self-service and admin management. It uses an encrypted portal_tok HttpOnly cookie (AES-256-GCM) backed by a regular session in the database.

Method Path Description
GET/POST /portal/login Portal sign-in form (email + password)
GET/POST /portal/login/mfa TOTP MFA step during portal sign-in
POST /portal/logout Sign out (deletes session)
GET /portal Account overview
GET /portal/account Profile (display name, password change, TOTP + WebAuthn enrollment)
POST /portal/account/profile Update display name
POST /portal/account/password Change password
POST /portal/mfa/totp/enroll Start TOTP enrollment (stores QR data in cookie)
POST /portal/mfa/totp/confirm Confirm first TOTP code to activate
POST /portal/mfa/totp/delete Remove TOTP credential
POST /portal/mfa/webauthn/register/begin Begin WebAuthn key registration (cookie auth)
POST /portal/mfa/webauthn/register/finish Complete WebAuthn registration
POST /portal/mfa/webauthn/{id}/delete Remove a WebAuthn credential

Admin panel (requires is_admin = true on the user's account):

Method Path Description
GET /portal/admin/users List users
GET/POST /portal/admin/users/new Create user
GET/POST /portal/admin/users/{id}/edit Edit user (name, active, admin role, group memberships)
POST /portal/admin/users/{id}/delete Delete user
POST /portal/admin/users/{id}/reset-password Admin password reset (no current-password check; revokes existing sessions)
POST /portal/admin/users/{id}/clear-mfa Remove the user's TOTP and every WebAuthn credential
GET /portal/admin/groups List groups (roles)
GET/POST /portal/admin/groups/new Create group + assign members
GET/POST /portal/admin/groups/{id}/edit Edit group display name + membership
POST /portal/admin/groups/{id}/delete Delete group (members are not deleted)
GET /portal/admin/clients List OAuth clients
GET/POST /portal/admin/clients/new Create OAuth client
GET/POST /portal/admin/clients/{id}/edit Edit client
POST /portal/admin/clients/{id}/delete Delete client
POST /portal/admin/clients/{id}/rotate-secret Rotate client secret
GET /portal/admin/integrations List app integrations (Vault SCIM, AWS IAM, …)
GET/POST /portal/admin/integrations/new Add a new integration
GET/POST /portal/admin/integrations/{id}/edit Edit integration; rotate token
POST /portal/admin/integrations/{id}/delete Remove integration
POST /portal/admin/integrations/{id}/test Probe SCIM ServiceProviderConfig using the integration's auth mode
GET /portal/admin/audit Live audit-log stream page (last 100 + tail via SSE)
GET /portal/admin/audit/sse Server-Sent-Events backing endpoint for the audit page; honours Last-Event-ID for resume

Role assignment: Two layers of role management are available:

  • Portal admin flag — toggle the "Administrator" checkbox on any user's edit page. Grants access to /portal/admin/*. Effective on next portal login.
  • Application roles via groups — create groups under /portal/admin/groups, assign users, and the same group is fanned out to every enabled integration as a SCIM Group (or AWS IAM group via the integration's group map). Saving the group triggers an immediate downstream sync.
Health
GET /health  →  200 OK

GitHub OAuth Compatibility

Point existing GitHub OAuth integrations at this IdP by setting the authorization and token endpoints:

Authorization URL: https://id.example.com/login/oauth/authorize
Token URL:         https://id.example.com/login/oauth/access_token
API base URL:      https://id.example.com/api/v3

The GitHub-compatible user object maps:

  • id — stable integer derived from user UUID (FNV-64a hash)
  • login — local part of email address
  • name — user display name
  • email — primary email

AWS IAM Integration

Register this IdP as an OIDC provider
aws iam create-open-id-connect-provider \
  --url https://id.example.com \
  --thumbprint-list <cert-thumbprint> \
  --client-id-list sts.amazonaws.com
Configure AssumeRoleWithWebIdentity

Attach a trust policy to your IAM role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/id.example.com"},
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {"StringEquals": {"id.example.com:aud": "your-client-id"}}
  }]
}
IAM provisioning setup

Add an aws_iam integration at /portal/admin/integrations/new to enable automatic IAM user creation when users/groups change in auth (admin API or portal). Credentials come from the AWS SDK default credential chain on the host running authd. SCIM display name → IAM group name mapping is configured per-integration via the Group mapping field.

Outbound provisioning

Auth fans out user and group lifecycle events to downstream systems whenever an authoritative mutation occurs — inbound SCIM, the admin API, self-registration, and the portal admin actions all trigger the same fan-out. Each downstream is a provision.Provisioner (internal/provision/); failures are logged but never block the originating request.

Integrations are persisted in the app_integrations table and managed via /portal/admin/integrations. Saving or deleting an integration hot-reloads the in-memory provisioner registry — changes take effect immediately, no restart required. SCIM bearer tokens are envelope-encrypted with the master KEK (same DEK+KEK pattern as TOTP secrets); only name, provider, and the non-secret JSON config are queryable.

Multiple rows of the same provider type are allowed (e.g. vault-prod + vault-staging). The integration name doubles as the cache key for external_ids, so do not rename a live integration — add a new one and disable/delete the old one instead.

See docs/integration-guide.md for the full bring-up runbook (auth ↔ vault) and the recipe for adding any new SCIM/REST-capable downstream app.

Vault SCIM (auth as IdP for vault)

Vault is the canonical example: auth pushes users and groups to vault's SCIM server so vault membership stays in lockstep with auth's identity. Pick one of the two auth modes below — they're mutually exclusive per integration.

Option A — Bearer token (SaaS-friendly, simplest)

1. Mint a SCIM token in vault:

POST $VAULT/api/v1/scim/tokens
Authorization: Bearer <vault server-admin token>
{ "description": "auth -> vault provisioner" }

The response includes the raw token once — store it.

2. Add a Vault SCIM integration at /portal/admin/integrations/new:

  • Name: vault (or any unique identifier; see note above)
  • Provider: scim
  • Base URL: https://vault.example.com/scim/v2
  • Authentication: Bearer token
  • Token: paste the value from step 1
  • Enabled: yes
Option B — mTLS (no token bootstrap; cert infra required)

1. Configure vault to require client certs on /scim/v2/* and allow-list auth's CA + the SAN/CN that identifies authd. Vault verifies the cert at the TLS handshake; no SCIM token needed.

2. Set the env vars on the authd process:

AUTH_SCIM_CERT=/run/secrets/auth-outbound.crt
AUTH_SCIM_KEY=/run/secrets/auth-outbound.key
AUTH_SCIM_CA=/run/secrets/downstream-ca.crt   # optional; falls back to system roots

The cert file is hot-reloaded — pair this with tbot/cert-manager/SPIFFE for automatic rotation.

3. Add the integration the same way as Option A, but pick mTLS (client cert) for Authentication. The token field disappears; the cert/key/CA come from the env vars above (one shared identity for every mTLS integration).

After either option: register vault as an OAuth2 client (for SSO; see "Vault SSO" below) and click Test on the integration row to confirm authd can reach the SCIM endpoint before sending real traffic.

4. Backfill existing users (one-time, after first deployment or when recovering from drift):

authd admin sync --target=vault     # by integration name
authd admin sync --target=all       # every enabled integration

Iterates auth's users and scim_groups tables and pushes them. Idempotent — vault's SCIM POST returns the existing record on duplicate email.

Legacy env vars: AUTH_VAULT_SCIM_ENABLED/URL/TOKEN/TIMEOUT are deprecated. On the first boot after upgrading, if app_integrations is empty and AUTH_VAULT_SCIM_ENABLED=true, authd auto-imports the env vars into a row named vault and logs a deprecation warning. Subsequent boots ignore the env vars entirely — manage the row via the portal.

Self-heal: external_ids is a best-effort cache. On a 404 from PUT/PATCH/DELETE the client invalidates the cache, re-resolves via filter=externalId eq (vault's Phase 2 filter subset), then either retries or falls through to POST (vault's POST is idempotent on email).

Vault SSO (vault uses auth as OIDC provider)
  1. Register vault as an OAuth2 client:
    POST /admin/clients
    {
      "name": "vault",
      "redirect_uris": ["https://vault.example.com/api/v1/auth/oidc/callback"],
      "scopes": ["openid", "email", "profile", "offline_access"],
      "public": false
    }
    
  2. Configure vault env: VAULT_OIDC_ISSUER, VAULT_OIDC_CLIENT_ID, VAULT_OIDC_CLIENT_SECRET, VAULT_OIDC_REDIRECT_URI, optionally VAULT_OIDC_ENFORCE=true.
  3. Vault CLI: vault login --oidc opens the browser to auth, captures the session token via a loopback listener, and persists it. --manual for SSH/headless cases.

PCI-DSS v4.0.1 Notes

This service satisfies the following PCI-DSS v4.0.1 requirements in the Cardholder Data Environment (CDE):

Requirement Control
8.2.1 Unique user identifiers enforced (email uniqueness at registration)
8.2.8 15-minute idle timeout for CDE-scoped sessions
8.3.4 Account lockout after 10 failed attempts, 30-minute lock duration
8.3.6 Password complexity: ≥12 chars, upper+lower+digit+special
8.3.9 Password age: 90-day maximum when MFA is not enabled
8.4.2 MFA required for all access to CDE (non-public clients)
8.5.1 TOTP codes single-use; WebAuthn sign count validated and incremented
8.6.1 Machine client secrets flagged when older than 12 months
10.2.1 All auth events, failures, and privilege changes journalled to NATS JetStream (auth.audit.events); fail-closed publish refuses the request on journal outage

Evidence for each control is available in the auth_audit JetStream stream (authd audit query, or the /portal/admin/audit live-tail page) and the policy engine rule set in internal/policy/pci.go.

MFA Setup

TOTP enrollment
# 1. Enroll (returns secret + OTP URI for QR code)
curl -X POST https://id.example.com/mfa/totp/enroll \
  -H "Authorization: Bearer <access-token>"

# 2. Confirm (verify first code from authenticator app)
curl -X POST https://id.example.com/mfa/totp/confirm \
  -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'
WebAuthn registration
// 1. Begin registration (get challenge)
const { options, session_id } = await fetch('/mfa/webauthn/register/begin', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + token }
}).then(r => r.json());

// 2. Authenticate with device, then finish
const credential = await navigator.credentials.create({ publicKey: options });
await fetch(`/mfa/webauthn/register/finish?session_id=${session_id}&device_name=MyYubiKey`, {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + token },
  body: JSON.stringify(credential)
});

Development

Local PostgreSQL
docker run -d --name authdb -p 5432:5432 \
  -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=authdb postgres:15
Running locally
export AUTH_ISSUER=http://localhost:8080
export AUTH_DATABASE_URL="postgres://postgres:secret@localhost/authdb?sslmode=disable"
export AUTH_MASTER_KEY="$(go run ./cmd/authd keygen)"
go run ./cmd/authd serve
Code quality
gofmt -s -w .
go test ./...
go vet ./...
staticcheck ./...
find . -type f -name "*.go" -print0 | xargs -0 -n 100 gopls check -severity=hint
govulncheck ./...

Directories

Path Synopsis
cmd
authd command
Command authd is the tokyo3-auth Identity Provider server.
Command authd is the tokyo3-auth Identity Provider server.
internal
api
Package api implements the HTTP server for the IdP.
Package api implements the HTTP server for the IdP.
audit
Package audit provides the audit event types for authd.
Package audit provides the audit event types for authd.
auth
Package auth handles password hashing and opaque token generation.
Package auth handles password hashing and opaque token generation.
jwt
Package jwt handles RS256 key management and ID token signing.
Package jwt handles RS256 key management and ID token signing.
mfa
Package mfa implements TOTP and WebAuthn multi-factor authentication.
Package mfa implements TOTP and WebAuthn multi-factor authentication.
policy
Package policy provides a pluggable rule engine for access control.
Package policy provides a pluggable rule engine for access control.
provision
Package provision defines the outbound user/group provisioning interface that the auth Server fans out to whenever an authoritative user mutation occurs (SCIM ingest, admin API, self-registration, portal admin actions).
Package provision defines the outbound user/group provisioning interface that the auth Server fans out to whenever an authoritative user mutation occurs (SCIM ingest, admin API, self-registration, portal admin actions).
provision/iam
Package iam is the AWS IAM provisioner.
Package iam is the AWS IAM provisioner.
provision/scim
Package scim is the outbound SCIM 2.0 provisioner.
Package scim is the outbound SCIM 2.0 provisioner.
store/postgres
Package postgres implements store.Store using PostgreSQL via pgx/v5.
Package postgres implements store.Store using PostgreSQL via pgx/v5.
store/sqlite
Package sqlite implements store.Store using SQLite via modernc.org/sqlite.
Package sqlite implements store.Store using SQLite via modernc.org/sqlite.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL