90 lines
2.2 KiB
Go
90 lines
2.2 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const defaultTokenTTL = 24 * time.Hour
|
|
|
|
func hashPassword(password string) (string, error) {
|
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(bytes), nil
|
|
}
|
|
|
|
func checkPassword(hash, password string) error {
|
|
if strings.TrimSpace(hash) == "" {
|
|
return errors.New("password hash is empty")
|
|
}
|
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
|
}
|
|
|
|
func tokenSecret() string {
|
|
secret := strings.TrimSpace(os.Getenv("TOKEN_SECRET"))
|
|
if secret == "" {
|
|
return "local-dev-secret-change-me"
|
|
}
|
|
return secret
|
|
}
|
|
|
|
func tokenTTL() time.Duration {
|
|
raw := strings.TrimSpace(os.Getenv("TOKEN_TTL"))
|
|
if raw == "" {
|
|
return defaultTokenTTL
|
|
}
|
|
ttl, err := time.ParseDuration(raw)
|
|
if err != nil || ttl <= 0 {
|
|
return defaultTokenTTL
|
|
}
|
|
return ttl
|
|
}
|
|
|
|
func signToken(payload tokenPayload) (string, error) {
|
|
bytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
body := base64.RawURLEncoding.EncodeToString(bytes)
|
|
signature := computeTokenSignature(body)
|
|
return "fable-token." + body + "." + signature, nil
|
|
}
|
|
|
|
func parseToken(token string) (tokenPayload, error) {
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 3 || parts[0] != "fable-token" {
|
|
return tokenPayload{}, errors.New("invalid token format")
|
|
}
|
|
if !hmac.Equal([]byte(parts[2]), []byte(computeTokenSignature(parts[1]))) {
|
|
return tokenPayload{}, errors.New("invalid token signature")
|
|
}
|
|
bytes, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return tokenPayload{}, err
|
|
}
|
|
var payload tokenPayload
|
|
if err := json.Unmarshal(bytes, &payload); err != nil {
|
|
return tokenPayload{}, err
|
|
}
|
|
if payload.ExpiresAt > 0 && time.Now().UTC().Unix() > payload.ExpiresAt {
|
|
return tokenPayload{}, errors.New("token expired")
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func computeTokenSignature(body string) string {
|
|
mac := hmac.New(sha256.New, []byte(tokenSecret()))
|
|
_, _ = mac.Write([]byte(body))
|
|
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|