feat(auth): JWT(HS256 stdlib) + RBAC(owner/admin/viewer) + API key + 密码 hash;config 校验
This commit is contained in:
1 parent
7f7cb8fbf7
commit
a3f6f4f687
10 files changed
+1116
No files matched your search
@@ -0,0 +1,144 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
)
|
||||
|
||||
// Claims is the verified JWT payload.
|
||||
type Claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Role Role `json:"role"`
|
||||
Exp int64 `json:"exp"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
}
|
||||
|
||||
// jwtHeader is the JOSE header for the HS256 tokens this package emits.
|
||||
type jwtHeader struct {
|
||||
Alg string `json:"alg"`
|
||||
Typ string `json:"typ"`
|
||||
}
|
||||
|
||||
// JWTService signs and verifies HS256 JWTs using a shared secret.
|
||||
type JWTService struct {
|
||||
secret []byte
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// New validates cfg and returns a JWTService. It panics when JWTSecret is
|
||||
// shorter than 32 characters so a misconfiguration fails fast at startup.
|
||||
func New(cfg config.AuthConfig) *JWTService {
|
||||
if err := Validate(cfg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &JWTService{
|
||||
secret: []byte(cfg.JWTSecret),
|
||||
log: logger.WithSource(slog.Default(), source),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that cfg satisfies the auth package's configuration
|
||||
// requirements, returning ErrInvalidSecret when JWTSecret is too short.
|
||||
func Validate(cfg config.AuthConfig) error {
|
||||
if len(cfg.JWTSecret) < minSecretLen {
|
||||
return ErrInvalidSecret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign issues an HS256 token for sub with the given role and lifetime.
|
||||
func (s *JWTService) Sign(sub string, role Role, ttl time.Duration) (string, error) {
|
||||
if !role.Valid() {
|
||||
return "", fmt.Errorf("%w: %q", ErrInvalidRole, role)
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
claims := Claims{
|
||||
Sub: sub,
|
||||
Role: role,
|
||||
IssuedAt: now,
|
||||
Exp: now + int64(ttl.Seconds()),
|
||||
}
|
||||
headerB64, err := marshalB64(jwtHeader{Alg: "HS256", Typ: "JWT"})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth: encode header: %w", err)
|
||||
}
|
||||
payloadB64, err := marshalB64(claims)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth: encode payload: %w", err)
|
||||
}
|
||||
signingInput := headerB64 + "." + payloadB64
|
||||
sig := hmacSign(s.secret, []byte(signingInput))
|
||||
return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil
|
||||
}
|
||||
|
||||
// Verify validates the token signature, algorithm, role, and expiry, returning
|
||||
// the parsed Claims on success.
|
||||
func (s *JWTService) Verify(token string) (*Claims, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("%w: expected 3 segments", ErrMalformedToken)
|
||||
}
|
||||
headerB64, payloadB64, sigB64 := parts[0], parts[1], parts[2]
|
||||
signingInput := headerB64 + "." + payloadB64
|
||||
|
||||
provided, err := base64.RawURLEncoding.DecodeString(sigB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: decode signature: %v", ErrMalformedToken, err)
|
||||
}
|
||||
if !hmac.Equal(provided, hmacSign(s.secret, []byte(signingInput))) {
|
||||
return nil, ErrInvalidSignature
|
||||
}
|
||||
|
||||
headerJSON, err := base64.RawURLEncoding.DecodeString(headerB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: decode header: %v", ErrMalformedToken, err)
|
||||
}
|
||||
var hdr jwtHeader
|
||||
if err := json.Unmarshal(headerJSON, &hdr); err != nil {
|
||||
return nil, fmt.Errorf("%w: decode header: %v", ErrMalformedToken, err)
|
||||
}
|
||||
if hdr.Alg != "HS256" {
|
||||
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlg, hdr.Alg)
|
||||
}
|
||||
|
||||
payloadJSON, err := base64.RawURLEncoding.DecodeString(payloadB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: decode payload: %v", ErrMalformedToken, err)
|
||||
}
|
||||
var claims Claims
|
||||
if err := json.Unmarshal(payloadJSON, &claims); err != nil {
|
||||
return nil, fmt.Errorf("%w: decode payload: %v", ErrMalformedToken, err)
|
||||
}
|
||||
if !claims.Role.Valid() {
|
||||
return nil, fmt.Errorf("%w: %q", ErrInvalidRole, claims.Role)
|
||||
}
|
||||
if claims.Exp > 0 && time.Now().Unix() >= claims.Exp {
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// marshalB64 JSON-encodes v and base64url-encodes the result without padding.
|
||||
func marshalB64(v any) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// hmacSign returns the HS256 HMAC of input under secret.
|
||||
func hmacSign(secret, input []byte) []byte {
|
||||
h := hmac.New(sha256.New, secret)
|
||||
h.Write(input)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
Reference in new issue
Block a user