Token and Signing Key Baseline
The team's minimum requirements for access tokens, refresh tokens, one-time login codes and signing keys. Modeled on Google and HashiCorp practice, backed by IETF, OWASP and NIST.
MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as defined in RFC 2119: MUST is a hard requirement, SHOULD is the default you need a written reason to skip, MAY is optional.
Scope and how to deviate
- Applies to any service that issues or verifies access tokens, refresh tokens, one-time codes or signing keys.
- Record your values: a service MUST record its chosen values (TTLs, algorithm, rotation cadence) in its README, and MUST justify any deviation from this document there (OWASP ASVS 5.0 7.1.1).
- Reference implementation:
- aioz-common
secret:secret.NewToken(256-bit random token plus its hash),secret.Hash(SHA-256 stored form),secret.Token(redacts itself in fmt, slog, JSON and SQL;Reveal()returns the plaintext). - aioz-common
jwt: pinned-algorithm signers and verifiers,RequireIssuer,RequireAudience,WithLeeway, andNewRingfor verifying against several keys. - aioz-template
accountslice (access token TTL default 15m). - Ships in aioz-common
v0.3.2(jwt.LoadKeyRing,jwt.GenerateKey,jwt.WithType/jwt.RequireType, RFC 7638kidcomputation) and in the aioz-templategenkeycommand. A service on an older aioz-common meets the rules below by other means until it upgrades.
- aioz-common
Algorithms
- Pin the algorithm on the verifier: the verifier MUST use a configured algorithm, MUST NOT take it from the token's
algheader, and MUST rejectnone(RFC 8725 §3.1, §3.2; ASVS 9.1.2; RFC 9068 §4). - One key, one algorithm: each key MUST be used with exactly one algorithm (RFC 8725 §3.1).
- Asymmetric across a trust boundary: a token verified by another service MUST be signed with an asymmetric algorithm: RS256, or EdDSA (Ed25519) for new keys.
- RFC 9068 §2.1 recommends asymmetric signing and requires RS256 support.
- Google publishes RS256 as its only ID token algorithm (Google OpenID Connect).
- Vault identity tokens default to RS256 and allow EdDSA (Vault identity tokens API).
- FIPS 186-5 approves EdDSA (FIPS 186-5 §1 and §7).
- HS256 only inside one service: HS256 MAY be used when the signer and verifier are the same service, with a random key of at least 256 bits. RFC 7518 §3.2 requires a key at least as long as the hash output (256 bits for HS256); RFC 8725 §3.5 forbids using a human-memorable password as the key.
- House default: a key-pair ring loaded from files (aioz-common
jwt.LoadKeyRing).
Key size
- RSA keys MUST be at least 2048 bits (NIST SP 800-131A r2, Table 2).
- New keys SHOULD be Ed25519 or RSA-3072. RSA-2048 gives 112-bit security (SP 800-57 Part 1 r5, Table 2), and 112-bit strength is acceptable for applying protection only through 2030 (SP 800-57 Part 1 r5, §5.6.3 Table 4). Ed25519 is intended to provide about 128-bit security (FIPS 186-5 §7).
genkeydefaults to Ed25519.
Key storage
- Not in the artifact: private keys MUST NOT be in git, container images, or inline config values. Load them from files or a secret manager (Google Cloud: best practices for managing service account keys, which says not to commit keys to source repositories or embed them in binaries; playbook Security Basics, "Secrets in the vault").
- File permissions: key files SHOULD be mode 0600 and their directories 0700 (common practice; no primary source found).
genkeyenforces this. - Wrapped or remote: keys SHOULD live in a KMS or Vault, or be encrypted under a key-encryption key. Tink advises against plaintext keysets and recommends encrypting them with a KEK held in a KMS (Tink key management overview); Vault Transit keys are not exportable unless
exportableis set, and that setting cannot be undone (Vault Transit API).
Key id and rotation
kidon every token: every token signed with an asymmetric key MUST carry akidheader. It SHOULD be the RFC 7638 JWK thumbprint, which RFC 7638 §1 names as a suitablekidvalue (aioz-commonjwt.Thumbprintcomputes it).- Publish, then switch, then retire: rotation MUST follow this order:
- Publish the new public key to every verifier before any signer uses it.
- Switch the signer to the new key.
- Keep the old public key for at least max access-token TTL + clock leeway + rollout time.
- Precedent for overlap: Vault identity token keys have
rotation_period24h and keep the old public key forverification_ttl24h after rotation (Vault identity tokens API). Google's JWKS served two RS256 keys at once when fetched on 2026-09-11 (observed, Google certs endpoint). - Cadence: keys MUST be rotated at least yearly and immediately on suspected compromise. SP 800-57 suggests a 1 to 3 year cryptoperiod for private signature keys (SP 800-57 Part 1 r5, §5.3.6 Table 1); the one-year cap is the house rule. Keys SHOULD rotate every 90 days or faster, automated. Tink recommends 30- to 90-day keys for symmetric encryption and allows a lower rotation frequency for asymmetric keys only if you can securely revoke them (Tink key management overview).
- House procedure: the service's
genkeycommand (genkey,genkey list,genkey public <name>). - Forced re-login: a planned deploy MAY drop the old key outright, forcing every user to log in again, when that is an explicit decision documented in the service README or the PR.
- JWKS caching: a JWKS served to other services SHOULD send
Cache-Control: max-age. Google tells verifiers to cache its keys using the response's cache directives (Google OpenID Connect), and its certs endpoint sendsCache-Control: public, max-age=...(observed 2026-09-11).
Access tokens
- Lifetime MUST be at most 1 hour. Bearer tokens SHOULD be "short-lived (one hour or less)" (RFC 6750 §5.3); Google user and service account access tokens expire after one hour (Google Cloud token types). House default: 15 minutes (aioz-template
accountTokenTTL). - Claims: a JWT access token MUST carry
iss,sub,aud,exp,iat,jti, and the headertyp: at+jwt. Addclient_idwhen there are OAuth clients (RFC 9068 §2.1, §2.2). - Verification order: the verifier MUST check, in order:
- the signature, before trusting any claim (ASVS 9.1.1);
typisat+jwt(RFC 9068 §4; RFC 8725 §3.11);issmatches exactly (RFC 9068 §4; RFC 8725 §3.8);audcontains this service (RFC 9068 §4; RFC 8725 §3.9);expandnbf(RFC 7519 §4.1.4, §4.1.5).
- Type confusion: a token of another type (a refresh token, an ID token) MUST be refused as an access token (RFC 9068 §4 rejects any
typother thanat+jwt). - Clock leeway MUST be at most 60s. RFC 7519 allows "some small leeway, usually no more than a few minutes" (RFC 7519 §4.1.4); Vault JWT auth
clock_skew_leewaydefaults to 60 seconds (Vault JWT auth API). House default: 30s. - One audience: a token SHOULD target exactly one audience. RFC 9700 says access tokens SHOULD be audience-restricted to a specific resource server or, if that is not feasible, a small set (RFC 9700 §2.3).
Refresh tokens
- Opaque and random: a refresh token MUST be opaque (not a JWT), come from a CSPRNG, and carry at least 256 bits of entropy.
- RFC 6749 §10.10: the probability of guessing a token MUST be at most 2^-128 and SHOULD be at most 2^-160.
- ASVS 7.2.3: reference tokens come from a CSPRNG with at least 128 bits of entropy.
- House value: 256 bits via aioz-common
secret.NewToken(32 bytes fromcrypto/rand).
- Stored as a hash: the server MUST store only a hash of the token. SHA-256 is enough at this entropy, since a slow hash only helps against guessable input (house rule; aioz-common
secret.Hash). - Rotate on use: a refresh token MUST be replaced on every use, and presenting an already-rotated token MUST revoke the whole token family (RFC 9700 §4.14.2; rotation is one of the two replay defenses RFC 9700 §2.2.2 requires for public clients).
- Reuse grace window: a service MAY accept the just-rotated token for a short window to absorb lost responses and parallel tabs. The window SHOULD be 10s or less (house rule; no primary source).
- Idle and absolute lifetime: a refresh token MUST have both. RFC 9700 says refresh tokens "SHOULD expire if the client has been inactive for some time" and leaves the value to the server (RFC 9700 §4.14.2). House limits:
- Idle MUST be at most 30d and SHOULD be 1-7d.
- Absolute MUST be at most 90d and SHOULD be 30d.
One-time login codes
Emailed or SMS codes used to log in.
- Random source: codes MUST come from a CSPRNG, never
math/rand. NIST requires out-of-band secrets from an approved random bit generator (SP 800-63B-4 §3.1.3.2). - Length: codes MUST be at least six decimal digits or equivalent (SP 800-63B-4 §3.1.3.2).
- Stored hashed: codes MUST be stored hashed (bcrypt or a keyed HMAC), never in plaintext. A six-digit code has only 10^6 values, so an unkeyed fast hash can be reversed by trying them all.
- Single use: a code MUST be accepted at most once (SP 800-63B-4 §3.1.3.2). A resend MUST issue a new code and invalidate the old one.
- Attempt cap: each code MUST have an attempt cap enforced atomically in the database, so concurrent guesses cannot exceed it. A resend MUST NOT reset the failed-attempt count: NIST says "Generating a new authentication secret SHALL NOT reset the failed authentication count" (SP 800-63B-4 §3.1.3.2; throttling in §3.2.2).
- TTL MUST be at most 10 minutes. NIST: "the authentication SHALL be considered invalid unless completed within 10 minutes" (SP 800-63B-4 §3.1.3.2).
- Comparison MUST be constant time.
- No backdoors: production MUST NOT contain fixed test codes or bypass accounts. If a test environment needs them, gate them behind config that is empty by default.
- Email is weaker: NIST 800-63B-4 says "Email SHALL NOT be used for out-of-band authentication" (SP 800-63B-4 §3.1.3.1); it exempts only address-confirmation and recovery codes. An emailed login code is therefore not a NIST-recognized authenticator. We still apply the out-of-band limits above to it, and a service that needs AAL2 MUST NOT rely on an emailed code alone. For comparison, NIST caps recovery codes sent to an email address at 24 hours and those sent by text or voice at 10 minutes (SP 800-63B-4 §4.2.1.2).
Revocation
- Revoke on (ASVS 7.4.1-7.4.3):
- logout (the whole refresh-token family) MUST;
- detected refresh-token reuse MUST;
- account disable or delete MUST;
- credential change MUST (ASVS 7.4.3 requires offering to end all other sessions; we make it the default).
- User and admin controls: a service SHOULD offer a per-user revoke-all and an admin kill switch (ASVS 7.4.5), and a session list where the user can revoke sessions (ASVS 7.5.2).
- Unknown tokens: a logout or revocation endpoint answers 200 even for an unknown or invalid token (RFC 7009 §2.2).
- JWTs outlive revocation: a JWT access token stays valid until it expires unless verifiers check a denylist, which is why its TTL is short (ASVS 7.4.1; OWASP JWT Cheat Sheet).
Transport
- Header only: tokens MUST travel only in
Authorization: Bearer(or a dedicated header for refresh tokens), never in a URL or query string (RFC 9700 §4.3.2; RFC 6750 §2.3, §5.3). - No caching: token responses MUST send
Cache-Control: no-storeandPragma: no-cache(RFC 6749 §5.1). - TLS only: requests carrying tokens MUST use TLS (RFC 6750 §5.3 "Always use TLS (https)").
Logging
- Never log secrets: services MUST NOT log tokens, codes, keys or secrets (OWASP Logging Cheat Sheet, "Data to exclude").
- Correlate safely: correlate by
jtior a keyed hash. Vault's audit devices write an HMAC-SHA256 of most string values (Vault audit devices). - In Go: hold tokens in aioz-common
secret.Token, which redacts itself in fmt, slog, JSON and SQL, and callReveal()once at handover. - Denylist key: an early-kill denylist for JWTs MUST be keyed by
iss+jti, not by the raw token or a hash of it, which JWT malleability can bypass (OWASP JWT Cheat Sheet, "JWT denylist").
Compromise runbook
When a signing key or token store may be compromised:
- Generate a new key and switch every signer to it, dropping the compromised
kidfrom verifiers with no overlap. - Revoke refresh-token families, per affected user or globally.
- Force re-authentication.
- Search the audit logs for use of the compromised key or tokens.
- Post in
#incidents(playbook Security Basics).
Sources: Google says to act quickly on a suspected key compromise and can auto-disable leaked keys (Google Cloud service account keys); rotation limits how long a leaked key is useful (Tink key management overview); SP 800-57 covers cryptoperiods and compromised keys (SP 800-57 Part 1 r5).
Reference values
| source | access token | refresh / session | signing keys |
|---|---|---|---|
| 1h (token types) | expires after 6 months unused; 7 days for "Testing" apps; 100 live refresh tokens per account per client ID (OAuth 2.0) | RS256; 2 keys published, max-age cached (observed 2026-09-11, certs) | |
| HashiCorp Vault | system max TTL 32 days / 768h (tokens) | same max TTL applies (tokens) | identity key rotation_period 24h, verification_ttl 24h, RS256 default (identity tokens) |
| HashiCorp Boundary | - | auth_token_time_to_live 7d, auth_token_time_to_stale 1d (controller config) | - |
| NIST SP 800-63B-4 AAL2 | - | reauth at most 24h (SHOULD), 1h inactivity (SHOULD) (§2.2.3) | - |
| NIST SP 800-63B-4 AAL3 | - | reauth at most 12h (SHALL), 15m inactivity (SHOULD) (§2.3.3) | - |
| OWASP | - | idle 2-5 min high-value, 15-30 min low-risk; absolute 4-8h for a full-day office app (Session Management) | - |
| This baseline | at most 1h, default 15m | idle at most 30d (1-7d), absolute at most 90d (30d); one-time codes at most 10 min | RS256 or EdDSA; rotate at least yearly, SHOULD 90d |
Sources
- RFC 2119: https://www.rfc-editor.org/rfc/rfc2119.html
- RFC 6749: https://www.rfc-editor.org/rfc/rfc6749.html
- RFC 6750: https://www.rfc-editor.org/rfc/rfc6750.html
- RFC 7009: https://www.rfc-editor.org/rfc/rfc7009.html
- RFC 7518: https://www.rfc-editor.org/rfc/rfc7518.html
- RFC 7519: https://www.rfc-editor.org/rfc/rfc7519.html
- RFC 7638: https://www.rfc-editor.org/rfc/rfc7638.html
- RFC 8725: https://www.rfc-editor.org/rfc/rfc8725.html
- RFC 9068: https://www.rfc-editor.org/rfc/rfc9068.html
- RFC 9700: https://www.rfc-editor.org/rfc/rfc9700.html
- OWASP ASVS 5.0 V7 Session Management: https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x16-V7-Session-Management.md
- OWASP ASVS 5.0 V9 Self-contained Tokens: https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x18-V9-Self-contained-Tokens.md
- OWASP JSON Web Token Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html
- OWASP Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- OWASP Logging Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
- NIST SP 800-63B-4: https://pages.nist.gov/800-63-4/sp800-63b.html
- NIST SP 800-57 Part 1 Rev. 5: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf
- NIST SP 800-131A Rev. 2: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf
- NIST FIPS 186-5: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf
- Google OAuth 2.0: https://developers.google.com/identity/protocols/oauth2
- Google OpenID Connect: https://developers.google.com/identity/openid-connect/openid-connect
- Google OAuth 2.0 certs (JWKS): https://www.googleapis.com/oauth2/v3/certs
- Google Cloud token types: https://docs.cloud.google.com/docs/authentication/token-types
- Google Cloud service account key best practices: https://docs.cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys
- Tink key management overview: https://developers.google.com/tink/key-management-overview
- Vault tokens: https://developer.hashicorp.com/vault/docs/concepts/tokens
- Vault JWT/OIDC auth API: https://developer.hashicorp.com/vault/api-docs/auth/jwt
- Vault identity tokens API: https://developer.hashicorp.com/vault/api-docs/secret/identity/tokens
- Vault Transit API: https://developer.hashicorp.com/vault/api-docs/secret/transit
- Vault audit devices: https://developer.hashicorp.com/vault/docs/audit
- Boundary controller configuration: https://developer.hashicorp.com/boundary/docs/configuration/controller
Last updated: 2026-09-11. Found something wrong? Open a PR.