Secret generation, hashing and constant-time comparison for the authorization-server facade.
Nothing secret is ever stored in plaintext. Two shapes are used, for two different reasons:
- Client secrets —
hash/2/verify/3, PBKDF2-HMAC-SHA256 with a per-secret random salt. A client secret is chosen once and checked often; the salt and the iteration count are what make a stolen table useless. - Codes and tokens —
token_hash/1, plain SHA-256. Authorization codes and refresh tokens are 256 bits of:crypto.strong_rand_bytes/1with a few minutes' or days' life, so there is nothing to brute-force and no dictionary to salt against; PBKDF2 on the token path would only add latency to every refresh.
Every comparison goes through :crypto.hash_equals/2 on equal-length
digests, so neither the length of a candidate nor the position of its first
wrong byte is observable.
Overriding the hasher
A host with an existing password-hashing dependency (Argon2, bcrypt) can pass
secret_hasher: {MyApp.Hash, :hash} — a 1-arity function returning a
deterministic digest for a given secret. verify/3 then re-hashes the
candidate and compares. Deterministic is a real constraint: a randomly-salted
hasher like Argon2.hash_pwd_salt/1 will never compare equal here. Wrap such
a library's own verify_pass/2 instead of using this module.
Summary
Functions
Constant-time equality for two binaries of any length.
A URL-safe random secret. bytes is entropy, not output length (32 bytes →
43 characters).
Hash a client secret for storage.
SHA-256 of a code or token, lowercase hex — what an authorization code or refresh token is stored as.
Verify a candidate secret against a stored hash, in constant time.
Functions
Constant-time equality for two binaries of any length.
:crypto.hash_equals/2 requires equal sizes (and raises otherwise), so both
sides are digested first — which also removes length from the comparison.
@spec generate(pos_integer()) :: String.t()
A URL-safe random secret. bytes is entropy, not output length (32 bytes →
43 characters).
Hash a client secret for storage.
Returns pbkdf2-sha256$<iterations>$<salt>$<derived-key> — self-describing,
so the iteration count can be raised later without invalidating old rows.
Options: :iterations (default 100000), :secret_hasher.
SHA-256 of a code or token, lowercase hex — what an authorization code or refresh token is stored as.
Verify a candidate secret against a stored hash, in constant time.
A malformed or nil stored hash answers false — after doing the same work,
so an unknown client_id and a wrong secret take the same time.