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