Files
descrybe/apps/api/internal/auth/tokens.go
T

70 lines
1.9 KiB
Go
Raw Normal View History

package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
var ErrTokenInvalid = errors.New("token invalid or expired")
// IssueSetPasswordToken creates a signed, time-limited token (no DB row).
// secret must come from env (TOKEN_SIGNING_SECRET); never commit secrets.
func IssueSetPasswordToken(secret string, userID uuid.UUID, ttl time.Duration) (string, error) {
if strings.TrimSpace(secret) == "" {
return "", errors.New("token signing secret not configured")
}
if ttl <= 0 {
ttl = 72 * time.Hour
}
exp := time.Now().Add(ttl).Unix()
nonce, err := RandomToken(8)
if err != nil {
return "", err
}
payload := fmt.Sprintf("%s.%d.%s", userID.String(), exp, nonce)
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(payload))
sig := hex.EncodeToString(mac.Sum(nil))
raw := payload + "." + sig
return base64.RawURLEncoding.EncodeToString([]byte(raw)), nil
}
func ParseSetPasswordToken(secret, token string) (uuid.UUID, error) {
if strings.TrimSpace(secret) == "" || strings.TrimSpace(token) == "" {
return uuid.Nil, ErrTokenInvalid
}
raw, err := base64.RawURLEncoding.DecodeString(token)
if err != nil {
return uuid.Nil, ErrTokenInvalid
}
parts := strings.Split(string(raw), ".")
if len(parts) != 4 {
return uuid.Nil, ErrTokenInvalid
}
userID, err := uuid.Parse(parts[0])
if err != nil {
return uuid.Nil, ErrTokenInvalid
}
exp, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil || time.Now().Unix() > exp {
return uuid.Nil, ErrTokenInvalid
}
payload := parts[0] + "." + parts[1] + "." + parts[2]
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(payload))
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(parts[3])) {
return uuid.Nil, ErrTokenInvalid
}
return userID, nil
}