fix(security): hash password reset tokens before database storage
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s

INF-10: Reset tokens are now SHA-256 hashed before INSERT. Validation
hashes the received token and compares against stored hash. Plain
tokens never persisted.
This commit is contained in:
senke 2026-02-22 17:36:10 +01:00
parent 6b25ccc9da
commit 763aea15cb

View file

@ -3,8 +3,10 @@ package services
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"database/sql" "database/sql"
"encoding/base64" "encoding/base64"
"encoding/hex"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -53,6 +55,13 @@ func NewPasswordService(db *database.Database, logger *zap.Logger) *PasswordServ
} }
} }
// hashToken returns the hex-encoded SHA-256 hash of the token.
// INF-10: Tokens are stored hashed in the DB; only the hash is persisted.
func (ps *PasswordService) hashToken(token string) string {
h := sha256.Sum256([]byte(token))
return hex.EncodeToString(h[:])
}
// GetUserByEmail retrieves a user by email // GetUserByEmail retrieves a user by email
func (ps *PasswordService) GetUserByEmail(email string) (*UserInfo, error) { func (ps *PasswordService) GetUserByEmail(email string) (*UserInfo, error) {
ctx := context.Background() ctx := context.Background()
@ -87,12 +96,13 @@ func (ps *PasswordService) GeneratePasswordResetToken(userID uuid.UUID) (string,
// Set expiration (1 hour) // Set expiration (1 hour)
expiresAt := time.Now().Add(1 * time.Hour) expiresAt := time.Now().Add(1 * time.Hour)
// Store in database // INF-10: Store hash in DB, return plain token to user
ctx := context.Background() ctx := context.Background()
tokenHash := ps.hashToken(token)
_, err = ps.db.ExecContext(ctx, ` _, err = ps.db.ExecContext(ctx, `
INSERT INTO password_reset_tokens (user_id, token, expires_at, used) INSERT INTO password_reset_tokens (user_id, token, expires_at, used)
VALUES ($1, $2, $3, FALSE) VALUES ($1, $2, $3, FALSE)
`, userID, token, expiresAt) `, userID, tokenHash, expiresAt)
if err != nil { if err != nil {
return "", time.Time{}, err return "", time.Time{}, err
@ -109,13 +119,14 @@ func (ps *PasswordService) GeneratePasswordResetToken(userID uuid.UUID) (string,
func (ps *PasswordService) ResetPassword(token, newPassword string) error { func (ps *PasswordService) ResetPassword(token, newPassword string) error {
ctx := context.Background() ctx := context.Background()
// Get token info // INF-10: Hash received token and look up by hash
tokenHash := ps.hashToken(token)
var resetToken PasswordResetToken var resetToken PasswordResetToken
err := ps.db.QueryRowContext(ctx, ` err := ps.db.QueryRowContext(ctx, `
SELECT id, user_id, token, expires_at, used, created_at SELECT id, user_id, token, expires_at, used, created_at
FROM password_reset_tokens FROM password_reset_tokens
WHERE token = $1 AND used = FALSE WHERE token = $1 AND used = FALSE
`, token).Scan( `, tokenHash).Scan(
&resetToken.ID, &resetToken.ID,
&resetToken.UserID, &resetToken.UserID,
&resetToken.Token, &resetToken.Token,