fix(security): hash password reset tokens before database storage
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:
parent
6b25ccc9da
commit
763aea15cb
1 changed files with 15 additions and 4 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue