apistockdocs
v0.4 GitHub apistock.dev
Technical/Configuration and security

Secrets and keys

Every secret, key, token and code in a Full app: what it is, why it exists, who creates it, how it's stored, whether it's safe to expose, how to rotate it, and what an attacker could do with it. Verified against modules/auth, modules/auth/social, config and examples/full-single/internal/app.

For step-by-step instructions for the values you provide, see the beginner guides: Encryption key, Google, Apple, Email, and Every key and credential.

Summary#

Secret Created by Where it lives Exposed if leaked?
AUTH_ENCRYPTION_KEYS You (openssl); aps dev in development Environment TOTP secrets, if the database leaks too
DATABASE_URL credentials Your database provider; compose.yaml locally Environment Everything
GOOGLE_CLIENT_SECRET Google Cloud Console Environment Impersonating your app at Google's token endpoint
Apple .p8 key Apple Developer File or environment Impersonating your app at Apple, revoking users' Apple tokens
RESEND_API_KEY / SMTP_PASSWORD Your email provider Environment Sending email as your domain
Passwords Users auth_users, argon2id hash Offline cracking of weak passwords
Session tokens The app Client; SHA-256 in auth_sessions Acting as that user until the session ends
Email codes The app Email; SHA-256 in auth_codes Verifying an address or resetting a password, briefly
TOTP secrets The app Authenticator app; encrypted in auth_totp Generating second-factor codes
Recovery codes The app User; SHA-256 in auth_recovery_codes One second-factor sign-in each
Passkeys The user's device Device; public key in auth_passkeys Nothing (public keys only)
OAuth state, PKCE and nonces The app Database and a cookie, minutes Nothing after use
Apple client secret The app, per request Memory, 5 minutes Short-lived impersonation at Apple
Seed administrator credentials cmd/seed Printed once The local administrator

Not used, so never create them: a JWT signing secret (sessions are opaque tokens), a session or cookie secret (cookies hold the opaque token), a CSRF secret (cross-site requests are blocked with Sec-Fetch-Site and Origin through http.CrossOriginProtection), a passkey server key, a webhook secret (Apple's notifications are verified with Apple's public keys), and an Apple client secret (generated per request).

Values you provide#

AUTH_ENCRYPTION_KEYS#

What One or more AES-256-GCM keys, as id:base64 entries separated by commas: k2:…,k1:…
Why TOTP secrets must be readable by the server to check codes, so they can't be hashed. Encrypting them means a database dump alone doesn't let anyone generate users' second-factor codes
Format Each id is unique; each key decodes to exactly 32 bytes (key "k1" must be 32 bytes in base64 otherwise). Parsed by authlib.ParseKeyring
Generate echo "k1:$(openssl rand -base64 32)"; in PowerShell 7: "k1:" + [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)). aps dev writes one to .env when empty (cli/internal/cli/dev_keys.go)
Env AUTH_ENCRYPTION_KEYS or AUTH_ENCRYPTION_KEYS_FILE. Required in production and by cmd/seed. Empty in development: 503 mfa_unavailable
Public-safe? No
How it's used The first key encrypts; the key id is stored in a column next to each ciphertext, and any listed key decrypts. Encrypts TOTP secrets (auth_totp, with the user ID as AES-GCM additional data, so a ciphertext copied to another user's row doesn't decrypt) and Apple refresh tokens (auth_identities, and auth_token_revocations while they wait to be revoked, bound to provider and subject), which are needed to revoke Apple's tokens when an account is deleted
Rotate 1. Add the new key first on every instance: k2:…,k1:…. 2. cmd/api rotate-auth-keys re-encrypts every secret with k2. 3. Remove k1
If leaked Alone: nothing. With a database copy: an attacker can compute TOTP codes, but still needs each user's password or another first factor. Rotate the key and consider asking users to set up their authenticator again
If lost Every TOTP secret is unreadable. Users sign in with recovery codes, or an operator runs cmd/api reset-mfa <email> per user. Back the key up

DATABASE_URL#

What A PostgreSQL connection URL including a user and password
Generate Development: compose.yaml sets user, password and database to the app name, bound to 127.0.0.1 only. Production: create a dedicated role with your provider; use a long random password
Env DATABASE_URL or DATABASE_URL_FILE
Public-safe? No. postgres never includes the URL in errors; config.Secret prints [redacted] in logs
Rotate Create a second password or role, deploy with it, then revoke the old one
If leaked Full read and write of every table, including password hashes and audit history. Rotate immediately and treat sessions as compromised: DELETE FROM auth_sessions signs everyone out

GOOGLE_CLIENT_SECRET#

What The OAuth client secret of the Google Web application client
Why Exchanging the authorization code for tokens at Google's token endpoint requires it, alongside PKCE (S256)
Where from Google Cloud Console → Google Auth Platform → Clients → the web client. Shown when created
Env GOOGLE_CLIENT_SECRET or GOOGLE_CLIENT_SECRET_FILE; required with GOOGLE_CLIENT_ID
Public-safe? No. GOOGLE_CLIENT_ID, GOOGLE_IOS_CLIENT_ID and GOOGLE_ANDROID_CLIENT_ID are public
Rotate In the client, Add secret, deploy the new one, then disable and delete the old one
If leaked Someone could redeem authorization codes issued to your client, if they also intercept a code for a registered redirect URI; PKCE and the state cookie make that hard. Rotate

Apple private key#

What A Sign in with Apple key: a .p8 file holding an ECDSA P-256 private key in PKCS #8 PEM
Why Apple has no static client secret: each token request carries a short JWT signed with this key
Where from Apple Developer → Certificates, IDs & Profiles → Keys → + → Sign in with Apple. Downloadable once
Env APPLE_PRIVATE_KEY_FILE (a path) or APPLE_PRIVATE_KEY (the contents). With APPLE_KEY_ID (its id) and APPLE_TEAM_ID
Validation Parsed at start by social.ParseApplePrivateKey: must be PEM PRIVATE KEY, PKCS #8, P-256
Public-safe? No. The Team ID, Key ID, Services ID and bundle IDs are public
Rotate Create a new Sign in with Apple key for the same primary App ID, deploy its APPLE_KEY_ID and file, confirm sign-in works, then revoke the old key
If leaked An attacker can create valid client secrets for your app: redeem codes, and revoke your users' Apple tokens. Revoke the key at once

RESEND_API_KEY and SMTP_PASSWORD#

What The credential the mail worker uses to deliver email
Where from Resend → API Keys (permission Sending access, scoped to one domain); or your SMTP provider's credentials page
Env RESEND_API_KEY / RESEND_API_KEY_FILE, or SMTP_PASSWORD / SMTP_PASSWORD_FILE with SMTP_USERNAME. Required when MAIL_DELIVERY is provider (always in production)
Public-safe? No
Rotate Create a new key, deploy it, delete the old one. Queued emails retry with the new key after the restart
If leaked Email sent as your verified domain: phishing that passes SPF and DKIM. Delete the key at the provider

Values the app creates#

Passwords#

Hashed with argon2id (m = 19 MiB, t = 2, p = 1, 16-byte salt, 32-byte key; ADR-0038) and stored in PHC format, $argon2id$v=19$m=…,t=…,p=…$salt$hash. Policy: 12 to 128 characters. Parameters are part of the stored string, so they can be raised later and old hashes still verify. Accounts created through Google or Apple have no password (has_password: false).

Session tokens#

Created authlib.NewToken: 32 bytes from crypto/rand, base64url, on sign-in
Stored Only SHA-256(token) in auth_sessions.token_hash. A database leak doesn't reveal usable tokens
Sent Browsers: cookie __Host-session (Secure, HttpOnly, SameSite=Lax, Path=/, no Domain). Native clients that sign in with "transport": "bearer": Authorization: Bearer <token>
Lifetime Idle 14 days (auth.session_idle_ttl), absolute 90 days (auth.session_absolute_ttl), both runtime settings
Revoke POST /v1/auth/logout, POST /v1/auth/logout-all, DELETE /v1/auth/sessions/{id}. Resetting the password ends every session; changing it ends all but the current one; deleting the account ends all
If leaked The holder acts as the user until the session is revoked or expires; sensitive changes still ask for the password or a passkey once the sign-in is 10 minutes old

Verification and reset codes#

6 random digits (authlib.NewCode), emailed to the user. Stored as SHA-256(code_id:code) in auth_codes, single use, attempt-limited, valid 15 minutes for email verification (auth.verification_code_ttl) and 30 minutes for password reset (auth.reset_code_ttl). A 6-digit code is only safe because of the short lifetime, the attempt limit and the per-IP rate limit.

TOTP secrets#

Created when a user starts POST /v1/auth/mfa/totp, returned once as a base32 key, otpauth:// URI and QR image, then encrypted with AUTH_ENCRYPTION_KEYS into auth_totp. Codes are RFC 6238 (SHA-1, 6 digits, 30 seconds); a used time step is recorded so a code can't be replayed on any instance.

Recovery codes#

10 codes like hibtq-ysrvv, shown once when TOTP is confirmed (and by cmd/seed). Stored as SHA-256(user_id:normalized code) in auth_recovery_codes; each works once. POST /v1/auth/mfa/recovery-codes replaces the set.

Passkeys#

WebAuthn keeps the private key on the user's authenticator (device, password manager or security key). The app stores the credential ID, the verified credential record from modules/auth/passkey (public key, flags, signature counter) as JSON, the authenticator's AAGUID, backup flags and a name in auth_passkeys. Registration and sign-in challenges are random, stored server-side in auth_webauthn_ceremonies, single use and short-lived. There is no server-side key to generate: the relying party is identified by WEBAUTHN_RP_ID and WEBAUTHN_ORIGINS, which are public.

OAuth state, PKCE and nonces#

For web sign-in, the app creates a random state, PKCE verifier (Google) and nonce, stores them in auth_oauth_states, and binds the state to the browser with a short-lived __Host-oauth cookie. The callback must present both. For native sign-in, POST /v1/auth/{provider}/nonce stores a single-use nonce in auth_social_nonces, which the ID token must carry (hashed with SHA-256 for Apple). Each is consumed with a conditional UPDATE … WHERE consumed_at IS NULL, so it works once on any number of instances, and is refused after expires_at.

Apple client secret#

social.appleClientSecret signs an ES256 JWT on each call to Apple's token and revoke endpoints: header kid = APPLE_KEY_ID; claims iss = APPLE_TEAM_ID, sub = the Services ID or bundle ID in use, aud = https://appleid.apple.com, iat now and exp 5 minutes later. Nothing is stored.

IDs#

Public identifiers such as usr_…, ses_…, prj_… are a type prefix and 128 random bits in base32 (authlib.NewID). They aren't secrets, but they can't be guessed or enumerated.

Seed administrator#

cmd/seed (development only; it refuses when APP_ENV=production) creates admin@example.com with a random password, a TOTP secret and 10 recovery codes, and prints them once. Only the argon2id hash, the encrypted TOTP secret and the recovery code hashes are stored. Lost: POST /v1/auth/password/forgot, cmd/api reset-mfa, or docker compose down -v.

Storing secrets in production#

  • Use your platform's secret store, and prefer mounted files (NAME_FILE) over variables: files don't show in process listings, crash dumps of the environment or deploy logs.
  • Give each environment its own values. Never copy .env to a server.
  • Restrict who can read production secrets, and log access where the platform allows.
  • .env is in .gitignore, and the repository's CI runs gitleaks. aps add mail refuses to save a secret to .env unless git ignores it.

If a secret leaks#

Leaked Do now
DATABASE_URL Rotate the database password; DELETE FROM auth_sessions; to sign everyone out; review audit_events
AUTH_ENCRYPTION_KEYS Rotate (add new, rotate-auth-keys, remove old); if the database also leaked, reset users' TOTP
GOOGLE_CLIENT_SECRET Add a new secret, deploy, delete the old
Apple .p8 Create a new key, deploy, revoke the old key
RESEND_API_KEY / SMTP_PASSWORD Delete the key at the provider; create and deploy a new one
A user's session token DELETE /v1/auth/sessions/{id} or ask the user to sign out everywhere
esc
↑↓ move↵ openesc close