mirror of
https://github.com/openziti/ziti.git
synced 2026-09-24 19:32:22 +00:00
790 lines
23 KiB
Go
790 lines
23 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package model
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-openapi/jsonpointer"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/michaelquigley/pfxlog"
|
|
nfPem "github.com/openziti/foundation/v2/pem"
|
|
"github.com/openziti/foundation/v2/stringz"
|
|
"github.com/openziti/jwks"
|
|
"github.com/openziti/storage/boltz"
|
|
"github.com/openziti/ziti/v2/controller/apierror"
|
|
"github.com/openziti/ziti/v2/controller/db"
|
|
cmap "github.com/orcaman/concurrent-map/v2"
|
|
"go.etcd.io/bbolt"
|
|
)
|
|
|
|
// TokenIssuerCache maintains a cache of available JWT token issuers.
|
|
// It handles discovery, validation, and caching of public keys for token verification.
|
|
// Listens for database events to keep cached state synchronized.
|
|
type TokenIssuerCache struct {
|
|
issuers cmap.ConcurrentMap[string, TokenIssuer]
|
|
env Env
|
|
}
|
|
|
|
// NewTokenIssuerCache creates a new TokenIssuerCache and loads existing issuers from the database.
|
|
// Registers listeners for issuer creation, update, and deletion events.
|
|
func NewTokenIssuerCache(env Env) *TokenIssuerCache {
|
|
result := &TokenIssuerCache{
|
|
env: env,
|
|
issuers: cmap.New[TokenIssuer](),
|
|
}
|
|
|
|
env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtCreate, boltz.EntityCreatedAsync)
|
|
env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtUpdate, boltz.EntityUpdatedAsync)
|
|
env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtDelete, boltz.EntityDeletedAsync)
|
|
|
|
result.loadExisting()
|
|
|
|
return result
|
|
}
|
|
|
|
// onExtJwtCreate handles creation of new external JWT signers.
|
|
// Creates a TokenIssuerExtJwt, resolves keys from certificate or JWKS endpoint, and caches it.
|
|
func (a *TokenIssuerCache) onExtJwtCreate(signer *db.ExternalJwtSigner) {
|
|
logger := pfxlog.Logger().WithFields(map[string]interface{}{
|
|
"id": signer.Id,
|
|
"name": signer.Name,
|
|
"hasCertPem": signer.CertPem != nil,
|
|
"jwksEndpoint": signer.JwksEndpoint,
|
|
})
|
|
|
|
if signer.Issuer == nil {
|
|
logger.Error("could not add signer, issuer is nil")
|
|
return
|
|
}
|
|
|
|
signerRec := &TokenIssuerExtJwt{
|
|
externalJwtSigner: signer,
|
|
jwksResolver: &jwks.HttpResolver{},
|
|
kidToPubKey: map[string]IssuerPublicKey{},
|
|
}
|
|
|
|
if err := signerRec.Resolve(false); err != nil {
|
|
logger.WithError(err).Error("could not resolve signer cert/jwks")
|
|
}
|
|
|
|
a.issuers.Set(*signer.Issuer, signerRec)
|
|
|
|
}
|
|
|
|
// onExtJwtUpdate handles updates to existing external JWT signers.
|
|
// Reloads the signer from database and refreshes the cache entry.
|
|
func (a *TokenIssuerCache) onExtJwtUpdate(signer *db.ExternalJwtSigner) {
|
|
//read on update because patches can pass partial data
|
|
err := a.env.GetDb().View(func(tx *bbolt.Tx) error {
|
|
var err error
|
|
signer, _, err = a.env.GetStores().ExternalJwtSigner.FindById(tx, signer.Id)
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
pfxlog.Logger().Errorf("error on external signature update for authentication module %T: could not read entity: %v", a, err)
|
|
}
|
|
|
|
a.onExtJwtCreate(signer)
|
|
}
|
|
|
|
// onExtJwtDelete handles deletion of external JWT signers.
|
|
// Removes the signer from the cache.
|
|
func (a *TokenIssuerCache) onExtJwtDelete(signer *db.ExternalJwtSigner) {
|
|
logger := pfxlog.Logger().WithFields(map[string]interface{}{
|
|
"id": signer.Id,
|
|
"name": signer.Name,
|
|
"hasCertPem": signer.CertPem != nil,
|
|
"jwksEndpoint": signer.JwksEndpoint,
|
|
})
|
|
|
|
if signer.Issuer == nil {
|
|
logger.Error("could not add signer, issuer is nil")
|
|
return
|
|
}
|
|
|
|
a.issuers.Remove(*signer.Issuer)
|
|
}
|
|
|
|
// loadExisting loads all external JWT signers from the database during initialization.
|
|
func (a *TokenIssuerCache) loadExisting() {
|
|
err := a.env.GetDb().View(func(tx *bbolt.Tx) error {
|
|
ids, _, err := a.env.GetStores().ExternalJwtSigner.QueryIds(tx, "")
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, id := range ids {
|
|
signer, err := a.env.GetStores().ExternalJwtSigner.LoadById(tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.onExtJwtCreate(signer)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
pfxlog.Logger().Errorf("error loading external jwt signerByIssuer: %v", err)
|
|
}
|
|
}
|
|
|
|
// GetByIssuerString returns the TokenIssuer for the given issuer claim string.
|
|
// Returns nil if no issuer is found.
|
|
func (a *TokenIssuerCache) GetByIssuerString(issuer string) TokenIssuer {
|
|
tokenIssuer, ok := a.issuers.Get(issuer)
|
|
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return tokenIssuer
|
|
}
|
|
|
|
// GetById returns the TokenIssuer with the given ID.
|
|
// Returns nil if no issuer is found.
|
|
func (a *TokenIssuerCache) GetById(issuerId string) TokenIssuer {
|
|
for _, issuer := range a.issuers.Items() {
|
|
if issuer.Id() == issuerId {
|
|
return issuer
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetIssuerStrings returns a list of all known issuer strings.
|
|
func (a *TokenIssuerCache) GetIssuerStrings() []string {
|
|
return a.issuers.Keys()
|
|
}
|
|
|
|
// pubKeyLookup is a callback for JWT parsing to resolve the public key for signature verification.
|
|
// Locates the token issuer, validates its configuration, and returns the corresponding public key.
|
|
func (a *TokenIssuerCache) pubKeyLookup(token *jwt.Token) (interface{}, error) {
|
|
logger := pfxlog.Logger().Entry
|
|
|
|
kidVal, ok := token.Header["kid"]
|
|
|
|
if !ok {
|
|
logger.Error("missing kid")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
kid, ok := kidVal.(string)
|
|
|
|
if !ok {
|
|
logger.Error("kid is not a string")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
if kid == "" {
|
|
logger.Error("kid is empty")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
logger = logger.WithField("kid", kid)
|
|
|
|
claims := token.Claims.(jwt.MapClaims)
|
|
if claims == nil {
|
|
logger.Error("unknown signer, attempting to look up by claims, but claims were nil")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
issuer, err := token.Claims.GetIssuer()
|
|
if err != nil {
|
|
logger.WithError(err).Error("unknown signer, attempting to retrieve issuer claim from token failed")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
if issuer == "" {
|
|
logger.Error("unknown signer, attempting to look up by issue, but issuer is empty or not a string")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
logger = logger.WithField("issuer", issuer)
|
|
|
|
tokenIssuer := a.GetByIssuerString(issuer)
|
|
|
|
if tokenIssuer == nil {
|
|
issuers := a.GetIssuerStrings()
|
|
logger.WithField("knownIssuers", issuers).Error("issuer not found, issuers are bit-for-bit compared, they must match exactly")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
logger = logger.WithField("issuerType", tokenIssuer.TypeName()).WithField("tokenIssuerName", tokenIssuer.Name())
|
|
|
|
if !tokenIssuer.IsEnabled() {
|
|
logger.Error("token issuer is disabled")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
audiences, err := token.Claims.GetAudience()
|
|
|
|
if err != nil {
|
|
logger.WithError(err).Error("could not retrieve audience values from token")
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
audienceFound := false
|
|
for _, audience := range audiences {
|
|
if audience == tokenIssuer.ExpectedAudience() {
|
|
audienceFound = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !audienceFound {
|
|
logger.Errorf("token audience does not match expected audience, expected %s, got %s", tokenIssuer.ExpectedAudience(), audiences)
|
|
return nil, apierror.NewInvalidAuth()
|
|
}
|
|
|
|
_ = tokenIssuer.Resolve(false)
|
|
key, ok := tokenIssuer.PubKeyByKid(kid)
|
|
|
|
if !ok {
|
|
if err := tokenIssuer.Resolve(true); err != nil {
|
|
logger.WithError(err).Error("error attempting to resolve extJwtSigner certificate used for signing")
|
|
}
|
|
|
|
key, ok = tokenIssuer.PubKeyByKid(kid)
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("kid [%s] not found for issuer [%s]", kid, issuer)
|
|
}
|
|
}
|
|
|
|
claims[InternalTokenIssuerClaim] = tokenIssuer
|
|
|
|
return key.PubKey, nil
|
|
}
|
|
|
|
// VerifyTokenByInspection verifies a JWT by examining its issuer claim to locate the appropriate issuer.
|
|
// Parses the token and validates signature, audience, and extracts identity/name/attribute claims.
|
|
// Returns the verification result containing extracted claims and the verifying issuer.
|
|
func (a *TokenIssuerCache) VerifyTokenByInspection(candidateToken string) (*TokenVerificationResult, error) {
|
|
|
|
claims := jwt.MapClaims{}
|
|
|
|
token, err := jwt.ParseWithClaims(candidateToken, claims, a.pubKeyLookup)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse token: %w", err)
|
|
}
|
|
|
|
if !token.Valid {
|
|
return nil, errors.New("token is not valid")
|
|
}
|
|
|
|
tokenIssuer := claims[InternalTokenIssuerClaim].(TokenIssuer)
|
|
|
|
if tokenIssuer == nil {
|
|
return nil, errors.New("token issuer is nil")
|
|
}
|
|
|
|
if !tokenIssuer.IsEnabled() {
|
|
return nil, errors.New("token issuer is disabled")
|
|
}
|
|
|
|
idClaimValue, err := resolveStringClaimSelector(claims, tokenIssuer.IdentityIdClaimsSelector())
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not resolve identity claim property %s: %w", tokenIssuer.IdentityIdClaimsSelector(), err)
|
|
}
|
|
|
|
attributeSelector := tokenIssuer.EnrollmentAttributeClaimsSelector()
|
|
var attributeClaimValues []string
|
|
if attributeSelector != "" {
|
|
attributeClaimValues, err = resolveStringSliceClaimProperty(claims, attributeSelector)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not resolve attribute claim property %s: %w", tokenIssuer.EnrollmentAttributeClaimsSelector(), err)
|
|
}
|
|
}
|
|
|
|
if attributeClaimValues == nil {
|
|
attributeClaimValues = []string{}
|
|
}
|
|
|
|
nameSelector := tokenIssuer.EnrollmentNameClaimSelector()
|
|
nameClaimValue := ""
|
|
|
|
if nameSelector != "" {
|
|
nameClaimValue, err = resolveStringClaimSelector(claims, nameSelector)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not resolve name claim property %s: %w", tokenIssuer.EnrollmentNameClaimSelector(), err)
|
|
}
|
|
}
|
|
|
|
return &TokenVerificationResult{
|
|
TokenIssuer: tokenIssuer,
|
|
Token: token,
|
|
Claims: claims,
|
|
IdClaimSelector: tokenIssuer.IdentityIdClaimsSelector(),
|
|
IdClaimValue: idClaimValue,
|
|
AttributeClaimSelector: tokenIssuer.EnrollmentAttributeClaimsSelector(),
|
|
AttributeClaimValue: attributeClaimValues,
|
|
NameClaimSelector: tokenIssuer.EnrollmentNameClaimSelector(),
|
|
NameClaimValue: nameClaimValue,
|
|
}, nil
|
|
}
|
|
|
|
// TokenVerificationResult contains the result of JWT token verification.
|
|
// Includes the parsed token, extracted claims, and the issuer that verified it.
|
|
type TokenVerificationResult struct {
|
|
TokenIssuer TokenIssuer
|
|
Token *jwt.Token
|
|
Claims map[string]any
|
|
IdClaimSelector string
|
|
IdClaimValue string
|
|
AttributeClaimSelector string
|
|
AttributeClaimValue []string
|
|
NameClaimSelector string
|
|
NameClaimValue string
|
|
}
|
|
|
|
// IsValid returns true if the JWT token signature is valid.
|
|
func (r *TokenVerificationResult) IsValid() bool {
|
|
return r.Token.Valid
|
|
}
|
|
|
|
// TokenIssuer represents a JWT token issuer capable of verifying tokens.
|
|
// Implementations provide token verification, configuration queries, and claim extraction.
|
|
type TokenIssuer interface {
|
|
// Id returns the unique identifier of this token issuer.
|
|
Id() string
|
|
// TypeName returns the type name of the token issuer (e.g., "externalJwtSigner").
|
|
TypeName() string
|
|
// Name returns the human-readable name of the token issuer.
|
|
Name() string
|
|
// IsEnabled returns true if this token issuer is enabled.
|
|
IsEnabled() bool
|
|
|
|
// PubKeyByKid returns the public key for the given key ID.
|
|
PubKeyByKid(kid string) (IssuerPublicKey, bool)
|
|
// Resolve loads the issuer's public keys from certificate or JWKS endpoint.
|
|
// If force is true, refreshes cached keys even if already loaded.
|
|
Resolve(force bool) error
|
|
|
|
// ExpectedIssuer returns the issuer claim value that tokens from this issuer should contain.
|
|
ExpectedIssuer() string
|
|
// ExpectedAudience returns the audience claim value that tokens should contain.
|
|
ExpectedAudience() string
|
|
|
|
// AuthenticatorId returns the authenticator ID for this issuer.
|
|
AuthenticatorId() string
|
|
|
|
// EnrollmentAuthPolicyId returns the auth policy ID to apply to identities enrolled via this issuer.
|
|
EnrollmentAuthPolicyId() string
|
|
// EnrollmentAttributeClaimsSelector returns the JSON pointer path to the attributes claim.
|
|
EnrollmentAttributeClaimsSelector() string
|
|
// EnrollmentNameClaimSelector returns the JSON pointer path to the identity name claim.
|
|
EnrollmentNameClaimSelector() string
|
|
|
|
// IdentityIdClaimsSelector returns the JSON pointer path to the identity ID claim.
|
|
IdentityIdClaimsSelector() string
|
|
|
|
// UseExternalId returns true if the identity ID should be stored as an external ID.
|
|
UseExternalId() bool
|
|
|
|
// VerifyToken verifies and parses a JWT token, returning the verification result.
|
|
VerifyToken(token string) (*TokenVerificationResult, error)
|
|
|
|
// EnrollToCertEnabled returns true if enrollment to certificate is allowed.
|
|
EnrollToCertEnabled() bool
|
|
// EnrollToTokenEnabled returns true if enrollment to token is allowed.
|
|
EnrollToTokenEnabled() bool
|
|
}
|
|
|
|
var _ TokenIssuer = (*TokenIssuerExtJwt)(nil)
|
|
|
|
// TokenIssuerExtJwt is a TokenIssuer implementation for external JWT signers.
|
|
// Supports verification via certificate PEM or JWKS endpoints.
|
|
type TokenIssuerExtJwt struct {
|
|
sync.Mutex
|
|
jwksLastRequest time.Time
|
|
|
|
kidToPubKey map[string]IssuerPublicKey
|
|
jwksResponse *jwks.Response
|
|
externalJwtSigner *db.ExternalJwtSigner
|
|
|
|
jwksResolver jwks.Resolver
|
|
}
|
|
|
|
// EnrollToCertEnabled returns true if this issuer allows enrollment to certificate.
|
|
func (r *TokenIssuerExtJwt) EnrollToCertEnabled() bool {
|
|
return r.externalJwtSigner.EnrollToCertEnabled
|
|
}
|
|
|
|
// EnrollToTokenEnabled returns true if this issuer allows enrollment to token authentication.
|
|
func (r *TokenIssuerExtJwt) EnrollToTokenEnabled() bool {
|
|
return r.externalJwtSigner.EnrollToTokenEnabled
|
|
}
|
|
|
|
// EnrollmentAttributeClaimsSelector returns the JSON pointer path to the attributes claim.
|
|
func (r *TokenIssuerExtJwt) EnrollmentAttributeClaimsSelector() string {
|
|
return r.externalJwtSigner.EnrollAttributeClaimsSelector
|
|
}
|
|
|
|
// EnrollmentNameClaimSelector returns the JSON pointer path to the identity name claim.
|
|
func (r *TokenIssuerExtJwt) EnrollmentNameClaimSelector() string {
|
|
return r.externalJwtSigner.EnrollNameClaimSelector
|
|
}
|
|
|
|
// EnrollmentAuthPolicyId returns the auth policy ID to apply to enrolled identities.
|
|
func (r *TokenIssuerExtJwt) EnrollmentAuthPolicyId() string {
|
|
return r.externalJwtSigner.EnrollAuthPolicyId
|
|
}
|
|
|
|
// VerifyToken verifies a JWT using this issuer's configuration.
|
|
// Attempts resolution of keys if not already cached before verification.
|
|
func (r *TokenIssuerExtJwt) VerifyToken(token string) (*TokenVerificationResult, error) {
|
|
err := r.Resolve(false)
|
|
|
|
pfxlog.Logger().WithError(err).Warn("error during routine resolve of external jwt signer cert/jwks, attempting to verify the token with any cached keys")
|
|
|
|
claims := jwt.MapClaims{}
|
|
resultToken, err := jwt.ParseWithClaims(token, claims, r.keyFunc)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !resultToken.Valid {
|
|
return nil, errors.New("token is not valid")
|
|
}
|
|
|
|
idClaimValue, err := resolveStringClaimSelector(claims, r.IdentityIdClaimsSelector())
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not resolve id claim property: %w", err)
|
|
}
|
|
|
|
return &TokenVerificationResult{
|
|
TokenIssuer: r,
|
|
Token: resultToken,
|
|
Claims: claims,
|
|
IdClaimSelector: r.IdentityIdClaimsSelector(),
|
|
IdClaimValue: idClaimValue,
|
|
}, nil
|
|
}
|
|
|
|
// resolveStringSliceClaimProperty extracts a string or string array from JWT claims using a JSON pointer.
|
|
// Returns a string slice even if the claim is a single string value.
|
|
func resolveStringSliceClaimProperty(claims jwt.MapClaims, property string) ([]string, error) {
|
|
if property == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
if !strings.HasPrefix(property, "/") {
|
|
property = "/" + property
|
|
}
|
|
|
|
jsonPointer, err := jsonpointer.New(property)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse json pointer: %s: %w", property, err)
|
|
}
|
|
|
|
val, _, err := jsonPointer.Get(claims)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not resolve json pointer: %s: %w", property, err)
|
|
}
|
|
|
|
strVal, ok := val.(string)
|
|
|
|
if ok {
|
|
if strVal != "" {
|
|
return []string{strVal}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
arrVals, ok := val.([]any)
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("could not resolve json pointer: %s: value is not a string or an array of strings, got: %v", property, arrVals)
|
|
}
|
|
|
|
var attributes []string
|
|
for i, arrVal := range arrVals {
|
|
arrStrVal, ok := arrVal.(string)
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("could not resolve json point %s as array of strings at index %d: %v: value is not a string", property, i, arrVal)
|
|
}
|
|
|
|
if arrStrVal != "" {
|
|
attributes = append(attributes, arrStrVal)
|
|
}
|
|
}
|
|
|
|
return attributes, nil
|
|
}
|
|
|
|
// resolveStringClaimSelector extracts a string value from JWT claims using a JSON pointer.
|
|
func resolveStringClaimSelector(claims jwt.MapClaims, property string) (string, error) {
|
|
if property == "" {
|
|
return "", nil
|
|
}
|
|
|
|
if !strings.HasPrefix(property, "/") {
|
|
property = "/" + property
|
|
}
|
|
|
|
jsonPointer, err := jsonpointer.New(property)
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not parse json pointer: %s: %w", property, err)
|
|
}
|
|
|
|
val, _, err := jsonPointer.Get(claims)
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not resolve json pointer: %s: %w", property, err)
|
|
}
|
|
|
|
strVal, ok := val.(string)
|
|
|
|
if !ok {
|
|
return "", fmt.Errorf("could not resolve json pointer: %s: value is not a string", property)
|
|
}
|
|
|
|
return strVal, nil
|
|
}
|
|
|
|
// UseExternalId returns true if the identity ID should be stored as an external ID.
|
|
func (r *TokenIssuerExtJwt) UseExternalId() bool {
|
|
return r.externalJwtSigner.UseExternalId
|
|
}
|
|
|
|
// IdentityIdClaimsSelector returns the JSON pointer path to the identity ID claim.
|
|
// Defaults to the standard identity ID claims selector if not configured.
|
|
func (r *TokenIssuerExtJwt) IdentityIdClaimsSelector() string {
|
|
ret := stringz.OrEmpty(r.externalJwtSigner.IdentityIdClaimsSelector)
|
|
|
|
if ret == "" {
|
|
return db.DefaultIdentityIdClaimsSelector
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// AuthenticatorId returns an authenticator ID specific to this token issuer.
|
|
func (r *TokenIssuerExtJwt) AuthenticatorId() string {
|
|
return "extJwtId:" + r.externalJwtSigner.Id
|
|
}
|
|
|
|
// IsEnabled returns true if this token issuer is enabled.
|
|
func (r *TokenIssuerExtJwt) IsEnabled() bool {
|
|
return r.externalJwtSigner.Enabled
|
|
}
|
|
|
|
// Name returns the human-readable name of this token issuer.
|
|
func (r *TokenIssuerExtJwt) Name() string {
|
|
return r.externalJwtSigner.Name
|
|
}
|
|
|
|
// ExpectedIssuer returns the issuer claim value that tokens should contain.
|
|
func (r *TokenIssuerExtJwt) ExpectedIssuer() string {
|
|
return stringz.OrEmpty(r.externalJwtSigner.Issuer)
|
|
}
|
|
|
|
// ExpectedAudience returns the audience claim value that tokens should contain.
|
|
func (r *TokenIssuerExtJwt) ExpectedAudience() string {
|
|
return stringz.OrEmpty(r.externalJwtSigner.Audience)
|
|
}
|
|
|
|
// TypeName returns the type name of this token issuer.
|
|
func (r *TokenIssuerExtJwt) TypeName() string {
|
|
return "externalJwtSigner"
|
|
}
|
|
|
|
// Id returns the unique identifier of this token issuer.
|
|
func (r *TokenIssuerExtJwt) Id() string {
|
|
return r.externalJwtSigner.Id
|
|
}
|
|
|
|
// keyFunc is a JWT parsing callback that resolves the public key for signature verification.
|
|
func (r *TokenIssuerExtJwt) keyFunc(token *jwt.Token) (any, error) {
|
|
kid, err := getJwtTokenKid(token)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not determine public key to use: %w", err)
|
|
}
|
|
|
|
if kid == "" {
|
|
return nil, fmt.Errorf("could not determine public key to use: kid is empty")
|
|
}
|
|
|
|
issuerPublicKey, ok := r.PubKeyByKid(kid)
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("no public key found for kid %s", kid)
|
|
}
|
|
|
|
return issuerPublicKey.PubKey, nil
|
|
}
|
|
|
|
// PubKeyByKid returns the public key for the given key ID from the cache.
|
|
// Thread-safe using internal mutex.
|
|
func (r *TokenIssuerExtJwt) PubKeyByKid(kid string) (IssuerPublicKey, bool) {
|
|
r.Mutex.Lock()
|
|
defer r.Mutex.Unlock()
|
|
|
|
key, ok := r.kidToPubKey[kid]
|
|
|
|
return key, ok
|
|
}
|
|
|
|
// Resolve loads public keys from either the configured certificate PEM or JWKS endpoint.
|
|
// If force is true, refreshes keys even if already cached.
|
|
// Handles timeout caching for JWKS endpoint queries to avoid excessive network calls.
|
|
func (r *TokenIssuerExtJwt) Resolve(force bool) error {
|
|
r.Mutex.Lock()
|
|
defer r.Mutex.Unlock()
|
|
|
|
if r.externalJwtSigner.CertPem != nil {
|
|
if len(r.kidToPubKey) != 0 && !force {
|
|
return nil
|
|
}
|
|
|
|
certs := nfPem.PemStringToCertificates(*r.externalJwtSigner.CertPem)
|
|
|
|
if len(certs) == 0 {
|
|
return errors.New("could not add signer, PEM did not parse to any certificates")
|
|
}
|
|
|
|
kid := ""
|
|
|
|
if r.externalJwtSigner.Kid == nil {
|
|
kid = nfPem.FingerprintFromCertificate(certs[0])
|
|
pfxlog.Logger().WithField("id", r.externalJwtSigner.Id).WithField("name", r.externalJwtSigner.Name).Warnf("external jwt signer does not have a kid, generated: %s", kid)
|
|
} else {
|
|
kid = *r.externalJwtSigner.Kid
|
|
}
|
|
|
|
// first cert only
|
|
r.kidToPubKey = map[string]IssuerPublicKey{
|
|
kid: {
|
|
PubKey: certs[0].PublicKey,
|
|
Chain: certs,
|
|
},
|
|
}
|
|
|
|
return nil
|
|
|
|
} else if r.externalJwtSigner.JwksEndpoint != nil {
|
|
if (!r.jwksLastRequest.IsZero() && time.Since(r.jwksLastRequest) < JwksQueryTimeout) && !force {
|
|
return nil
|
|
}
|
|
|
|
r.jwksLastRequest = time.Now()
|
|
|
|
jwksResponse, _, err := r.jwksResolver.Get(*r.externalJwtSigner.JwksEndpoint)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not resolve jwks endpoint: %v", err)
|
|
}
|
|
|
|
for _, key := range jwksResponse.Keys {
|
|
//if we have an x509chain the first must be the signing key
|
|
if len(key.X509Chain) != 0 {
|
|
// x5c is the only attribute with padding according to
|
|
// RFC 7517 Section-4.7 "x5c" (X.509 Certificate Chain) Parameter
|
|
x509Der, err := base64.StdEncoding.DecodeString(key.X509Chain[0])
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse JWKS keys: %v", err)
|
|
}
|
|
|
|
certs, err := x509.ParseCertificates(x509Der)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse JWKS DER as x509: %v", err)
|
|
}
|
|
|
|
if len(certs) == 0 {
|
|
return fmt.Errorf("no ceritficates parsed")
|
|
}
|
|
|
|
r.kidToPubKey[key.KeyId] = IssuerPublicKey{
|
|
PubKey: certs[0].PublicKey,
|
|
Chain: certs,
|
|
}
|
|
} else {
|
|
//else the key properties are the only way to construct the public key
|
|
k, err := jwks.KeyToPublicKey(key)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.kidToPubKey[key.KeyId] = IssuerPublicKey{
|
|
PubKey: k,
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
r.jwksResponse = jwksResponse
|
|
|
|
return nil
|
|
}
|
|
|
|
return errors.New("instructed to add external jwt signer that does not have a certificate PEM or JWKS endpoint")
|
|
}
|
|
|
|
// IssuerPublicKey represents a public key and associated certificate chain.
|
|
type IssuerPublicKey struct {
|
|
PubKey any // The public key used for signature verification
|
|
Chain []*x509.Certificate // Optional X.509 certificate chain
|
|
}
|
|
|
|
// getJwtTokenKid extracts the key ID (kid) from a JWT token header.
|
|
func getJwtTokenKid(token *jwt.Token) (string, error) {
|
|
if token.Header == nil {
|
|
return "", errors.New("token header is nil")
|
|
}
|
|
|
|
kidVal, ok := token.Header["kid"]
|
|
|
|
if !ok || kidVal == nil {
|
|
return "", errors.New("token header kid value is nil or missing")
|
|
}
|
|
|
|
kid, ok := kidVal.(string)
|
|
|
|
if !ok || kid == "" {
|
|
return "", errors.New("token header kid value is empty or not a string")
|
|
}
|
|
|
|
return kid, nil
|
|
|
|
}
|