Expand description
Scoped API keys (spec §v2.4 Task 2): mint a high-entropy key, store only its SHA-256 hash, authenticate requests, and scope-check them.
§Why SHA-256, not argon2
API keys are machine-generated with 256 bits of entropy (mint draws 32
bytes from the OS CSPRNG), so brute-forcing the preimage is infeasible
regardless of hash speed. argon2 buys nothing here and would make the lookup
column slow and variable-length. A plain hex SHA-256 gives a fast, fixed-width
(64-char) lookup key — exactly what a DB index wants.
§Why the verify is constant-time
verify hashes the presented plaintext and compares the two 32-byte
digests in constant time via hmac’s verify_slice (the same primitive the
webhook code uses). It deliberately does NOT compare the hex Strings with
==: String/&str equality short-circuits on the first differing byte,
which leaks, through timing, how many leading hex chars of the stored hash a
guess matched. Comparing the raw digests in fixed time closes that channel.
(The stored hash is not itself a secret the way an HMAC key is, but verifying
in constant time is cheap and removes the channel by construction.)
§DI: how the store reaches the extractor
ApiKey resolves the store as the concrete newtype ApiKeys (which wraps
Arc<dyn ApiKeyStore>), NOT a bare Arc<dyn ApiKeyStore>. The DI layer keys
providers on TypeId and round-trips a bare trait-object Arc just fine, but
the newtype is the documented, unambiguous contract: the app calls
app.provide(ApiKeys::new(store)) and the extractor resolves ApiKeys. See
the bare_arc_dyn_also_round_trips_through_di test for evidence the bare form
works too; we still standardize on the newtype.
Structs§
- ApiKey
- API-key extractor. Reads the key from
Authorization: Bearer <key>(checked first) orX-API-Key: <key>, hashes it, looks it up in the providedApiKeysstore, and yields the matchingApiKeyRecord. A missing, malformed, or unknown key is a 401. Scope checks are a separate, explicit step (ApiKeyRecord::require_scope) so a handler decides what it needs. - ApiKey
Record - A stored API-key record: its DB id, display prefix, hex hash, and scopes.
- ApiKeys
- The DI handle for an
ApiKeyStore. Apps register the store withapp.provide(ApiKeys::new(store)); theApiKeyextractor resolves this concrete type. A newtype (rather than a bareArc<dyn ApiKeyStore>) is the documented contract — see the module docs. - InMemory
ApiKey Store - In-memory store keyed by hex hash — for tests, the mock, and small deploys.
Production apps implement
ApiKeyStoreover their database instead. - Minted
ApiKey - A freshly minted key. The
plaintextis shown to the operator exactly once and is NEVER stored — only itshash(andprefix) are persisted.
Traits§
- ApiKey
Store - Looks up a stored key by its hex hash. Object-safe so apps can back it with a
DB table (returning
Nonefor an unknown hash, NOT an error).
Functions§
- hash_
key - Hex SHA-256 of the full key plaintext — the value stored in the lookup column.
- mint
- Mint a new API key: 32 bytes from the OS CSPRNG, formatted as
{prefix}_{base64url_nopad(random)}. The returnedMintedApiKey::hashis what you store;MintedApiKey::plaintextis shown once and discarded. - require_
scope - Scope check (mirrors
crate::require_role):Ok(())whenscopescontainsneededor the wildcard"*", otherwiseError::forbidden()(403). The wildcard is an admin/root grant — a"*"key passes every check. - verify
- Constant-time check that
plaintexthashes tostored_hash.
Type Aliases§
- ApiKey
Future - The boxed
Sendfuture everyApiKeyStoremethod returns. Hand-boxed (notasync-trait) so the trait stays object-safe behinddyn— the same idiom asjerrycan_jobs’sJobFutureandjerrycan_ratelimit’sHitFuture.