Files
ziti/controller/model/token_provider_cache.go
Paul Lorenz e38970803c Merge commit from fork
fixes GHSA-whjr-3j94-gw3c constrain external jwt signer JWKS fetching
2026-08-20 15:27:56 -04:00

1151 lines
34 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/sha1"
"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/ziti/v2/common"
"github.com/openziti/ziti/v2/controller/apierror"
"github.com/openziti/ziti/v2/controller/db"
"github.com/openziti/ziti/v2/controller/storage/boltz"
cmap "github.com/orcaman/concurrent-map/v2"
"go.etcd.io/bbolt"
)
var _ common.TokenIssuerCache = (*TokenIssuerCache)(nil)
// 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 {
// extJwt.Issuer -> extJwt issuer
externalIssuers cmap.ConcurrentMap[string, common.TokenIssuer]
// controller.Id -> controller issuer, controllers are stored by id
// as we are guarantee they have a KID and issuers may be variable
// due to xweb API address binds
controllerIssuers cmap.ConcurrentMap[string, common.TokenIssuer]
// jwksResolver fetches JWKS endpoints for external issuers. It is shared by every
// external issuer so that they all fetch under the same configured constraints.
jwksResolver *HardenedJwksResolver
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,
externalIssuers: cmap.New[common.TokenIssuer](),
controllerIssuers: cmap.New[common.TokenIssuer](),
jwksResolver: NewHardenedJwksResolver(JwksFetchConfig(env)),
}
env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtCreate, boltz.EntityCreatedAsync)
env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtUpdate, boltz.EntityUpdatedAsync)
env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtDelete, boltz.EntityDeletedAsync)
env.GetStores().Controller.AddEntityEventListenerF(result.onControllerCreate, boltz.EntityCreatedAsync)
env.GetStores().Controller.AddEntityEventListenerF(result.onControllerUpdate, boltz.EntityUpdatedAsync)
env.GetStores().Controller.AddEntityEventListenerF(result.onControllerDelete, boltz.EntityDeletedAsync)
result.loadExisting()
return result
}
func (a *TokenIssuerCache) onControllerCreate(controller *db.Controller) {
logger := pfxlog.Logger().WithFields(map[string]interface{}{
"id": controller.Id,
"name": controller.Name,
})
certs := nfPem.PemStringToCertificates(controller.CertPem)
if len(certs) == 0 {
logger.Error("could not add controller token issuer, no certificates found")
return
}
cert := certs[0]
controllerTokenIssuer := &ControllerTokenIssuer{
controllerId: controller.Id,
controllerName: controller.Name,
pubKey: common.IssuerPublicKey{
PubKey: cert.PublicKey,
Chain: certs},
kid: fmt.Sprintf("%x", sha1.Sum(cert.Raw)),
controllerIssuer: "",
}
a.controllerIssuers.Set(controller.Id, controllerTokenIssuer)
}
func (a *TokenIssuerCache) onControllerUpdate(controller *db.Controller) {
//read on update because patches can pass partial data
err := a.env.GetDb().View(func(tx *bbolt.Tx) error {
var err error
controller, _, err = a.env.GetStores().Controller.FindById(tx, controller.Id)
return err
})
if err != nil {
pfxlog.Logger().WithError(err).Error("error on controller update, could not update controller token issuer")
return
}
a.onControllerCreate(controller)
}
func (a *TokenIssuerCache) onControllerDelete(controller *db.Controller) {
a.controllerIssuers.Remove(controller.Id)
}
// addControllerIssuer creates a ControllerTokenIssuer from a model Controller and adds it to the cache.
func (a *TokenIssuerCache) addControllerIssuer(controller *Controller) {
logger := pfxlog.Logger().WithFields(map[string]interface{}{
"id": controller.Id,
"name": controller.Name,
})
certs := nfPem.PemStringToCertificates(controller.CertPem)
if len(certs) == 0 {
logger.Error("could not add controller token issuer, no certificates found")
return
}
cert := certs[0]
controllerTokenIssuer := &ControllerTokenIssuer{
controllerId: controller.Id,
controllerName: controller.Name,
pubKey: common.IssuerPublicKey{
PubKey: cert.PublicKey,
Chain: certs,
},
kid: fmt.Sprintf("%x", sha1.Sum(cert.Raw)),
controllerIssuer: "",
}
a.controllerIssuers.Set(controller.Id, controllerTokenIssuer)
}
// 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 ext jwt token issuer, issuer is nil")
return
}
signerRec := &TokenIssuerExtJwt{
externalJwtSigner: signer,
jwksResolver: a.jwksResolver,
kidToPubKey: map[string]common.IssuerPublicKey{},
}
if err := signerRec.Resolve(false); err != nil {
logger.WithError(err).Error("could not resolve ext jwt token issuer cert/jwks")
}
a.externalIssuers.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().WithError(err).Error("error on external signature update, could not update ext jwt token issuer")
}
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 remove ext jwt token issuer, issuer is nil")
return
}
a.externalIssuers.Remove(*signer.Issuer)
}
// reportBlockedJwksEndpoint logs an existing external JWT signer whose jwksEndpoint the current
// [edge.externalJwtSigners.jwksFetch] configuration refuses. A configuration change can orphan a
// signer that was created while its endpoint was still permitted, so this is reported at startup
// rather than only when a fetch is attempted. Endpoints that resolve to a blocked address are not
// visible here, as no name resolution is done; those are reported by the fetch itself.
func (a *TokenIssuerCache) reportBlockedJwksEndpoint(signer *db.ExternalJwtSigner) {
if err := checkJwksEndpointAllowed(a.jwksResolver.policy, signer); err != nil {
pfxlog.Logger().WithFields(map[string]interface{}{
"id": signer.Id,
"name": signer.Name,
"jwksEndpoint": *signer.JwksEndpoint,
}).WithError(err).Error("external jwt signer jwks endpoint is not permitted by the current jwks fetch configuration, its keys cannot be resolved and authentication with this signer will fail")
}
}
// loadExisting loads all external JWT signers and controllers during initialization.
func (a *TokenIssuerCache) loadExisting() {
err := a.env.GetDb().View(func(tx *bbolt.Tx) error {
extJwtIds, _, err := a.env.GetStores().ExternalJwtSigner.QueryIds(tx, "")
if err != nil {
return err
}
for _, id := range extJwtIds {
signer, err := a.env.GetStores().ExternalJwtSigner.LoadById(tx, id)
if err != nil {
pfxlog.Logger().WithError(err).WithField("extJwtId", id).Error("error loading external jwt as token issuer")
continue
}
a.reportBlockedJwksEndpoint(signer)
a.onExtJwtCreate(signer)
}
return nil
})
if err != nil {
pfxlog.Logger().WithError(err).Error("error loading existing external jwt token issuers")
}
controllers, err := a.env.GetManagers().Controller.ReadAll()
if err != nil {
pfxlog.Logger().WithError(err).Error("error loading controllers as token issuers")
return
}
for _, controller := range controllers {
a.addControllerIssuer(controller)
}
}
// GetByIssuerString returns the TokenIssuer for the given issuer claim string.
// Returns nil if no issuer is found.
func (a *TokenIssuerCache) GetByIssuerString(issuer string) common.TokenIssuer {
tokenIssuer, ok := a.externalIssuers.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) common.TokenIssuer {
for _, issuer := range a.externalIssuers.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.externalIssuers.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 {
tokenIssuer = a.GetIssuerByKid(kid)
if tokenIssuer == nil {
issuers := a.GetIssuerStrings()
kids := a.GetKids()
logger.WithField("knownIssuers", issuers).WithField("kids", kids).Error("issuer not found, kid not found, issuers and kids are compared and must match")
return nil, apierror.NewInvalidAuth()
}
if !tokenIssuer.IsControllerTokenIssuer() {
logger.Error("kid matched an external jwt signer but issuer string did not match, rejecting to prevent cross-issuer token acceptance")
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) (*common.TokenVerificationResult, common.TokenIssuer) {
claims := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(candidateToken, claims, a.pubKeyLookup)
result := &common.TokenVerificationResult{
Token: token,
Claims: claims,
Error: err,
}
if !result.IsValid() {
return result, nil
}
tokenIssuer := claims[InternalTokenIssuerClaim].(common.TokenIssuer)
if tokenIssuer == nil {
result.Error = errors.New("token issuer not found inside of parsed claims")
return result, nil
}
if !tokenIssuer.IsEnabled() {
result.Error = errors.New("token issuer is disabled")
return result, tokenIssuer
}
result.IdClaimValue, err = resolveStringClaimSelector(claims, tokenIssuer.IdentityIdClaimsSelector())
if err != nil {
result.Error = fmt.Errorf("could not resolve identity claim property %s: %w", tokenIssuer.IdentityIdClaimsSelector(), err)
return result, tokenIssuer
}
result.AttributeClaimSelector = tokenIssuer.EnrollmentAttributeClaimsSelector()
if result.AttributeClaimSelector != "" {
result.AttributeClaimValue, err = resolveStringSliceClaimProperty(claims, result.AttributeClaimSelector)
if err != nil {
result.Error = fmt.Errorf("could not resolve attribute claim property %s: %w", tokenIssuer.EnrollmentAttributeClaimsSelector(), err)
return result, tokenIssuer
}
}
if result.AttributeClaimValue == nil {
result.AttributeClaimValue = []string{}
}
result.NameClaimSelector = tokenIssuer.EnrollmentNameClaimSelector()
if result.NameClaimSelector != "" {
result.NameClaimValue, err = resolveStringClaimSelector(claims, result.NameClaimSelector)
if err != nil {
result.Error = fmt.Errorf("could not resolve name claim property %s: %w", tokenIssuer.EnrollmentNameClaimSelector(), err)
return result, tokenIssuer
}
}
return result, tokenIssuer
}
// IterateExternalIssuers calls f for each registered external JWT issuer in an unspecified order.
// Iteration stops early when f returns false. Controller issuers are not included.
func (a *TokenIssuerCache) IterateExternalIssuers(f func(issuer common.TokenIssuer) bool) {
keepGoing := true
a.externalIssuers.IterCb(func(key string, v common.TokenIssuer) {
if keepGoing {
keepGoing = f(v)
}
})
}
// IterateControllerIssuers calls f for each registered controller JWT issuer in an unspecified order.
// Iteration stops early when f returns false. ExtJwt issuers are not included.
func (a *TokenIssuerCache) IterateControllerIssuers(f func(issuer common.TokenIssuer) bool) {
keepGoing := true
a.controllerIssuers.IterCb(func(key string, v common.TokenIssuer) {
if keepGoing {
keepGoing = f(v)
}
})
}
// GetIssuerByKid searches enabled external JWT signers and then controller issuers for the one
// that owns the given key ID. Disabled external signers are skipped. Returns nil if no issuer
// claims that kid.
//
// External signers can share a kid when they draw from a common signing-key pool, so an external
// match is ambiguous and does not identify a token's issuer. Callers needing a definitive binding
// must resolve by issuer claim instead.
func (a *TokenIssuerCache) GetIssuerByKid(kid string) common.TokenIssuer {
for _, issuer := range a.externalIssuers.Items() {
if !issuer.IsEnabled() {
continue
}
if pubKey, ok := issuer.PubKeyByKid(kid); ok && pubKey.PubKey != nil {
return issuer
}
}
return a.GetControllerIssuerByKid(kid)
}
// GetControllerIssuerByKid returns the controller TokenIssuer that owns the given key ID, or nil if
// no controller issuer claims that kid. A controller issuer's key ID is the fingerprint of its TLS
// certificate, so this resolves controller-issued tokens by kid without consulting external signers.
func (a *TokenIssuerCache) GetControllerIssuerByKid(kid string) common.TokenIssuer {
for _, controller := range a.controllerIssuers.Items() {
if pubKey, ok := controller.PubKeyByKid(kid); ok && pubKey.PubKey != nil {
return controller
}
}
return nil
}
// GetKids returns a deduplicated list of all key IDs known across both external JWT signers
// and controller issuers. Used for diagnostic logging when a token cannot be matched.
func (a *TokenIssuerCache) GetKids() []string {
kidMap := map[string]struct{}{}
for _, issuer := range a.externalIssuers.Items() {
issuerKids := issuer.GetKids()
for _, kid := range issuerKids {
kidMap[kid] = struct{}{}
}
}
for _, controller := range a.controllerIssuers.Items() {
controllerKids := controller.GetKids()
for _, kid := range controllerKids {
kidMap[kid] = struct{}{}
}
}
kids := make([]string, 0, len(kidMap))
for kid := range kidMap {
kids = append(kids, kid)
}
return kids
}
var _ common.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]common.IssuerPublicKey
jwksResponse *jwks.Response
externalJwtSigner *db.ExternalJwtSigner
jwksResolver jwks.Resolver
}
// GetKids returns all key IDs currently cached for this external JWT signer, triggering
// a non-forced Resolve to populate the cache if it is empty.
func (r *TokenIssuerExtJwt) GetKids() []string {
_ = r.Resolve(false)
kids := make([]string, 0, len(r.kidToPubKey))
for kid := range r.kidToPubKey {
kids = append(kids, kid)
}
return kids
}
// IsControllerTokenIssuer returns false because this issuer represents an external signer,
// not a controller-issued token.
func (r *TokenIssuerExtJwt) IsControllerTokenIssuer() bool {
return false
}
// 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) *common.TokenVerificationResult {
err := r.Resolve(false)
if err != nil {
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)
result := &common.TokenVerificationResult{
Error: err,
Token: resultToken,
Claims: claims,
IdClaimSelector: r.IdentityIdClaimsSelector(),
}
if resultToken != nil && !resultToken.Valid && result.Error == nil {
result.Error = errors.New("token invalid for an unspecified reason")
}
if !result.IsValid() {
return result
}
if result.Error = r.verifyIssuerAndAudience(claims); result.Error != nil {
return result
}
result.IdClaimValue, result.Error = resolveStringClaimSelector(claims, r.IdentityIdClaimsSelector())
if result.Error != nil {
result.Error = fmt.Errorf("could not resolve identity claim property %s: %w", r.IdentityIdClaimsSelector(), result.Error)
}
return result
}
// verifyIssuerAndAudience confirms the token's issuer and audience claims match this signer's
// configured values. Signature verification alone resolves the signing key by kid, which is not
// sufficient: a signer that shares keys across audiences (for example a common JWKS) would
// otherwise accept a validly-signed token minted for a different audience or issuer. Callers must
// invoke this only after the signature has been verified.
func (r *TokenIssuerExtJwt) verifyIssuerAndAudience(claims jwt.MapClaims) error {
issuer, err := claims.GetIssuer()
if err != nil {
return fmt.Errorf("could not retrieve issuer claim from token: %w", err)
}
if issuer == "" {
return errors.New("token claims did not contain an issuer")
}
if issuer != r.ExpectedIssuer() {
return fmt.Errorf("token issuer [%s] does not match expected issuer [%s]", issuer, r.ExpectedIssuer())
}
audiences, err := claims.GetAudience()
if err != nil {
return fmt.Errorf("could not retrieve audience claim from token: %w", err)
}
if len(audiences) == 0 {
return errors.New("token claims did not contain an audience")
}
if !stringz.Contains(audiences, r.ExpectedAudience()) {
return fmt.Errorf("token audience %v does not match expected audience [%s]", audiences, r.ExpectedAudience())
}
return 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. An unset selector, a pointer that does
// not resolve against the claims, and a null claim all resolve to no values without error. A claim that is
// present but neither a string nor an array of strings errors.
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 {
// The role attributes claim is optional, so a pointer that does not resolve against this
// token's claims yields no attributes rather than an error, matching the unset-selector and
// empty-claim cases. This covers an absent key as well as a traversal failure, such as
// indexing into a scalar.
pfxlog.Logger().WithError(err).WithField("selector", property).
Debug("attribute claim selector did not resolve, enrolling with no role attributes")
return nil, nil
}
if val == nil {
pfxlog.Logger().WithField("selector", property).
Warn("attribute claim selector resolved to a null claim, enrolling with no role attributes")
return nil, nil
}
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, val)
}
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) (common.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]common.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] = common.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] = common.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")
}
var _ common.TokenIssuer = (*ControllerTokenIssuer)(nil)
// ControllerTokenIssuer implements TokenIssuer for controller-issued JWTs. Each running
// controller is represented by one instance keyed by the SHA-1 fingerprint of its TLS
// certificate (the key ID). It is always considered enabled and does not support enrollment (toCert/toToken enrollment).
type ControllerTokenIssuer struct {
controllerId string
controllerName string
pubKey common.IssuerPublicKey
kid string
controllerIssuer string
}
// GetKids returns the single key ID derived from the controller's TLS certificate fingerprint.
func (o *ControllerTokenIssuer) GetKids() []string {
return []string{o.kid}
}
// IsControllerTokenIssuer returns true, distinguishing this issuer from external JWT signers.
func (o *ControllerTokenIssuer) IsControllerTokenIssuer() bool {
return true
}
func (o *ControllerTokenIssuer) Id() string {
return o.controllerId
}
func (o *ControllerTokenIssuer) TypeName() string {
return "openZitiController"
}
func (o *ControllerTokenIssuer) Name() string {
return o.controllerName
}
func (o *ControllerTokenIssuer) IsEnabled() bool {
return true
}
func (o *ControllerTokenIssuer) PubKeyByKid(kid string) (common.IssuerPublicKey, bool) {
if kid == o.kid {
return o.pubKey, true
}
return common.IssuerPublicKey{}, false
}
func (o *ControllerTokenIssuer) Resolve(_ bool) error {
return nil
}
func (o *ControllerTokenIssuer) ExpectedIssuer() string {
return o.controllerIssuer
}
func (o *ControllerTokenIssuer) ExpectedAudience() string {
return "openziti"
}
func (o *ControllerTokenIssuer) AuthenticatorId() string {
return "internal"
}
func (o *ControllerTokenIssuer) EnrollmentAuthPolicyId() string {
return ""
}
func (o *ControllerTokenIssuer) EnrollmentAttributeClaimsSelector() string {
return ""
}
func (o *ControllerTokenIssuer) EnrollmentNameClaimSelector() string {
return "name"
}
func (o *ControllerTokenIssuer) IdentityIdClaimsSelector() string {
return "sub"
}
func (o *ControllerTokenIssuer) UseExternalId() bool {
return false
}
func (o *ControllerTokenIssuer) VerifyToken(token string) *common.TokenVerificationResult {
claims := jwt.MapClaims{}
resultToken, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
kid := ""
if token.Header != nil {
kid, _ = token.Header["kid"].(string)
}
if kid == "" {
kid = "<not set>"
}
key, ok := o.PubKeyByKid(kid)
if !ok || key.PubKey == nil {
return nil, errors.New("pubkey not found by kid " + kid)
}
return key.PubKey, nil
})
result := &common.TokenVerificationResult{
Token: resultToken,
Claims: claims,
IdClaimSelector: o.IdentityIdClaimsSelector(),
Error: err,
}
if resultToken != nil && !resultToken.Valid && result.Error == nil {
result.Error = errors.New("token invalid for an unspecified reason")
}
if !result.IsValid() {
return result
}
result.IdClaimValue, _ = claims["sub"].(string)
result.IdClaimValue = strings.TrimSpace(result.IdClaimValue)
if result.IdClaimValue == "" {
result.Error = errors.New("no sub claim found in token")
}
return result
}
func (o *ControllerTokenIssuer) EnrollToCertEnabled() bool {
return false
}
func (o *ControllerTokenIssuer) EnrollToTokenEnabled() bool {
return false
}
// 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
}