package helpers import ( "crypto/rand" "crypto/sha256" "database/sql" "encoding/base64" "time" ) func randomBase64(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil } func HashVerifier(verifier string) string { sum := sha256.Sum256([]byte(verifier)) return base64.RawURLEncoding.EncodeToString(sum[:]) } // StoreToken inserts a new token row func StoreToken(db *sql.DB, userID int64, selector, verifierHash string, expiresAt time.Time) error { _, err := db.Exec(` INSERT INTO remember_tokens (user_id, selector, verifier_hash, issued_at, expires_at) VALUES ($1,$2,$3,NOW(),$4)`, userID, selector, verifierHash, expiresAt) return err } // FindToken fetches selector+hash func FindToken(db *sql.DB, selector string) (userID int64, verifierHash string, expiresAt time.Time, revokedAt *time.Time, err error) { err = db.QueryRow(`SELECT user_id, verifier_hash, expires_at, revoked_at FROM remember_tokens WHERE selector=$1`, selector). Scan(&userID, &verifierHash, &expiresAt, &revokedAt) return } // RevokeToken marks token as revoked func RevokeToken(db *sql.DB, selector string) error { _, err := db.Exec(`UPDATE remember_tokens SET revoked_at=NOW() WHERE selector=$1`, selector) return err } // GenerateAndStore creates a new remember-me token, stores it server-side, // and returns the cookie-safe plaintext value to set on the client func GenerateAndStore(db *sql.DB, userID int64, duration time.Duration) (string, time.Time, error) { selector, err := randomBase64(16) if err != nil { return "", time.Time{}, err } verifier, err := randomBase64(32) if err != nil { return "", time.Time{}, err } hash := HashVerifier(verifier) expires := time.Now().Add(duration) if err := StoreToken(db, userID, selector, hash, expires); err != nil { return "", time.Time{}, err } // The client cookie value contains selector + verifier cookieVal := selector + ":" + verifier return cookieVal, expires, nil }