Archived
121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
package signing
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-jose/go-jose/v4"
|
|
)
|
|
|
|
type RS256Signer interface {
|
|
ActiveKey(ctx context.Context) (SigningKey, error)
|
|
SignRS256(ctx context.Context, key SigningKey, signingInput []byte) ([]byte, error)
|
|
}
|
|
|
|
type SigningKey struct {
|
|
ID string
|
|
Version int
|
|
}
|
|
|
|
type Claims struct {
|
|
Issuer string `json:"iss"`
|
|
Subject string `json:"sub"`
|
|
PrincipalName string `json:"principal_name,omitempty"`
|
|
Audience []string `json:"aud"`
|
|
IssuedAt int64 `json:"iat"`
|
|
NotBefore int64 `json:"nbf"`
|
|
ExpiresAt int64 `json:"exp"`
|
|
JWTID string `json:"jti"`
|
|
Scope string `json:"scope"`
|
|
}
|
|
|
|
type Issuer struct {
|
|
signer RS256Signer
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewIssuer(signer RS256Signer) (*Issuer, error) {
|
|
if signer == nil {
|
|
return nil, errors.New("signer is required")
|
|
}
|
|
return &Issuer{signer: signer, now: time.Now}, nil
|
|
}
|
|
|
|
func (i *Issuer) Sign(ctx context.Context, claims Claims) (string, error) {
|
|
if err := validateClaims(claims, i.now()); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
key, err := i.signer.ActiveKey(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("select signing key: %w", err)
|
|
}
|
|
if key.ID == "" || key.Version < 1 {
|
|
return "", errors.New("signer returned an invalid signing key")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|