README
¶
auth
A minimal self-hosted Identity Provider (IdP) for internal applications.
Overview
auth acts as an OAuth2/OIDC authorization server with four pillars:
- OAuth2/OIDC — Authorization Code + PKCE (S256), ID tokens (RS256), JWKS rotation, UserInfo, token revocation.
- GitHub OAuth compatibility — Drop-in replacement for GitHub OAuth so existing integrations work without code changes.
- 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).
- 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).groups(SCIM group display names) is emitted when the RP requests thegroupsscope — useful for RPs that map roles from a team/group claim (OpenSearch Securitybackend_roles, Vault JWTclaim_mappings, etc.). - 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/webauthnlibrary. 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, withLast-Event-IDreconnect 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.
The same NATS endpoint also receives operational log shipping — every structured log line authd emits is fanned out to subject app_log.authd (alongside stdout). Reuses the same AUTH_NATS_* env vars; ships nothing when AUTH_NATS_URL is unset. The shipper dials with RetryOnFailedConnect(true) so a broker that's down at boot doesn't fail process startup — entries drop on the floor (200-entry discard-on-full buffer) while disconnected and resume on reconnect.
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).
Process robustness
The IdP is on the critical path for every production access surface — AWS console SSO, SSH-proxy sessions, vault, every GitHub-OAuth-compatible RP — so a single panic must not take down the process. Three concentric guards:
- HTTP recover middleware wraps the whole mux. Per-request panics turn into a structured log line (method, path, remote, stack) and a
500 server_errorto the client.http.ErrAbortHandleris re-panicked so net/http's documented "abort without logging" sentinel still works. - Background-goroutine recover wraps every long-lived spawn — periodic provision sync, AWS-federation revocation reaper, device-grant reaper, and per-RP back-channel-logout pushes. Each reaper's per-tick body is wrapped separately so a single bad tick doesn't kill the loop.
- Graceful shutdown on listener failure — if
ListenAndServeTLSreturns an unexpected error (bind conflict, fd exhaustion), the server goroutine signalscancel()and letsrunServe's defers run: auditSink flushes any in-flight JetStream acks, the DB pool closes, the operational-log NATS connection drains. Previously this path calledos.Exit(1)and dropped pending audit events.
Slowloris defense via ReadHeaderTimeout=10s, ReadTimeout=60s, IdleTimeout=120s on the HTTP server. WriteTimeout is intentionally 0 because the /portal/admin/audit/sse stream needs to hold a response open indefinitely; SSE-style handlers that want per-frame deadlines should use http.ResponseController.SetWriteDeadline. TLS handshake errors and broken-pipe writes route through http.Server.ErrorLog into the structured logger, not stderr.
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
Docker images
Tagged releases push two images to GHCR:
| Image | Contents | Typical use |
|---|---|---|
ghcr.io/abagile/tokyo3-auth |
authd server |
Run the IdP itself; docker run + the env vars in Configuration |
ghcr.io/abagile/tokyo3-auth-cli |
auth-aws-creds helper |
CI runners and dev containers that prefer a containerized binary over go install |
The CLI image has no ENTRYPOINT — invoke the binary explicitly so the image can grow more tools later without breaking existing invocations:
docker run --rm ghcr.io/abagile/tokyo3-auth-cli auth-aws-creds login --issuer https://id.example.com --client-id tokyo3-cli
Both images are multi-arch (linux/amd64, linux/arm64). Tags follow semver ({{version}}, {{major}}.{{minor}}, {{major}}, latest) on git tag pushes; commit-SHA tags on every build for traceability.
The SSH-cert counterpart, auth-ssh-creds, lives in the tokyo3-ca repo because its wire shape tracks certd's /api/v1/ssh/sign-user endpoint (SSO is the prerequisite, not the contract it has to stay in sync with). Its CLI image is ghcr.io/abagile/tokyo3-ca-cli; both helpers read the same shared ~/.config/auth-sso/ SSO cache, so one auth-aws-creds login (or auth-ssh-creds login) populates the cache for both.
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_DB_CERT |
If Postgres requires mTLS | — | Client cert PEM path for the runtime DB pool. Hot-reloaded (mtime polled every 30s by the db-cert-reloader goroutine); rotated files are picked up by the next pool dial via GetClientCertificate. SetConnMaxLifetime is 5m so pooled conns recycle naturally within that window. |
AUTH_DB_KEY |
If AUTH_DB_CERT is set |
— | Client key PEM. Hot-reloaded together with AUTH_DB_CERT. |
AUTH_DB_CA |
No | falls back to AUTH_WORKLOAD_CA |
CA bundle PEM for verifying the Postgres server cert. Hot-reloaded every 30s; trust pool snapshot is re-read per handshake via VerifyConnection. |
AUTH_ADMIN_DB_CERT / AUTH_ADMIN_DB_KEY / AUTH_ADMIN_DB_CA |
If migrate uses a separate role with mTLS | falls back to AUTH_DB_* (CA also falls back to AUTH_WORKLOAD_CA) |
mTLS material for the migrate (DDL) connection. Not hot-reloaded — the connection closes after migrations finish, so rotation isn't relevant on this path. |
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_AUDIENCE |
If using AWS federation | — | Single aud value emitted on every JWT minted for AWS console/CLI federation. Register the same string as --client-id-list on each AWS account's IAM OIDC provider. Empty disables the federation flow (AWS tiles on /portal/apps won't launch and /aws/credentials returns 503). Per-role gating happens via aws:RequestTag/<key> conditions, not per-role audiences. |
AUTH_STEP_UP_MFA_TTL |
No | 5m |
Freshness window for the step-up MFA gate that protects AWS roles flagged require_step_up_mfa. A click on such a role re-prompts MFA when the session's last MFA challenge is older than this duration (or never happened). Accepts any time.ParseDuration value (30s, 10m, 1h); invalid or empty values fall back to 5m. |
AUTH_AWSFED_REAP_INTERVAL |
No | 6h |
Period for the AWS federation revocation reaper. Trims aws_revoked_users entries past each role's MaxSessionDuration and re-pushes the trimmed inline policy. No-op when no aws_federation integration is enabled. Set to 0 to disable. |
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_NATS_URL |
If JetStream audit publishing is required | — | NATS server URL for audit publish + read and operational log shipping. Empty disables audit publishing (no-op sink), the live tail page renders empty, and app_log.authd shipping silently no-ops. |
AUTH_NATS_CERT |
If NATS requires mTLS | — | Client cert PEM path for the audit sink + source. Hot-reloaded (mtime polled every 30s by the nats-cert-reloader goroutine; single instance shared by sink and source); rotated files are picked up on the next NATS reconnect via GetClientCertificate. Caveat: the operational-log shipper (applog.AppLoggerWithNATS) still pins this at boot — its hot-reload requires an upstream change in tokyo3-base/applog. |
AUTH_NATS_KEY |
If AUTH_NATS_CERT is set |
— | Client key PEM. Hot-reloaded together with AUTH_NATS_CERT for the audit channels; same boot-pin caveat for the log shipper. |
AUTH_NATS_CA |
No | falls back to AUTH_WORKLOAD_CA |
CA bundle PEM for verifying the NATS server cert. Hot-reloaded every 30s for the audit channels; trust pool snapshot is re-read per handshake via VerifyConnection. Log-shipper caveat applies. |
AUTH_SCIM_MTLS_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_MTLS_KEY |
If any mTLS integration | — | Client key PEM. Required iff AUTH_SCIM_MTLS_CERT is set. |
AUTH_SCIM_MTLS_CA |
No | falls back to AUTH_WORKLOAD_CA, then system roots |
CA bundle PEM for verifying downstream SCIM servers. |
AUTH_WORKLOAD_CA |
No | — | Single workload CA root used as the fallback for every per-channel CA env var (AUTH_DB_CA, AUTH_ADMIN_DB_CA, AUTH_NATS_CA, AUTH_SCIM_MTLS_CA). Set this alone in deployments that issue all internal certs from one CA; set per-channel vars to override individually. |
AUTH_WEBAUTHN_ORIGINS |
No | Derived from AUTH_ISSUER |
Space-separated additional WebAuthn origins |
AWS_REGION |
If an aws_iam or aws_federation integration is enabled |
— | Signing region for the AWS SDK's IAM calls (IAM is global but the SDK requires a region; us-east-1 is canonical). Not read by auth itself — consumed by the AWS SDK Go default credential chain. |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
If neither machine IAM role nor any other credential source is available | — | Standard AWS SDK env vars, not read by auth. The SDK's default credential chain (used by the aws_iam and aws_federation provisioners) picks these up if set. Prefer a machine IAM role — EC2 instance profile, ECS task role, EKS IRSA, or IAM Roles Anywhere — over static keys for production. The federation flow (/portal/aws, /aws/credentials) does NOT need any AWS credentials at all; only the IAM-creates-users provisioner and the federation revocation provisioner do. |
Cert rotation
authd does not hold long-lived TLS material in memory across rotations on the channels that matter. cert-agentd (or any external rotator: cert-manager, SPIFFE, tbot, manual replace) can drop a fresh cert in place and the next handshake on each channel presents it — no SIGHUP, no restart.
| Channel | Mechanism | Picked up on |
|---|---|---|
Incoming HTTPS (AUTH_API_CERT) |
tls.Config.GetCertificate via btls.CertLoader |
Every TLS handshake (per-request) |
SCIM outbound mTLS (AUTH_SCIM_MTLS_CERT) |
tls.Config.GetClientCertificate via btls.CertLoader |
Every outbound SCIM request |
Postgres pool (AUTH_DB_CERT/KEY/CA) |
tls.Config.GetClientCertificate + VerifyConnection via tls/reloader; mtime polled every 30s by db-cert-reloader |
Every new pool dial. SetConnMaxLifetime is 5m so existing pooled conns recycle within that window |
NATS audit sink + source (AUTH_NATS_CERT/KEY/CA) |
Same as DB; one reloader shared between sink and source, polled by nats-cert-reloader |
Every NATS reconnect (the lib auto-reconnects on disconnect) |
Note: the operational-log shipper that fans authd's stdout to app_log.authd (applog.AppLoggerWithNATS, same AUTH_NATS_* env vars) still loads its TLS material once at boot. Closing this gap requires an upstream change in tokyo3-base/applog to accept a *tls.Config instead of raw file paths. Until then, a process restart is needed to roll the shipper onto a rotated cert. The audit sink + source (the channels that gate request-handling via fail-closed publish) are unaffected.
What rotation does not do: it never disturbs an already-established TLS session. TLS authenticates at handshake time only; the X.509 cert is not consulted again on the wire. So in-flight requests, open NATS subscriptions, and active DB transactions survive a rotation untouched — only subsequent dials see the new material.
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 |
GET |
/api/v3/user/orgs |
Synthetic single-org list (login derived from issuer host) — required by Teleport's github connector |
GET |
/api/v3/user/teams |
User's SCIM groups projected as teams under the synthetic org |
To use an existing GitHub OAuth app with this IdP:
- Set the app's Authorization callback URL to
https://your-app/github/callback - Set the app's Homepage URL to your application
- 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 auth_portal HttpOnly cookie (AES-256-GCM) backed by a regular session in the database. Idle timeout is 15 minutes sliding — every authenticated portal hit (or silent SSO via /authorize) extends access_expires_at and re-issues the cookie's MaxAge. No matter how active the user stays, sessions hard-cap at 4 hours from created_at — re-authentication at /authorize mints a new row and restarts the clock.
Session TTLs (constants in internal/api/oauth2.go):
| Window | Value | Slid by |
|---|---|---|
Access token (access_expires_at) |
15 min | portal hits, refresh-token grant |
Refresh token (refresh_expires_at) |
1 hour | refresh-token grant only |
Absolute (from created_at) |
4 hours | never — re-auth required |
bearerAuth enforces now < access_expires_at AND now − created_at < 4h. The OIDC /token response's expires_in is set to the access window's remaining seconds, so RPs that respect the contract align naturally.
Silent SSO: when an OIDC RP redirects a user to /authorize and the browser already has a valid auth_portal cookie, the request short-circuits — the auth server issues an authorization code immediately without showing the login form. Criteria: cookie decryptable, session not expired, user active, MFA verified (when the user has MFA enabled), login_hint (if any) matches, max_age (if any) hasn't elapsed, and policy engine raises no violation. OIDC prompt=login forces re-auth; prompt=none returns error=login_required to the RP when no session is usable. Conversely, every successful /authorize credential check (password, TOTP, WebAuthn) seats the auth_portal cookie as part of issuing the RP code — so a user who signs in to an RP for the first time can hit the auth portal afterwards without re-authenticating, and the next RP redirect rides silent SSO automatically.
User-switch cleanup: when the same browser authenticates as a different user than the one currently in the auth_portal cookie (e.g. user A is still signed in, then user B reaches the login form via prompt=login and authenticates), the prior user's sessions are wiped before the new cookie is seated — DeleteSessionsByUserID(A) plus a user-scoped back-channel logout so every RP also revokes A's local tokens. Without this, A's session row would be orphaned in the DB and A's vault session would silently keep working under B's browser tab.
Portal logout = sign out everywhere: clicking Sign out in auth's own portal deletes every session the user holds at auth (the portal session and every RP-token session) and broadcasts a user-scoped back-channel logout to every client with a backchannel_logout_uri set. The RP then revokes its local session(s) via its sub-based deletion path. This is intentionally a global wipe — the IdP portal is the central control surface; signing out there is the user saying "end my SSO altogether." Per-RP local sign-out lives at the RP (vault's /portal/logout, etc.) and does not propagate back to auth.
| 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/apps |
User-facing application portal — unified tile list of OIDC apps + AWS federation roles |
GET |
/portal/aws |
Redirects to /portal/apps (legacy, kept for one transition cycle) |
POST |
/portal/aws/console |
Assume the requested AWS role and redirect to AWS Console |
GET |
/portal/aws/refresh |
AWS-Issuer URL: silently re-federates when the console session expires |
GET |
/portal/admin/aws |
Admin: AWS accounts, roles, group→role assignments |
POST |
/portal/admin/aws/accounts/new |
Register an AWS account |
POST |
/portal/admin/aws/accounts/{id}/delete |
Delete (cascades to roles + assignments) |
POST |
/portal/admin/aws/roles/new |
Register an assumable role |
POST |
/portal/admin/aws/roles/{id}/delete |
Delete |
POST |
/portal/admin/aws/assignments/new |
Add a group → role assignment |
POST |
/portal/admin/aws/assignments/{id}/delete |
Delete |
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 SCIMGroup(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 addressname— user display nameemail— primary email
Application Portal
/portal/apps is the user-facing tile grid that aggregates every app the signed-in user can launch — OAuth2 clients (Vault, GitHub-compatible apps, internal apps) and AWS federation roles, all in one place. Replaces the AWS-only /portal/aws page; that URL now redirects here.
Making an OAuth client visible
OAuth2 clients are invisible by default — appropriate for machine clients (CI deploy keys, service accounts) and SaaS apps that initiate their own SSO from outside. To surface a client as a launchable tile, edit it at /portal/admin/clients/{id}/edit and fill in the Portal visibility section:
- Show as tile on /portal/apps — enables the tile.
- Launch URL — where the click navigates. Use the RP's initiate-login endpoint (e.g.
https://vault.example.com/ui/vault/auth/oidc), not auth's/authorize— the RP needs to bootstrap its own state/nonce for the code flow. - Brand color / Icon URL — optional cosmetics for the tile card.
- Visible to everyone — shows the tile to every authenticated user. Use for org-wide tools (status page, wiki).
- Visible to SCIM groups — when "visible to everyone" is off, the tile renders only for members of any checked group. Mirrors the AWS-role assignment model.
Visibility resolution
The handler at /portal/apps runs one DB query per source:
ListPortalClientsForUser(user) → OAuth clients where show_in_portal AND
(visible_to_all OR user ∈ any linked group)
ListAWSRolesForUser(user) → AWS roles where user ∈ any group assigned
to the role (aws_role_assignments)
Results are merged into a single tile grid and rendered. OIDC tiles produce plain <a href="LaunchURL"> links; AWS tiles produce a form-POST to /portal/aws/console, which performs the existing AssumeRoleWithWebIdentity + getSigninToken dance.
What this is not
- Not an entitlement grant. Showing a tile makes the launch URL discoverable; the RP still decides what the user can do after they land. OIDC scopes, role mappings, ABAC conditions — all still apply at the target app.
- Not a redirect-through-auth gate. Tiles for OIDC apps are normal hyperlinks the browser navigates to directly. Auth doesn't proxy launches, so the open-redirect concern that would apply to a
/portal/launch?to=…shape doesn't exist here.
AWS OIDC Federation (Console SSO without IAM users)
Federation is the recommended path for AWS Console + CLI access. Auth's portal acts as the single front door: users see role tiles, click a tile, land in the AWS Console with short-lived STS credentials. No IAM users are created, no long-lived AWS keys exist anywhere.
Architecture in one paragraph
Auth publishes /.well-known/openid-configuration and /.well-known/jwks.json; AWS verifies signed id_tokens against the public JWKS. The portal handler at /portal/aws/console exchanges a freshly-minted federation id_token for STS credentials via sts:AssumeRoleWithWebIdentity (unauthenticated — the JWT is the auth), then trades those credentials for a console SigninToken at signin.aws.amazon.com/federation, then 302s the browser into the console. Auth needs no AWS credentials for this flow.
Setup (per AWS account)
-
Set the federation audience in auth's environment. One value per IdP — typically scoped per AWS account so a JWT minted for one account can't be replayed against another:
AUTH_AWS_AUDIENCE=tokyo3-aws-prodRestart authd for the change to take effect. With this unset, the
/portal/awstile page and/aws/credentialsendpoint return clear "federation_disabled" errors instead of silently dropping requests. -
Register the OIDC provider (once per AWS account, scriptable via Terraform). Use the audience value from step 1 as
--client-id-list:aws iam create-open-id-connect-provider \ --url https://id.example.com \ --client-id-list tokyo3-aws-prod # Thumbprint auto-discovered since Nov 2023; pass --thumbprint-list only if pinning. -
Create one role per persona (e.g.
PlatformAdmin,BackendReadOnly). Trust policies share the same audience but discriminate byaws:RequestTag/team— the per-role gate moves from theaudclaim to the session-tag claim:{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::ACCOUNT:oidc-provider/id.example.com" }, "Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"], "Condition": { "StringEquals": { "id.example.com:aud": "tokyo3-aws-prod", "aws:RequestTag/team": "platform" } } }] }aws:RequestTag/teamevaluates the team value being requested in the JWT'shttps://aws.amazon.com/tagsclaim — auth sets it server-side from the SCIM group that actually authorized this assumption (the alphabetically-first group that contains the user and is mapped to the requested role viaaws_role_assignments), so users can't forge a different team. Seedocs/integration-guide.mdPart 3 for the full ABAC pattern catalogue (per-user S3 prefix, team-scoped KMS, etc.). -
Configure auth at
/portal/admin/aws:- Add each AWS account: account ID, alias, OIDC provider ARN.
- Add each role: ARN, slug (URL/CLI-safe identifier — e.g.
platform-prod), display name, optional step-up MFA flag, session TTL. - Add SCIM-group → role assignments mapping group membership to assumable roles.
-
Users log in at
/portal/apps, click an AWS tile, land in the AWS Console. Roles flaggedrequire_step_up_mfainterpose a fresh MFA challenge (/portal/step-up) when the session's last MFA is older thanAUTH_STEP_UP_MFA_TTL(default 5m); the user re-verifies and the assume continues without a second click.
Revocation (optional but recommended)
Without revocation, STS sessions for a deactivated user keep working until natural expiry (≤role TTL, default 1h). To kill them in ≤30s, enable the revocation provisioner:
-
Attach a machine IAM role to whatever compute hosts authd (EC2 instance profile, ECS task role, EKS IRSA, IAM Roles Anywhere on-prem). No static AWS keys. Scoped policy:
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["iam:GetRolePolicy", "iam:PutRolePolicy", "iam:DeleteRolePolicy"], "Resource": ["arn:aws:iam::ACCOUNT:role/<role-name-pattern>"] }] } -
Add an
aws_federationintegration row at/portal/admin/integrations/new. The provisioner picks up credentials transparently via the SDK's default chain — there's no token field to fill. -
On deactivation, the provisioner adds the user's UUID to each federation role's
AuthRevokedUsersinline policy:{ "Effect": "Deny", "Action": ["*"], "Resource": ["*"], "Condition": { "StringEquals": { "aws:PrincipalTag/sub": ["<deactivated-uuid>"] } } }AWS evaluates this on every API call — sessions tagged with the deactivated user's
subgetAccessDeniedon their next request (~30s policy propagation). -
A daily reaper (
AUTH_AWSFED_REAP_INTERVAL, default 6h) trims revocation entries past the role'sMaxSessionDuration— by then every session blocked by the Deny has expired naturally. Keeps the inline policy from growing forever.
CloudTrail correlation
Every console-assumed session lands in CloudTrail with:
userIdentity.sessionContext.webIdFederationData.federatedProvider= your issuer URLuserIdentity.sessionContext.webIdFederationData.attributes.sub= auth user UUIDuserIdentity.principalIdincludes theRoleSessionNameauth set, like<email-local>-<uuid-prefix>-<unix>
Cross-reference sub with auth's audit stream (action aws.console.assumed) for the complete picture.
CLI access via auth-aws-creds
The browser flow handles AWS Console access. For CLI / SDK access, install the bundled helper:
go install github.com/abagile/tokyo3-auth/cmd/auth-aws-creds@latest
This ships from the same repo as authd but is a separate main package — go install pulls only the helper's transitive imports (no DB driver, no AWS SDK, no audit pipeline). Distributing prebuilt binaries works the same way; the standalone binary lives in cmd/auth-aws-creds/.
One-time setup (creates an OAuth public client in auth via /portal/admin/clients/new — public + PKCE, no client secret needed):
auth-aws-creds login --issuer https://id.example.com --client-id tokyo3-cli
# Opens browser, completes OIDC code flow on a loopback port, caches the refresh token.
Register the public client with http://127.0.0.1/callback (or http://localhost/callback) as a redirect URI. Auth applies RFC 8252 §7.3 loopback-aware matching, so the port the CLI picks at runtime (kernel-assigned by default, or --port N if you want to pin) is ignored — no per-port pre-registration needed. Same client_id works for both auth-aws-creds and auth-ssh-creds.
Headless login (no local browser — CI host, jump box, Docker exec session): add --device for the RFC 8628 device authorization grant. The CLI prints a verification URL + short code; complete the browser step on any device (phone, another laptop), and the CLI polls for the result.
auth-aws-creds login --issuer https://id.example.com --client-id tokyo3-cli --device
# Visit this URL to approve sign-in:
# https://id.example.com/device?user_code=ABCD-WXYZ
# Or open https://id.example.com/device and enter ABCD-WXYZ
# Waiting for approval…
The OAuth client must have allow_device_grant set under /portal/admin/clients/{id}/edit for this to work — off by default to keep the device grant surface narrow. After login completes, auth-aws-creds get behaves identically regardless of which login flow you used.
Configure each AWS profile to invoke the helper as its credential source. The role flag matches the slug an admin set under /portal/admin/aws/roles:
# ~/.aws/config
[profile platform-prod]
credential_process = auth-aws-creds get --role platform-prod
region = us-east-1
[profile backend-dev]
credential_process = auth-aws-creds get --role backend-dev
region = us-east-1
The deprecated --audience flag is still accepted as an alias for --role to ease migration from 0.x configurations; it will be removed in a future release.
After that, aws --profile platform-prod s3 ls (or any boto3 / SDK invocation) silently:
- Reads the cached STS credentials if still valid (skew = 60s).
- Otherwise refreshes the OAuth access token via the cached refresh token (no browser).
- Calls auth's
/aws/credentialsendpoint to obtain fresh STS credentials. - Caches the response and emits it as
credential_processJSON on stdout.
A full browser-based re-login is only required when the refresh token expires (matches the OAuth client's refresh-token lifetime auth was configured with). auth-aws-creds logout clears the local cache.
Cache layout under ${XDG_CONFIG_HOME:-~/.config}/auth-sso/ (shared across every auth-*-creds helper, so one login serves all of them):
auth-sso/
├── config.json ← issuer + client_id (non-secret, 0600)
├── tokens.json ← OAuth access + refresh + id tokens (0600)
└── aws-creds/
└── sts/<slug>.json ← STS session credentials per role (0600)
Upgrading from a previous version that used
~/.config/auth-aws-creds/? Just runauth-aws-creds loginonce — the helper writes to the new location and ignores the old cache. You canrm -rf ~/.config/auth-aws-credsat your leisure.
Programmatic endpoint: the helper talks to POST /aws/credentials (bearer-auth, form body role=<slug>; audience=<slug> accepted as a deprecated alias). The response shape matches AWS CLI v2's credential_process JSON so a passthrough helper requires no marshalling.
AWS IAM Users (legacy)
Deprecated for human access. Use AWS OIDC Federation above for console and CLI access. This section covers the older
aws_iamprovisioner that creates an IAM user per workforce member; keep it around for the narrow set of cases below, not as the default human-access path.
The aws_iam provisioner creates one IAM user per active auth user, tags them ManagedBy=tokyo3-auth, syncs SCIM-group → IAM-group memberships, and revokes access keys + group memberships on deactivation. It does not set a console password or generate access keys — those are operator-controlled lifecycle events outside auth's scope.
When to enable
Only when you have a concrete dependency on a stable per-user IAM ARN:
- CodeCommit Git credentials — generated per IAM user via
iam:CreateServiceSpecificCredentialoriam:UploadSSHPublicKey. No federated equivalent exists. (AWS stopped accepting new CodeCommit customers in 2024; existing setups may still depend on this.) - SES SMTP credentials — derived from an IAM user's access keys; SES's SMTP endpoint does not accept STS sessions. The SES API works fine with federated credentials, so this only matters when you're stuck on SMTP.
- Resource policies that hardcode
arn:aws:iam::ACCOUNT:user/<name>asPrincipal. Greenfield environments don't have these; long-running AWS accounts often do. Audit with:grep -rE 'arn:aws:iam::[0-9]+:user/' <terraform-state>. - Third-party SaaS that only documents IAM user setup for its AWS integration. Almost all support role-based access today; check the vendor's "advanced" / "production" docs before assuming IAM users are required.
For everything else — and especially for human workforce access — use federation. Federation gives you short-lived STS credentials, no per-user IAM rows, no long-lived keys, no ~/.aws/credentials to leak.
Setup
Add an aws_iam integration at /portal/admin/integrations/new (the admin form shows a deprecation banner with the same guidance as this section). The integration's Group mapping field maps SCIM group display names to IAM group names — when a user is added to a SCIM group, they're added to the corresponding IAM group automatically.
Credentials come from the AWS SDK default credential chain on the host running authd. Use a machine IAM role (EC2 instance profile, ECS task role, EKS IRSA, IAM Roles Anywhere); avoid static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in production. The role needs: iam:CreateUser, iam:TagUser, iam:DeleteUser, iam:CreateGroup, iam:AddUserToGroup, iam:RemoveUserFromGroup, iam:ListGroupsForUser, iam:ListAccessKeys, iam:DeleteAccessKey.
What the provisioner does NOT do
To set realistic expectations:
- No console password is set —
iam:CreateLoginProfileis never called. Useaws iam create-login-profileout-of-band after provisioning if console access is desired. - No access keys are minted —
iam:CreateAccessKeyis never called. Operators mint keys for the specific service integrations that need them. - No MFA enrollment — IAM MFA is independent of auth's MFA.
- Username collision risk: usernames are derived as the email's local part (
alice@example.com→alice). Two users with the same local part collide; auth logs the conflict but doesn't surface it elsewhere.
These omissions are deliberate — credentials are policy decisions, not provisioning decisions — but they mean the IAM provisioner is not a "create a fully-usable IAM user" feature. Pair it with whatever credential-issuance tooling your org uses.
SSH access via auth-ssh-creds
auth-ssh-creds exchanges an SSO ID token (issued by auth) for a
short-lived SSH user certificate via certd's /api/v1/ssh/sign-user
endpoint, then writes the cert + private key next to each other so
ssh -i picks them up directly.
The binary lives in the tokyo3-ca repo, not here, because the wire shape it tracks is certd's sign-user contract — when that contract evolves (TTL caps, principals validation, key types, key_id derivation), the helper that POSTs to it should land in the same review. SSO via auth is the prerequisite, not the API surface the helper has to stay in sync with.
go install github.com/abagile/tokyo3-ca/cmd/auth-ssh-creds@latest
The helper shares the SSO cache at ~/.config/auth-sso/ with
auth-aws-creds, so a single auth-aws-creds login (or
auth-ssh-creds login) populates the tokens for both. Re-use the same
tokyo3-cli (or whatever you named it) public client registration —
no separate auth-side setup needed beyond the federated audience your
certd already validates.
Full usage docs (flag reference, cache layout, ssh_config snippet
emission via --proxy-jump, Docker image) live in the
tokyo3-ca README.
How the bearer token reaches certd: auth-ssh-creds sends
Authorization: Bearer <id_token> to certd's /api/v1/ssh/sign-user
endpoint. certd validates the token against auth's JWKS
(<issuer>/.well-known/jwks.json) — no certd → auth callback needed;
the client carries the token end-to-end. JWKS caching means a
transient auth restart doesn't break cert issuance.
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.mdfor 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_MTLS_CERT=/run/secrets/auth-outbound.crt
AUTH_SCIM_MTLS_KEY=/run/secrets/auth-outbound.key
AUTH_SCIM_MTLS_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)
- 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 } - Configure vault env:
VAULT_OIDC_ISSUER,VAULT_OIDC_CLIENT_ID,VAULT_OIDC_CLIENT_SECRET,VAULT_OIDC_REDIRECT_URI, optionallyVAULT_OIDC_ENFORCE=true. - Vault CLI:
vault login --oidcopens the browser to auth, captures the session token via a loopback listener, and persists it.--manualfor 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
|
|
|
auth-aws-creds
command
Command auth-aws-creds is a credential helper that bridges auth's OIDC federation to the AWS CLI / SDKs via the credential_process protocol.
|
Command auth-aws-creds is a credential helper that bridges auth's OIDC federation to the AWS CLI / SDKs via the credential_process protocol. |
|
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. |
|
jwt
Package jwt is the server-side wrapper around github.com/abagile/tokyo3-base/auth/jwt: it adds the store-backed key-management bits the IdP needs but a generic JWT signer shouldn't know about.
|
Package jwt is the server-side wrapper around github.com/abagile/tokyo3-base/auth/jwt: it adds the store-backed key-management bits the IdP needs but a generic JWT signer shouldn't know about. |
|
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/awsfed
Package awsfed is the AWS OIDC federation revocation provisioner.
|
Package awsfed is the AWS OIDC federation revocation provisioner. |
|
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. |