Files
krow_backend/go-api/internal/auth/token.go
2026-08-24 13:06:29 +05:30

75 lines
3.1 KiB
Go

package auth
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"regexp"
)
// ErrEmptyToken is returned when a token is required and none was supplied.
// It is deliberately distinct from "no such session": an absent cookie is a
// different situation from a cookie that no longer matches a row.
var ErrEmptyToken = errors.New("auth: session token is empty")
// TokenBytes is the entropy behind a session token.
//
// 32 bytes — 256 bits — is the size of the SHA-256 output it is hashed to, so
// nothing is wasted at either end, and it puts guessing a live session far
// beyond reach: an attacker who could test a billion candidates a second would
// still need on the order of 10^60 years.
//
// This is a raw byte count, not a character count. The encoded token is 43
// characters of base64url.
const TokenBytes = 32
// tokenHashPattern is the exact shape stored in sessions.token_hash, and the
// same pattern the sessions_token_hash_sha256 CHECK constraint enforces in
// migration 000004. Validating here turns a database constraint violation into
// a clear Go error at the point the mistake was made.
var tokenHashPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
// GenerateToken returns a new, cryptographically random session token.
//
// The returned string is the secret itself. It is what goes into the HttpOnly
// cookie and it must never be written to the database, to a log, or to an
// error message. Only its hash is persisted — see HashToken.
//
// base64url without padding, so the value is safe in a cookie, a header and a
// URL without escaping, and contains no '=' to be mangled by a cookie parser.
func GenerateToken() (string, error) {
buf := make([]byte, TokenBytes)
if _, err := rand.Read(buf); err != nil {
// There is no fallback. math/rand here would produce tokens an
// attacker can predict from a handful of observed sessions.
return "", fmt.Errorf("auth: read random bytes: %w", err)
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
// HashToken returns the lowercase hex SHA-256 of a session token.
//
// This is what the database stores. A plain hash — not argon2 — is the right
// choice here and the wrong one for a password, and the difference is entropy:
// a session token is 256 uniformly random bits, so there is no dictionary to
// run against it and no work factor worth paying on every single request. A
// password is chosen by a human and needs argon2id precisely because it is not.
//
// The function is pure and deterministic: the same token always hashes to the
// same string, which is what makes lookup by hash possible at all.
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
// IsTokenHash reports whether s has the shape HashToken produces.
//
// Used to catch the one mistake that would be catastrophic and silent: passing
// a raw token where a hash is expected, and storing the secret in plaintext.
// A raw token is base64url and contains characters outside [0-9a-f], or is the
// wrong length, so it always fails this test.
func IsTokenHash(s string) bool { return tokenHashPattern.MatchString(s) }