Archived
重构 JWT 与 Transit 签名流程
This commit is contained in:
+68
-65
@@ -2,18 +2,19 @@ package signing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
const maximumTTL = 5 * time.Minute
|
||||
|
||||
type RS256Signer interface {
|
||||
ActiveKey(ctx context.Context) (SigningKey, error)
|
||||
SignRS256(ctx context.Context, key SigningKey, signingInput []byte) ([]byte, error)
|
||||
ActiveKey(context.Context) (SigningKey, error)
|
||||
SignRS256(context.Context, SigningKey, []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type SigningKey struct {
|
||||
@@ -21,6 +22,14 @@ type SigningKey struct {
|
||||
Version int
|
||||
}
|
||||
|
||||
type IssueRequest struct {
|
||||
Subject string
|
||||
PrincipalName string
|
||||
Audience string
|
||||
JWTID string
|
||||
Scope string
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
@@ -34,22 +43,29 @@ type Claims struct {
|
||||
}
|
||||
|
||||
type Issuer struct {
|
||||
issuer string
|
||||
ttl time.Duration
|
||||
signer RS256Signer
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewIssuer(signer RS256Signer) (*Issuer, error) {
|
||||
if signer == nil {
|
||||
return nil, errors.New("signer is required")
|
||||
func NewIssuer(issuer string, ttl time.Duration, signer RS256Signer) (*Issuer, error) {
|
||||
if issuer == "" || signer == nil {
|
||||
return nil, errors.New("issuer and signer are required")
|
||||
}
|
||||
return &Issuer{signer: signer, now: time.Now}, nil
|
||||
if ttl <= 0 || ttl > maximumTTL {
|
||||
return nil, errors.New("TTL must be between zero and five minutes")
|
||||
}
|
||||
return &Issuer{issuer: issuer, ttl: ttl, signer: signer}, nil
|
||||
}
|
||||
|
||||
func (i *Issuer) Sign(ctx context.Context, claims Claims) (string, error) {
|
||||
if err := validateClaims(claims, i.now()); err != nil {
|
||||
func (i *Issuer) Issue(ctx context.Context, request IssueRequest) (string, error) {
|
||||
return i.issueAt(ctx, request, time.Now())
|
||||
}
|
||||
|
||||
func (i *Issuer) issueAt(ctx context.Context, request IssueRequest, now time.Time) (string, error) {
|
||||
if err := validateIssueRequest(request); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, err := i.signer.ActiveKey(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("select signing key: %w", err)
|
||||
@@ -58,63 +74,50 @@ func (i *Issuer) Sign(ctx context.Context, claims Claims) (string, error) {
|
||||
return "", errors.New("signer returned an invalid signing key")
|
||||
}
|
||||
|
||||
claims := Claims{
|
||||
Issuer: i.issuer,
|
||||
Subject: request.Subject,
|
||||
PrincipalName: request.PrincipalName,
|
||||
Audience: []string{request.Audience},
|
||||
IssuedAt: now.Unix(),
|
||||
NotBefore: now.Unix(),
|
||||
ExpiresAt: now.Add(i.ttl).Unix(),
|
||||
JWTID: request.JWTID,
|
||||
Scope: request.Scope,
|
||||
}
|
||||
signingInput, err := encodeSigningInput(key.ID, claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signature, err := i.signer.SignRS256(ctx, key, []byte(signingInput))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("sign JWT: %w", err)
|
||||
}
|
||||
return serializeCompact(signingInput, signature), nil
|
||||
}
|
||||
|
||||
func validateIssueRequest(request IssueRequest) error {
|
||||
if request.Subject == "" || request.Audience == "" || request.JWTID == "" || request.Scope == "" {
|
||||
return errors.New("required JWT input is missing")
|
||||
}
|
||||
if strings.ContainsAny(request.Subject, "\r\n") || strings.ContainsAny(request.JWTID, "\r\n") {
|
||||
return errors.New("invalid JWT input")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSigningInput(keyID string, claims Claims) (string, error) {
|
||||
header, err := json.Marshal(map[string]string{"alg": "RS256", "kid": keyID, "typ": "at+jwt"})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode protected header: %w", err)
|
||||
}
|
||||
payload, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode claims: %w", err)
|
||||
}
|
||||
|
||||
opaque := &contextSigner{ctx: ctx, signer: i.signer, key: key}
|
||||
options := (&jose.SignerOptions{}).
|
||||
WithType(jose.ContentType("at+jwt")).
|
||||
WithHeader(jose.HeaderKey("kid"), key.ID)
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: opaque}, options)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create JWT signer: %w", err)
|
||||
}
|
||||
jws, err := signer.Sign(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("sign JWT: %w", err)
|
||||
}
|
||||
compact, err := jws.CompactSerialize()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("serialize JWT: %w", err)
|
||||
}
|
||||
return compact, nil
|
||||
return base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(payload), nil
|
||||
}
|
||||
|
||||
type contextSigner struct {
|
||||
ctx context.Context
|
||||
signer RS256Signer
|
||||
key SigningKey
|
||||
}
|
||||
|
||||
func (*contextSigner) Public() *jose.JSONWebKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*contextSigner) Algs() []jose.SignatureAlgorithm {
|
||||
return []jose.SignatureAlgorithm{jose.RS256}
|
||||
}
|
||||
|
||||
func (s *contextSigner) SignPayload(payload []byte, algorithm jose.SignatureAlgorithm) ([]byte, error) {
|
||||
if algorithm != jose.RS256 {
|
||||
return nil, errors.New("unsupported signing algorithm")
|
||||
}
|
||||
return s.signer.SignRS256(s.ctx, s.key, payload)
|
||||
}
|
||||
|
||||
func validateClaims(claims Claims, now time.Time) error {
|
||||
if claims.Issuer == "" || claims.Subject == "" || len(claims.Audience) != 1 || claims.JWTID == "" || claims.Scope == "" {
|
||||
return errors.New("required JWT claim is missing")
|
||||
}
|
||||
if strings.ContainsAny(claims.Subject, "\r\n") || strings.ContainsAny(claims.JWTID, "\r\n") {
|
||||
return errors.New("invalid JWT claim")
|
||||
}
|
||||
if claims.ExpiresAt <= claims.NotBefore || claims.NotBefore < claims.IssuedAt {
|
||||
return errors.New("invalid JWT time window")
|
||||
}
|
||||
if time.Unix(claims.ExpiresAt, 0).After(now.Add(5 * time.Minute)) {
|
||||
return errors.New("JWT lifetime exceeds five minutes")
|
||||
}
|
||||
return nil
|
||||
func serializeCompact(signingInput string, signature []byte) string {
|
||||
return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user