Skip to content

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, and NewRing for verifying against several keys.
    • aioz-template account slice (access token TTL default 15m).
    • Ships in aioz-common v0.3.2 (jwt.LoadKeyRing, jwt.GenerateKey, jwt.WithType / jwt.RequireType, RFC 7638 kid computation) and in the aioz-template genkey command. A service on an older aioz-common meets the rules below by other means until it upgrades.

Algorithms ​

  • Pin the algorithm on the verifier: the verifier MUST use a configured algorithm, MUST NOT take it from the token's alg header, and MUST reject none (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.
  • 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 ​

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). genkey enforces 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 exportable is set, and that setting cannot be undone (Vault Transit API).

Key id and rotation ​

  • kid on every token: every token signed with an asymmetric key MUST carry a kid header. It SHOULD be the RFC 7638 JWK thumbprint, which RFC 7638 §1 names as a suitable kid value (aioz-common jwt.Thumbprint computes it).
  • Publish, then switch, then retire: rotation MUST follow this order:
    1. Publish the new public key to every verifier before any signer uses it.
    2. Switch the signer to the new key.
    3. 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_period 24h and keep the old public key for verification_ttl 24h 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 genkey command (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 sends Cache-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 account TokenTTL).
  • Claims: a JWT access token MUST carry iss, sub, aud, exp, iat, jti, and the header typ: at+jwt. Add client_id when there are OAuth clients (RFC 9068 §2.1, §2.2).
  • Verification order: the verifier MUST check, in order:
    1. the signature, before trusting any claim (ASVS 9.1.1);
    2. typ is at+jwt (RFC 9068 §4; RFC 8725 §3.11);
    3. iss matches exactly (RFC 9068 §4; RFC 8725 §3.8);
    4. aud contains this service (RFC 9068 §4; RFC 8725 §3.9);
    5. exp and nbf (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 typ other than at+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_leeway defaults 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 from crypto/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-store and Pragma: 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 jti or 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 call Reveal() 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:

  1. Generate a new key and switch every signer to it, dropping the compromised kid from verifiers with no overlap.
  2. Revoke refresh-token families, per affected user or globally.
  3. Force re-authentication.
  4. Search the audit logs for use of the compromised key or tokens.
  5. 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 ​

sourceaccess tokenrefresh / sessionsigning keys
Google1h (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 Vaultsystem 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 baselineat most 1h, default 15midle at most 30d (1-7d), absolute at most 90d (30d); one-time codes at most 10 minRS256 or EdDSA; rotate at least yearly, SHOULD 90d

Sources ​


Last updated: 2026-09-11. Found something wrong? Open a PR.