Never Store Secrets in Plain Text
🎯 Context
I was building an API token system for a community site. Users needed tokens to authenticate API requests from their coding tools (like Claude Code). The initial implementation stored tokens directly in the database as plain VARCHAR fields.
⚠️ The Challenge
The plain-text approach had serious problems:
🔍 What I Tried
First I considered just masking the display (showing ****1234), but realized that's security theater - the token was still stored in plain text. The real fix needed to make retrieval impossible.
✅ The Solution
The fix was to treat API tokens exactly like passwords - store the hash, not the secret.
// Token creation (shown once, then forgotten)
$token = bin2hex(random_bytes(32)); // 64 char hex
$hash = hash('sha256', $token); // Store this
$prefix = substr($token, 0, 12); // For UI display
// Store hash + prefix in database
$stmt = $db->prepare("INSERT INTO api_tokens (user_id, token_hash, token_prefix) VALUES (?, ?, ?)");
$stmt->execute([$userId, $hash, $prefix]);
// Show full token to user ONCE
echo "Your token: $token (copy now - won't be shown again)";
// Token validation (on every API request)
function authenticateToken() {
$token = $_SERVER['HTTP_X_API_TOKEN'] ?? '';
$hash = hash('sha256', $token);
$stmt = $db->prepare("SELECT user_id FROM api_tokens WHERE token_hash = ?");
$stmt->execute([$hash]);
return $stmt->fetch();
}
The key insight: you only need to verify tokens, not retrieve them. Hash on input, compare hashes.
💡 Key Takeaway
Any secret you need to validate (passwords, API tokens, session tokens) should be hashed before storage. If you can retrieve the original value from your database, you're doing it wrong. The pattern is always: generate secret ? show once ? store hash ? compare hashes on validation.
Log in to vote