Files
Sylvain RabotandGitHub ceabd4cd1a refactor: replace github.com/pkg/errors with stdlib errors (#661)
Migrate all internal usages of github.com/pkg/errors to the Go standard
library: errors.New/Is/As map directly to the stdlib equivalents, while
errors.Wrap/Wrapf become fmt.Errorf(..., %w, err). github.com/pkg/errors
is no longer a direct dependency (remains indirect via watermill).

Constraint: errors.Wrap returns nil on nil input; fmt.Errorf does not, so
run.go's "executing migration" wrap was rewritten as an explicit nil check
Confidence: high
Scope-risk: narrow
2026-07-07 09:11:30 +00:00

107 lines
2.6 KiB
Go

package licence
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
type validateTokenOptions struct {
audience string
subject string
}
// ValidateTokenOption configures optional licence JWT claim checks.
type ValidateTokenOption func(*validateTokenOptions)
// WithAudience requires the licence JWT audience to contain audience.
func WithAudience(audience string) ValidateTokenOption {
return func(options *validateTokenOptions) {
options.audience = audience
}
}
// WithSubject requires the licence JWT subject to equal subject.
func WithSubject(subject string) ValidateTokenOption {
return func(options *validateTokenOptions) {
options.subject = subject
}
}
func getKeyFromEmbeddedPublicKey() (interface{}, error) {
block, _ := pem.Decode([]byte(formancePublicKey))
if block == nil {
return nil, fmt.Errorf("failed to decode embedded Formance public key")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse embedded Formance public key: %w", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("embedded Formance public key is not RSA")
}
return rsaPub, nil
}
// ValidateToken validates a licence JWT against the embedded Formance public key.
//
// By default it verifies the signing method, signature, issuer, and expiration.
// Audience and subject checks can be enabled with WithAudience and WithSubject.
func ValidateToken(jwtToken string, expectedIssuer string, opts ...ValidateTokenOption) error {
options := validateTokenOptions{}
for _, opt := range opts {
opt(&options)
}
parserOptions := []jwt.ParserOption{
jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
jwt.WithExpirationRequired(),
jwt.WithIssuer(expectedIssuer),
}
if options.audience != "" {
parserOptions = append(parserOptions, jwt.WithAudience(options.audience))
}
if options.subject != "" {
parserOptions = append(parserOptions, jwt.WithSubject(options.subject))
}
parser := jwt.NewParser(parserOptions...)
token, err := parser.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
return getKeyFromEmbeddedPublicKey()
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return fmt.Errorf("token has invalid claims: token is expired")
}
return err
}
if !token.Valid {
return fmt.Errorf("token is not valid")
}
return nil
}
func (l *Licence) validate() error {
if l.validateToken != nil {
return l.validateToken()
}
return ValidateToken(
l.jwtToken,
l.expectedIssuer,
WithAudience(l.serviceName),
WithSubject(l.clusterID),
)
}