63 lines
1.9 KiB
Go
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
|
|
}
|