Files
go-cart-actor/internal/customerauth/tokens.go
T
mats 528c59bfd3
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
cart and checkout
2026-06-27 19:49:00 +02:00

63 lines
1.9 KiB
Go

package customerauth
import (
"crypto/hmac"
"encoding/base64"
"strconv"
"strings"
"time"
)
// Purpose-scoped tokens (email verification, password reset) reuse the session
// Signer's HMAC secret but bind a purpose + subject, so a token minted for one
// flow cannot be replayed in another. Format:
//
// base64url(<purpose> NUL <subject> NUL <expUnix>) "." base64url(hmac)
//
// subject is the normalized email. Like session tokens these are signed, not
// encrypted: they carry no secret, only a purpose, an email and an expiry, and
// the HMAC makes them tamper-evident.
const (
purposeVerifyEmail = "verify"
purposePasswordReset = "reset"
)
// IssuePurpose returns a signed, purpose-bound token for subject that expires
// after ttl.
func (s *Signer) IssuePurpose(purpose, subject string, ttl time.Duration) string {
exp := time.Now().Add(ttl).Unix()
payload := purpose + "\x00" + subject + "\x00" + strconv.FormatInt(exp, 10)
b := base64.RawURLEncoding.EncodeToString([]byte(payload))
return b + "." + s.sign(b)
}
// ParsePurpose verifies a purpose token and returns its subject. It requires the
// embedded purpose to match want and the token to be unexpired, returning
// ErrInvalidToken for a bad signature/format/purpose and ErrExpiredToken when
// the token has expired.
func (s *Signer) ParsePurpose(want, token string) (string, error) {
b, sig, ok := strings.Cut(token, ".")
if !ok || b == "" || sig == "" {
return "", ErrInvalidToken
}
if !hmac.Equal([]byte(sig), []byte(s.sign(b))) {
return "", ErrInvalidToken
}
raw, err := base64.RawURLEncoding.DecodeString(b)
if err != nil {
return "", ErrInvalidToken
}
parts := strings.Split(string(raw), "\x00")
if len(parts) != 3 || parts[0] != want {
return "", ErrInvalidToken
}
exp, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return "", ErrInvalidToken
}
if time.Now().Unix() >= exp {
return "", ErrExpiredToken
}
return parts[1], nil
}