- Add generate_api_token with format 'or_api_' + 48 bytes random - Implement hash_token with SHA-256 - Add verify_api_token with timing-safe comparison (secrets.compare_digest) - Only hash stored in DB, plaintext shown once - 20 comprehensive tests with 100% coverage - Handle TypeError for non-string inputs
85 lines
2.4 KiB
Python
85 lines
2.4 KiB
Python
"""API token generation and verification service.
|
|
|
|
This module provides secure API token generation using cryptographically
|
|
secure random generation and SHA-256 hashing. Only the hash is stored.
|
|
"""
|
|
|
|
import hashlib
|
|
import secrets
|
|
|
|
|
|
TOKEN_PREFIX = "or_api_"
|
|
TOKEN_ENTROPY_BYTES = 48 # Results in ~64 URL-safe base64 chars
|
|
|
|
|
|
def generate_api_token() -> tuple[str, str]:
|
|
"""Generate a new API token.
|
|
|
|
Generates a cryptographically secure random token with format:
|
|
'or_api_' + 48 bytes of URL-safe base64 (~64 chars)
|
|
|
|
Returns:
|
|
Tuple of (plaintext_token, token_hash) where:
|
|
- plaintext_token: The full token to show once to the user
|
|
- token_hash: SHA-256 hash to store in database
|
|
|
|
Example:
|
|
>>> plaintext, hash = generate_api_token()
|
|
>>> print(plaintext)
|
|
'or_api_x9QzGv2K...'
|
|
>>> # Store hash in DB, show plaintext to user once
|
|
"""
|
|
# Generate cryptographically secure random token
|
|
random_part = secrets.token_urlsafe(TOKEN_ENTROPY_BYTES)
|
|
plaintext = f"{TOKEN_PREFIX}{random_part}"
|
|
|
|
# Hash the entire token
|
|
token_hash = hash_token(plaintext)
|
|
|
|
return plaintext, token_hash
|
|
|
|
|
|
def hash_token(plaintext: str) -> str:
|
|
"""Hash a token using SHA-256.
|
|
|
|
Args:
|
|
plaintext: The plaintext token to hash.
|
|
|
|
Returns:
|
|
Hexadecimal string of the SHA-256 hash.
|
|
|
|
Raises:
|
|
TypeError: If plaintext is not a string.
|
|
"""
|
|
if not isinstance(plaintext, str):
|
|
raise TypeError("plaintext must be a string")
|
|
|
|
return hashlib.sha256(plaintext.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def verify_api_token(plaintext: str, token_hash: str) -> bool:
|
|
"""Verify an API token against its stored hash.
|
|
|
|
Uses timing-safe comparison to prevent timing attacks.
|
|
|
|
Args:
|
|
plaintext: The plaintext token provided by the user.
|
|
token_hash: The SHA-256 hash stored in the database.
|
|
|
|
Returns:
|
|
True if the token matches the hash, False otherwise.
|
|
|
|
Raises:
|
|
TypeError: If either argument is not a string.
|
|
"""
|
|
if not isinstance(plaintext, str):
|
|
raise TypeError("plaintext must be a string")
|
|
if not isinstance(token_hash, str):
|
|
raise TypeError("token_hash must be a string")
|
|
|
|
# Compute hash of provided plaintext
|
|
computed_hash = hash_token(plaintext)
|
|
|
|
# Use timing-safe comparison to prevent timing attacks
|
|
return secrets.compare_digest(computed_hash, token_hash)
|