feat(auth): add API-key authentication and tenant resolution
This commit is contained in:
39
src/application/auth/keys.py
Normal file
39
src/application/auth/keys.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""API-key generation and hashing (ADR-0008, ADR-0009).
|
||||
|
||||
Keys are `sk_{prefix}_{secret}`. `prefix` is non-secret and indexed
|
||||
(`api_keys.key_prefix`); `secret` is 256 bits of `secrets.token_urlsafe`
|
||||
entropy, stored only as a SHA-256 hash. A random 256-bit secret does not
|
||||
benefit from a slow password-hashing KDF the way a human-chosen password
|
||||
does — the cost that defends against dictionary/brute-force guessing over a
|
||||
low-entropy input has nothing to defend here, and would only tax every
|
||||
request. Comparison is constant-time to avoid a hash-timing oracle.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
|
||||
_PREFIX_LENGTH = 16
|
||||
|
||||
|
||||
def generate_api_key() -> tuple[str, str, str]:
|
||||
"""Return `(key_prefix, secret, full_key)` for a newly issued key."""
|
||||
key_prefix = secrets.token_hex(_PREFIX_LENGTH // 2)
|
||||
secret = secrets.token_urlsafe(32)
|
||||
return key_prefix, secret, f"sk_{key_prefix}_{secret}"
|
||||
|
||||
|
||||
def parse_api_key(full_key: str) -> tuple[str, str] | None:
|
||||
"""Return `(key_prefix, secret)`, or `None` if the token is malformed."""
|
||||
parts = full_key.split("_", 2)
|
||||
if len(parts) != 3 or parts[0] != "sk" or not parts[1] or not parts[2]:
|
||||
return None
|
||||
return parts[1], parts[2]
|
||||
|
||||
|
||||
def hash_secret(secret: str) -> str:
|
||||
return hashlib.sha256(secret.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def verify_secret(secret: str, key_hash: str) -> bool:
|
||||
return hmac.compare_digest(hash_secret(secret), key_hash)
|
||||
Reference in New Issue
Block a user