From ec51635f26d6652fb096752f537441f80dda4368 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 6 Apr 2022 15:48:17 -0400 Subject: [PATCH] adds optional issuer and audience properties to ext jwt signers --- .../routes/external_jwt_signer_api_model.go | 8 ++ controller/model/authenticator_mod_ext_jwt.go | 75 ++++++++-- controller/model/external_jwt_signer_model.go | 12 +- .../persistence/external_jwt_signer_store.go | 26 +++- rest_management_api_server/embedded_spec.go | 68 ++++++++- rest_model/external_jwt_signer_create.go | 6 + rest_model/external_jwt_signer_detail.go | 50 +++++++ rest_model/external_jwt_signer_patch.go | 6 + rest_model/external_jwt_signer_update.go | 6 + specs/management.yml | 24 ++++ specs/source/management/ext-jwt-signers.yml | 24 ++++ tests/auth_external_jwt_signer_test.go | 132 +++++++++++++++--- tests/external_jwt_signer_test.go | 28 ++-- 13 files changed, 422 insertions(+), 43 deletions(-) diff --git a/controller/internal/routes/external_jwt_signer_api_model.go b/controller/internal/routes/external_jwt_signer_api_model.go index acf7bd877..d8937e728 100644 --- a/controller/internal/routes/external_jwt_signer_api_model.go +++ b/controller/internal/routes/external_jwt_signer_api_model.go @@ -83,6 +83,8 @@ func MapExternalJwtSignerToRestModel(externalJwtSigner *model.ExternalJwtSigner) NotBefore: ¬Before, UseExternalID: &externalJwtSigner.UseExternalId, Kid: &externalJwtSigner.Kid, + Issuer: externalJwtSigner.Issuer, + Audience: externalJwtSigner.Audience, } return ret } @@ -97,6 +99,8 @@ func MapCreateExternalJwtSignerToModel(signer *rest_model.ExternalJWTSignerCreat ClaimsProperty: signer.ClaimsProperty, UseExternalId: BoolOrDefault(signer.UseExternalID), Kid: *signer.Kid, + Issuer: signer.Issuer, + Audience: signer.Audience, } } @@ -119,6 +123,8 @@ func MapUpdateExternalJwtSignerToModel(id string, signer *rest_model.ExternalJWT ClaimsProperty: signer.ClaimsProperty, ExternalAuthUrl: signer.ExternalAuthURL, Kid: *signer.Kid, + Issuer: signer.Issuer, + Audience: signer.Audience, } } @@ -141,5 +147,7 @@ func MapPatchExternalJwtSignerToModel(id string, signer *rest_model.ExternalJWTS UseExternalId: BoolOrDefault(signer.UseExternalID), ClaimsProperty: signer.ClaimsProperty, Kid: stringz.OrEmpty(signer.Kid), + Issuer: signer.Issuer, + Audience: signer.Audience, } } diff --git a/controller/model/authenticator_mod_ext_jwt.go b/controller/model/authenticator_mod_ext_jwt.go index 4006cc3a0..3abf3d562 100644 --- a/controller/model/authenticator_mod_ext_jwt.go +++ b/controller/model/authenticator_mod_ext_jwt.go @@ -21,8 +21,8 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/edge/controller/apierror" "github.com/openziti/edge/controller/persistence" - "github.com/openziti/storage/boltz" nfPem "github.com/openziti/foundation/util/pem" + "github.com/openziti/storage/boltz" cmap "github.com/orcaman/concurrent-map" "go.etcd.io/bbolt" "strings" @@ -136,12 +136,11 @@ func (a *AuthModuleExtJwt) process(context AuthContext, isPrimary bool) (identit jwtStr := strings.Replace(authHeader, "Bearer ", "", 1) + //pubKeyLookup also handles extJwtSigner.enabled checking jwtToken, err := jwt.Parse(jwtStr, a.pubKeyLookup) if err == nil && jwtToken.Valid { - claimsProperty := "sub" mapClaims := jwtToken.Claims.(jwt.MapClaims) - extJwt := mapClaims[ExtJwtInternalClaim].(*persistence.ExternalJwtSigner) if extJwt == nil { @@ -151,13 +150,65 @@ func (a *AuthModuleExtJwt) process(context AuthContext, isPrimary bool) (identit logger = logger.WithField("externalJwtSignerId", extJwt.Id) - if extJwt.ClaimsProperty != nil { - claimsProperty = *extJwt.ClaimsProperty + issuer := "" + if issuerVal, ok := mapClaims["iss"]; ok { + issuer, ok = issuerVal.(string) + if !ok { + logger.Error("issuer in claims was not a string") + return "", "", "", apierror.NewInvalidAuth() + } } - logger = logger.WithField("claimsProperty", claimsProperty) + logger = logger.WithField("claimsIssuer", issuer) - identityIdInterface, ok := mapClaims[claimsProperty] + if extJwt.Issuer != nil && *extJwt.Issuer != issuer { + logger.WithField("expectedIssuer", *extJwt.Issuer).Error("invalid issuer") + return "", "", "", apierror.NewInvalidAuth() + } + + if extJwt.Audience != nil { + audValues := mapClaims["aud"] + + if audValues == nil { + logger.WithField("audience", audValues).Error("audience is missing") + return "", "", "", apierror.NewInvalidAuth() + } + + audSlice, ok := audValues.([]string) + + if !ok { + audString, ok := audValues.(string) + + if !ok { + logger.WithField("audience", audValues).Error("audience is not a string or array of strings") + return "", "", "", apierror.NewInvalidAuth() + } + + audSlice = []string{audString} + } + + found := false + for _, validAud := range audSlice { + if validAud == *extJwt.Audience { + found = true + break + } + } + + if !found { + logger.WithField("expectedAudience", *extJwt.Audience).WithField("claimsAudiences", audSlice).Error("invalid audience") + return "", "", "", apierror.NewInvalidAuth() + } + } + + idClaimProperty := "sub" + if extJwt.ClaimsProperty != nil { + idClaimProperty = *extJwt.ClaimsProperty + } + + logger = logger.WithField("idClaimProperty", idClaimProperty) + + identityIdInterface, ok := mapClaims[idClaimProperty] if !ok { logger.Error("claims property on external jwt signer not found in claims") @@ -189,7 +240,7 @@ func (a *AuthModuleExtJwt) process(context AuthContext, isPrimary bool) (identit logger.WithError(err).Error("encountered unhandled nil auth policy during authentication") return "", "", "", apierror.NewInvalidAuth() } - externaJwtSignerId := "" + externalJwtSignerId := "" if identity.Disabled { logger. WithField("disabledAt", identity.DisabledAt). @@ -208,7 +259,7 @@ func (a *AuthModuleExtJwt) process(context AuthContext, isPrimary bool) (identit found := false for _, allowedId := range authPolicy.Primary.ExtJwt.AllowedExtJwtSigners { if allowedId == extJwt.Id { - externaJwtSignerId = allowedId + externalJwtSignerId = allowedId found = true break } @@ -226,13 +277,13 @@ func (a *AuthModuleExtJwt) process(context AuthContext, isPrimary bool) (identit return "", "", "", apierror.NewInvalidAuth() } - externaJwtSignerId = extJwt.Id + externalJwtSignerId = extJwt.Id } if extJwt.UseExternalId { - return "", identityId, externaJwtSignerId, nil + return "", identityId, externalJwtSignerId, nil } - return identityId, "", externaJwtSignerId, nil + return identityId, "", externalJwtSignerId, nil } logger.Error("authorization failed, jwt did not verify") diff --git a/controller/model/external_jwt_signer_model.go b/controller/model/external_jwt_signer_model.go index 852760d1c..c5929b0e6 100644 --- a/controller/model/external_jwt_signer_model.go +++ b/controller/model/external_jwt_signer_model.go @@ -20,8 +20,8 @@ import ( "github.com/openziti/edge/controller/apierror" "github.com/openziti/edge/controller/persistence" "github.com/openziti/fabric/controller/models" - "github.com/openziti/storage/boltz" nfpem "github.com/openziti/foundation/util/pem" + "github.com/openziti/storage/boltz" "github.com/pkg/errors" "go.etcd.io/bbolt" "reflect" @@ -32,16 +32,18 @@ type ExternalJwtSigner struct { models.BaseEntity Name string CertPem string + Kid string Enabled bool ExternalAuthUrl *string UseExternalId bool ClaimsProperty *string + Issuer *string + Audience *string CommonName string Fingerprint string NotAfter time.Time NotBefore time.Time - Kid string } func (entity *ExternalJwtSigner) toBoltEntity() (boltz.Entity, error) { @@ -68,6 +70,8 @@ func (entity *ExternalJwtSigner) toBoltEntity() (boltz.Entity, error) { UseExternalId: entity.UseExternalId, ClaimsProperty: entity.ClaimsProperty, Kid: entity.Kid, + Issuer: entity.Issuer, + Audience: entity.Audience, } return signer, nil @@ -91,6 +95,8 @@ func (entity *ExternalJwtSigner) toBoltEntityForPatch(*bbolt.Tx, Handler, boltz. UseExternalId: entity.UseExternalId, ClaimsProperty: entity.ClaimsProperty, Kid: entity.Kid, + Issuer: entity.Issuer, + Audience: entity.Issuer, } if entity.CertPem != "" { @@ -129,5 +135,7 @@ func (entity *ExternalJwtSigner) fillFrom(_ Handler, _ *bbolt.Tx, boltEntity bol entity.ClaimsProperty = boltExternalJwtSigner.ClaimsProperty entity.UseExternalId = boltExternalJwtSigner.UseExternalId entity.Kid = boltExternalJwtSigner.Kid + entity.Issuer = boltExternalJwtSigner.Issuer + entity.Audience = boltExternalJwtSigner.Audience return nil } diff --git a/controller/persistence/external_jwt_signer_store.go b/controller/persistence/external_jwt_signer_store.go index 7c18487c1..79abf0d8a 100644 --- a/controller/persistence/external_jwt_signer_store.go +++ b/controller/persistence/external_jwt_signer_store.go @@ -21,6 +21,7 @@ import ( "github.com/openziti/storage/ast" "github.com/openziti/storage/boltz" "go.etcd.io/bbolt" + "strings" "time" ) @@ -36,6 +37,8 @@ const ( FieldExternalJwtSignerClaimsProperty = "claimsProperty" FieldExternalJwtSignerUseExternalId = "useExternalId" FieldExternalJwtSignerKid = "kid" + FieldExternalJwtSignerIssuer = "issuer" + FieldExternalJwtSignerAudience = "audience" DefaultClaimsProperty = "sub" ) @@ -53,6 +56,8 @@ type ExternalJwtSigner struct { ExternalAuthUrl *string ClaimsProperty *string UseExternalId bool + Issuer *string + Audience *string } func (entity *ExternalJwtSigner) GetName() string { @@ -72,6 +77,8 @@ func (entity *ExternalJwtSigner) LoadValues(_ boltz.CrudStore, bucket *boltz.Typ entity.ExternalAuthUrl = bucket.GetString(FieldExternalJwtSignerExternalAuthUrl) entity.ClaimsProperty = bucket.GetString(FieldExternalJwtSignerClaimsProperty) entity.UseExternalId = bucket.GetBoolWithDefault(FieldExternalJwtSignerUseExternalId, false) + entity.Issuer = bucket.GetString(FieldExternalJwtSignerIssuer) + entity.Audience = bucket.GetString(FieldExternalJwtSignerAudience) } func (entity *ExternalJwtSigner) SetValues(ctx *boltz.PersistContext) { @@ -84,15 +91,28 @@ func (entity *ExternalJwtSigner) SetValues(ctx *boltz.PersistContext) { ctx.SetTimeP(FieldExternalJwtSignerNotAfter, entity.NotAfter) ctx.SetTimeP(FieldExternalJwtSignerNotBefore, entity.NotBefore) ctx.SetBool(FieldExternalJwtSignerEnabled, entity.Enabled) - ctx.SetStringP(FieldExternalJwtSignerExternalAuthUrl, entity.ExternalAuthUrl) ctx.SetBool(FieldExternalJwtSignerUseExternalId, entity.UseExternalId) - if entity.ClaimsProperty == nil || *entity.ClaimsProperty == "" { + if entity.ExternalAuthUrl != nil && strings.TrimSpace(*entity.ExternalAuthUrl) == "" { + entity.ExternalAuthUrl = nil + } + ctx.SetStringP(FieldExternalJwtSignerExternalAuthUrl, entity.ExternalAuthUrl) + + if entity.Issuer != nil && strings.TrimSpace(*entity.Issuer) == "" { + entity.Issuer = nil + } + ctx.SetStringP(FieldExternalJwtSignerIssuer, entity.Issuer) + + if entity.Audience != nil && strings.TrimSpace(*entity.Audience) == "" { + entity.Audience = nil + } + ctx.SetStringP(FieldExternalJwtSignerAudience, entity.Audience) + + if entity.ClaimsProperty == nil || strings.TrimSpace(*entity.ClaimsProperty) == "" { ctx.SetString(FieldExternalJwtSignerClaimsProperty, DefaultClaimsProperty) } else { ctx.SetStringP(FieldExternalJwtSignerClaimsProperty, entity.ClaimsProperty) } - } func (entity *ExternalJwtSigner) GetEntityType() string { diff --git a/rest_management_api_server/embedded_spec.go b/rest_management_api_server/embedded_spec.go index c3e94c70a..bb5b769cd 100644 --- a/rest_management_api_server/embedded_spec.go +++ b/rest_management_api_server/embedded_spec.go @@ -19046,6 +19046,10 @@ func init() { "kid" ], "properties": { + "audience": { + "type": "string", + "x-nullable": true + }, "certPem": { "type": "string" }, @@ -19061,6 +19065,10 @@ func init() { "format": "url", "x-nullable": true }, + "issuer": { + "type": "string", + "x-nullable": true + }, "kid": { "type": "string" }, @@ -19097,9 +19105,14 @@ func init() { "externalAuthUrl", "claimsProperty", "useExternalId", - "kid" + "kid", + "issuer", + "audience" ], "properties": { + "audience": { + "type": "string" + }, "certPem": { "type": "string" }, @@ -19119,6 +19132,9 @@ func init() { "fingerprint": { "type": "string" }, + "issuer": { + "type": "string" + }, "kid": { "type": "string" }, @@ -19151,6 +19167,10 @@ func init() { "externalJwtSignerPatch": { "type": "object", "properties": { + "audience": { + "type": "string", + "x-nullable": true + }, "certPem": { "type": "string", "x-nullable": true @@ -19168,6 +19188,10 @@ func init() { "format": "url", "x-nullable": true }, + "issuer": { + "type": "string", + "x-nullable": true + }, "kid": { "type": "string", "x-nullable": true @@ -19195,6 +19219,10 @@ func init() { "kid" ], "properties": { + "audience": { + "type": "string", + "x-nullable": true + }, "certPem": { "type": "string" }, @@ -19210,6 +19238,10 @@ func init() { "format": "url", "x-nullable": true }, + "issuer": { + "type": "string", + "x-nullable": true + }, "kid": { "type": "string" }, @@ -41860,6 +41892,10 @@ func init() { "kid" ], "properties": { + "audience": { + "type": "string", + "x-nullable": true + }, "certPem": { "type": "string" }, @@ -41875,6 +41911,10 @@ func init() { "format": "url", "x-nullable": true }, + "issuer": { + "type": "string", + "x-nullable": true + }, "kid": { "type": "string" }, @@ -41911,9 +41951,14 @@ func init() { "externalAuthUrl", "claimsProperty", "useExternalId", - "kid" + "kid", + "issuer", + "audience" ], "properties": { + "audience": { + "type": "string" + }, "certPem": { "type": "string" }, @@ -41933,6 +41978,9 @@ func init() { "fingerprint": { "type": "string" }, + "issuer": { + "type": "string" + }, "kid": { "type": "string" }, @@ -41965,6 +42013,10 @@ func init() { "externalJwtSignerPatch": { "type": "object", "properties": { + "audience": { + "type": "string", + "x-nullable": true + }, "certPem": { "type": "string", "x-nullable": true @@ -41982,6 +42034,10 @@ func init() { "format": "url", "x-nullable": true }, + "issuer": { + "type": "string", + "x-nullable": true + }, "kid": { "type": "string", "x-nullable": true @@ -42009,6 +42065,10 @@ func init() { "kid" ], "properties": { + "audience": { + "type": "string", + "x-nullable": true + }, "certPem": { "type": "string" }, @@ -42024,6 +42084,10 @@ func init() { "format": "url", "x-nullable": true }, + "issuer": { + "type": "string", + "x-nullable": true + }, "kid": { "type": "string" }, diff --git a/rest_model/external_jwt_signer_create.go b/rest_model/external_jwt_signer_create.go index 7ee35c2ab..70728bb50 100644 --- a/rest_model/external_jwt_signer_create.go +++ b/rest_model/external_jwt_signer_create.go @@ -43,6 +43,9 @@ import ( // swagger:model externalJwtSignerCreate type ExternalJWTSignerCreate struct { + // audience + Audience *string `json:"audience,omitempty"` + // cert pem // Required: true CertPem *string `json:"certPem"` @@ -57,6 +60,9 @@ type ExternalJWTSignerCreate struct { // external auth Url ExternalAuthURL *string `json:"externalAuthUrl,omitempty"` + // issuer + Issuer *string `json:"issuer,omitempty"` + // kid // Required: true Kid *string `json:"kid"` diff --git a/rest_model/external_jwt_signer_detail.go b/rest_model/external_jwt_signer_detail.go index 967338c1c..0798e6218 100644 --- a/rest_model/external_jwt_signer_detail.go +++ b/rest_model/external_jwt_signer_detail.go @@ -44,6 +44,10 @@ import ( type ExternalJWTSignerDetail struct { BaseEntity + // audience + // Required: true + Audience *string `json:"audience"` + // cert pem // Required: true CertPem *string `json:"certPem"` @@ -68,6 +72,10 @@ type ExternalJWTSignerDetail struct { // Required: true Fingerprint *string `json:"fingerprint"` + // issuer + // Required: true + Issuer *string `json:"issuer"` + // kid // Required: true Kid *string `json:"kid"` @@ -103,6 +111,8 @@ func (m *ExternalJWTSignerDetail) UnmarshalJSON(raw []byte) error { // AO1 var dataAO1 struct { + Audience *string `json:"audience"` + CertPem *string `json:"certPem"` ClaimsProperty *string `json:"claimsProperty"` @@ -115,6 +125,8 @@ func (m *ExternalJWTSignerDetail) UnmarshalJSON(raw []byte) error { Fingerprint *string `json:"fingerprint"` + Issuer *string `json:"issuer"` + Kid *string `json:"kid"` Name *string `json:"name"` @@ -129,6 +141,8 @@ func (m *ExternalJWTSignerDetail) UnmarshalJSON(raw []byte) error { return err } + m.Audience = dataAO1.Audience + m.CertPem = dataAO1.CertPem m.ClaimsProperty = dataAO1.ClaimsProperty @@ -141,6 +155,8 @@ func (m *ExternalJWTSignerDetail) UnmarshalJSON(raw []byte) error { m.Fingerprint = dataAO1.Fingerprint + m.Issuer = dataAO1.Issuer + m.Kid = dataAO1.Kid m.Name = dataAO1.Name @@ -164,6 +180,8 @@ func (m ExternalJWTSignerDetail) MarshalJSON() ([]byte, error) { } _parts = append(_parts, aO0) var dataAO1 struct { + Audience *string `json:"audience"` + CertPem *string `json:"certPem"` ClaimsProperty *string `json:"claimsProperty"` @@ -176,6 +194,8 @@ func (m ExternalJWTSignerDetail) MarshalJSON() ([]byte, error) { Fingerprint *string `json:"fingerprint"` + Issuer *string `json:"issuer"` + Kid *string `json:"kid"` Name *string `json:"name"` @@ -187,6 +207,8 @@ func (m ExternalJWTSignerDetail) MarshalJSON() ([]byte, error) { UseExternalID *bool `json:"useExternalId"` } + dataAO1.Audience = m.Audience + dataAO1.CertPem = m.CertPem dataAO1.ClaimsProperty = m.ClaimsProperty @@ -199,6 +221,8 @@ func (m ExternalJWTSignerDetail) MarshalJSON() ([]byte, error) { dataAO1.Fingerprint = m.Fingerprint + dataAO1.Issuer = m.Issuer + dataAO1.Kid = m.Kid dataAO1.Name = m.Name @@ -226,6 +250,10 @@ func (m *ExternalJWTSignerDetail) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateAudience(formats); err != nil { + res = append(res, err) + } + if err := m.validateCertPem(formats); err != nil { res = append(res, err) } @@ -250,6 +278,10 @@ func (m *ExternalJWTSignerDetail) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateIssuer(formats); err != nil { + res = append(res, err) + } + if err := m.validateKid(formats); err != nil { res = append(res, err) } @@ -276,6 +308,15 @@ func (m *ExternalJWTSignerDetail) Validate(formats strfmt.Registry) error { return nil } +func (m *ExternalJWTSignerDetail) validateAudience(formats strfmt.Registry) error { + + if err := validate.Required("audience", "body", m.Audience); err != nil { + return err + } + + return nil +} + func (m *ExternalJWTSignerDetail) validateCertPem(formats strfmt.Registry) error { if err := validate.Required("certPem", "body", m.CertPem); err != nil { @@ -330,6 +371,15 @@ func (m *ExternalJWTSignerDetail) validateFingerprint(formats strfmt.Registry) e return nil } +func (m *ExternalJWTSignerDetail) validateIssuer(formats strfmt.Registry) error { + + if err := validate.Required("issuer", "body", m.Issuer); err != nil { + return err + } + + return nil +} + func (m *ExternalJWTSignerDetail) validateKid(formats strfmt.Registry) error { if err := validate.Required("kid", "body", m.Kid); err != nil { diff --git a/rest_model/external_jwt_signer_patch.go b/rest_model/external_jwt_signer_patch.go index 712367566..c84db9a3f 100644 --- a/rest_model/external_jwt_signer_patch.go +++ b/rest_model/external_jwt_signer_patch.go @@ -42,6 +42,9 @@ import ( // swagger:model externalJwtSignerPatch type ExternalJWTSignerPatch struct { + // audience + Audience *string `json:"audience,omitempty"` + // cert pem CertPem *string `json:"certPem,omitempty"` @@ -54,6 +57,9 @@ type ExternalJWTSignerPatch struct { // external auth Url ExternalAuthURL *string `json:"externalAuthUrl,omitempty"` + // issuer + Issuer *string `json:"issuer,omitempty"` + // kid Kid *string `json:"kid,omitempty"` diff --git a/rest_model/external_jwt_signer_update.go b/rest_model/external_jwt_signer_update.go index 37c5c9db0..a0432a524 100644 --- a/rest_model/external_jwt_signer_update.go +++ b/rest_model/external_jwt_signer_update.go @@ -43,6 +43,9 @@ import ( // swagger:model externalJwtSignerUpdate type ExternalJWTSignerUpdate struct { + // audience + Audience *string `json:"audience,omitempty"` + // cert pem // Required: true CertPem *string `json:"certPem"` @@ -57,6 +60,9 @@ type ExternalJWTSignerUpdate struct { // external auth Url ExternalAuthURL *string `json:"externalAuthUrl,omitempty"` + // issuer + Issuer *string `json:"issuer,omitempty"` + // kid // Required: true Kid *string `json:"kid"` diff --git a/specs/management.yml b/specs/management.yml index 0cd50f802..45f33dcb6 100644 --- a/specs/management.yml +++ b/specs/management.yml @@ -13735,6 +13735,9 @@ definitions: - enabled - kid properties: + audience: + type: string + x-nullable: true certPem: type: string claimsProperty: @@ -13746,6 +13749,9 @@ definitions: type: string format: url x-nullable: true + issuer: + type: string + x-nullable: true kid: type: string name: @@ -13774,7 +13780,11 @@ definitions: - claimsProperty - useExternalId - kid + - issuer + - audience properties: + audience: + type: string certPem: type: string claimsProperty: @@ -13788,6 +13798,8 @@ definitions: format: url fingerprint: type: string + issuer: + type: string kid: type: string name: @@ -13809,6 +13821,9 @@ definitions: externalJwtSignerPatch: type: object properties: + audience: + type: string + x-nullable: true certPem: type: string x-nullable: true @@ -13822,6 +13837,9 @@ definitions: type: string format: url x-nullable: true + issuer: + type: string + x-nullable: true kid: type: string x-nullable: true @@ -13842,6 +13860,9 @@ definitions: - enabled - kid properties: + audience: + type: string + x-nullable: true certPem: type: string claimsProperty: @@ -13853,6 +13874,9 @@ definitions: type: string format: url x-nullable: true + issuer: + type: string + x-nullable: true kid: type: string name: diff --git a/specs/source/management/ext-jwt-signers.yml b/specs/source/management/ext-jwt-signers.yml index 4621d4ce3..80c25fe1b 100644 --- a/specs/source/management/ext-jwt-signers.yml +++ b/specs/source/management/ext-jwt-signers.yml @@ -178,6 +178,8 @@ definitions: - claimsProperty - useExternalId - kid + - issuer + - audience properties: name: type: string @@ -205,6 +207,10 @@ definitions: type: boolean kid: type: string + issuer: + type: string + audience: + type: string externalJwtSignerCreate: description: A create Certificate Authority (CA) object type: object @@ -233,6 +239,12 @@ definitions: useExternalId: type: boolean x-nullable: true + issuer: + type: string + x-nullable: true + audience: + type: string + x-nullable: true tags: $ref: '../shared/base-entity.yml#/definitions/tags' externalJwtSignerUpdate: @@ -262,6 +274,12 @@ definitions: useExternalId: type: boolean x-nullable: true + issuer: + type: string + x-nullable: true + audience: + type: string + x-nullable: true tags: $ref: '../shared/base-entity.yml#/definitions/tags' externalJwtSignerPatch: @@ -290,5 +308,11 @@ definitions: useExternalId: type: boolean x-nullable: true + issuer: + type: string + x-nullable: true + audience: + type: string + x-nullable: true tags: $ref: '../shared/base-entity.yml#/definitions/tags' \ No newline at end of file diff --git a/tests/auth_external_jwt_signer_test.go b/tests/auth_external_jwt_signer_test.go index a794fcc24..59657763b 100644 --- a/tests/auth_external_jwt_signer_test.go +++ b/tests/auth_external_jwt_signer_test.go @@ -36,17 +36,18 @@ func Test_Authenticate_External_Jwt(t *testing.T) { ctx.RequireAdminManagementApiLogin() // create a bunch of signers to use - validJwtSignerCommonName := "valid signer" - validJwtSignerCert, validJwtSignerPrivateKey := newSelfSignedCert(validJwtSignerCommonName) + + //valid signer with issuer and audience + validJwtSignerCert, validJwtSignerPrivateKey := newSelfSignedCert("valid signer") validJwtSignerCertPem := nfpem.EncodeToString(validJwtSignerCert) - validJwtSignerName := "Test JWT Signer - Enabled" - validJwtSignerEnabled := true validJwtSigner := &rest_model.ExternalJWTSignerCreate{ - CertPem: &validJwtSignerCertPem, - Enabled: &validJwtSignerEnabled, - Name: &validJwtSignerName, - Kid: S(uuid.NewString()), + CertPem: &validJwtSignerCertPem, + Enabled: B(true), + Name: S("Test JWT Signer - Enabled"), + Kid: S(uuid.NewString()), + Issuer: S("the-very-best-iss"), + Audience: S("the-very-best-aud"), } createResponseEnv := &rest_model.CreateEnvelope{} @@ -55,16 +56,30 @@ func Test_Authenticate_External_Jwt(t *testing.T) { ctx.Req.NoError(err) ctx.Req.Equal(http.StatusCreated, resp.StatusCode()) - notEnabledJwtSignerCommonName := "not enabled signer" - notEnabledJwtSignerCert, notEnabledJwtSignerPrivateKey := newSelfSignedCert(notEnabledJwtSignerCommonName) + //valid signer no issuer no audienceS + validJwtSignerNoIssNoAudCert, validJwtSignerNoIssNoAudPrivateKey := newSelfSignedCert("valid signer") + validJwtSignerCertPemNoIssNoAud := nfpem.EncodeToString(validJwtSignerNoIssNoAudCert) + + validJwtSignerNoIssNoAud := &rest_model.ExternalJWTSignerCreate{ + CertPem: &validJwtSignerCertPemNoIssNoAud, + Enabled: B(true), + Name: S("Test JWT Signer - Enabled No Iss No Aud"), + Kid: S(uuid.NewString()), + } + + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(validJwtSignerNoIssNoAud).SetResult(createResponseEnv).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode()) + + createResponseEnv = &rest_model.CreateEnvelope{} + + notEnabledJwtSignerCert, notEnabledJwtSignerPrivateKey := newSelfSignedCert("not enabled signer") notEnabledJwtSignerCertPem := nfpem.EncodeToString(notEnabledJwtSignerCert) - notEnabledJwtSignerName := "Test JWT Signer - Not Enabled" - notEnabledJwtSignerEnabled := false notEnabledJwtSigner := &rest_model.ExternalJWTSignerCreate{ CertPem: ¬EnabledJwtSignerCertPem, - Enabled: ¬EnabledJwtSignerEnabled, - Name: ¬EnabledJwtSignerName, + Enabled: B(false), + Name: S("Test JWT Signer - Not Enabled"), Kid: S(uuid.NewString()), } @@ -81,11 +96,11 @@ func Test_Authenticate_External_Jwt(t *testing.T) { jwtToken := jwt.New(jwt.SigningMethodES256) jwtToken.Claims = jwt.StandardClaims{ - Audience: "ziti.controller", + Audience: *validJwtSigner.Audience, ExpiresAt: time.Now().Add(2 * time.Hour).Unix(), Id: time.Now().String(), IssuedAt: time.Now().Unix(), - Issuer: "fake.issuer", + Issuer: *validJwtSigner.Issuer, NotBefore: time.Now().Unix(), Subject: ctx.AdminManagementSession.identityId, } @@ -107,6 +122,37 @@ func Test_Authenticate_External_Jwt(t *testing.T) { ctx.Req.NotNil(result.Data.Token) }) + t.Run("authenticating with a valid jwt succeeds and no iss no aud succeeds", func(t *testing.T) { + ctx.testContextChanged(t) + + jwtToken := jwt.New(jwt.SigningMethodES256) + jwtToken.Claims = jwt.StandardClaims{ + Audience: "i do not matter", + ExpiresAt: time.Now().Add(2 * time.Hour).Unix(), + Id: time.Now().String(), + IssuedAt: time.Now().Unix(), + Issuer: "i do not matter", + NotBefore: time.Now().Unix(), + Subject: ctx.AdminManagementSession.identityId, + } + + jwtToken.Header["kid"] = *validJwtSignerNoIssNoAud.Kid + + jwtStrSigned, err := jwtToken.SignedString(validJwtSignerNoIssNoAudPrivateKey) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(jwtStrSigned) + + result := &rest_model.CurrentAPISessionDetailEnvelope{} + + resp, err := ctx.newAnonymousClientApiRequest().SetResult(result).SetHeader("Authorization", "Bearer "+jwtStrSigned).Post("/authenticate?method=ext-jwt") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + + ctx.Req.NotNil(result) + ctx.Req.NotNil(result.Data) + ctx.Req.NotNil(result.Data.Token) + }) + t.Run("authenticating with a valid jwt but disabled signer fails", func(t *testing.T) { ctx.testContextChanged(t) @@ -134,6 +180,60 @@ func Test_Authenticate_External_Jwt(t *testing.T) { ctx.Req.Equal(http.StatusUnauthorized, resp.StatusCode()) }) + t.Run("authenticating with an invalid issuer jwt fails", func(t *testing.T) { + ctx.testContextChanged(t) + + jwtToken := jwt.New(jwt.SigningMethodES256) + jwtToken.Claims = jwt.StandardClaims{ + Audience: *validJwtSigner.Audience, + ExpiresAt: time.Now().Add(2 * time.Hour).Unix(), + Id: time.Now().String(), + IssuedAt: time.Now().Unix(), + Issuer: "i will cause this to fail", + NotBefore: time.Now().Unix(), + Subject: ctx.AdminManagementSession.identityId, + } + + jwtToken.Header["kid"] = *validJwtSigner.Kid + + jwtStrSigned, err := jwtToken.SignedString(validJwtSignerPrivateKey) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(jwtStrSigned) + + result := &rest_model.CurrentAPISessionDetailEnvelope{} + + resp, err := ctx.newAnonymousClientApiRequest().SetResult(result).SetHeader("Authorization", "Bearer "+jwtStrSigned).Post("/authenticate?method=ext-jwt") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusUnauthorized, resp.StatusCode()) + }) + + t.Run("authenticating with an invalid audience jwt fails", func(t *testing.T) { + ctx.testContextChanged(t) + + jwtToken := jwt.New(jwt.SigningMethodES256) + jwtToken.Claims = jwt.StandardClaims{ + Audience: "this test shall not succeed", + ExpiresAt: time.Now().Add(2 * time.Hour).Unix(), + Id: time.Now().String(), + IssuedAt: time.Now().Unix(), + Issuer: *validJwtSigner.Issuer, + NotBefore: time.Now().Unix(), + Subject: ctx.AdminManagementSession.identityId, + } + + jwtToken.Header["kid"] = *validJwtSigner.Kid + + jwtStrSigned, err := jwtToken.SignedString(validJwtSignerPrivateKey) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(jwtStrSigned) + + result := &rest_model.CurrentAPISessionDetailEnvelope{} + + resp, err := ctx.newAnonymousClientApiRequest().SetResult(result).SetHeader("Authorization", "Bearer "+jwtStrSigned).Post("/authenticate?method=ext-jwt") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusUnauthorized, resp.StatusCode()) + }) + t.Run("authenticating with a valid jwt but no kid fails", func(t *testing.T) { ctx.testContextChanged(t) diff --git a/tests/external_jwt_signer_test.go b/tests/external_jwt_signer_test.go index 47170ee60..042974f08 100644 --- a/tests/external_jwt_signer_test.go +++ b/tests/external_jwt_signer_test.go @@ -60,6 +60,8 @@ func Test_ExternalJWTSigner(t *testing.T) { Tags: nil, UseExternalID: B(true), Kid: S(uuid.New().String()), + Issuer: S("i-am-the-issuer"), + Audience: S("you-are-the-audience"), } createResponseEnv := &rest_model.CreateEnvelope{} @@ -95,6 +97,8 @@ func Test_ExternalJWTSigner(t *testing.T) { ctx.Req.Equal(*jwtSigner.ClaimsProperty, *jwtSignerDetail.ClaimsProperty) ctx.Req.Equal(*jwtSigner.ExternalAuthURL, *jwtSignerDetail.ExternalAuthURL) ctx.Req.Equal(*jwtSigner.Kid, *jwtSignerDetail.Kid) + ctx.Req.Equal(*jwtSigner.Issuer, *jwtSignerDetail.Issuer) + ctx.Req.Equal(*jwtSigner.Audience, *jwtSignerDetail.Audience) }) }) @@ -200,6 +204,8 @@ func Test_ExternalJWTSigner(t *testing.T) { ctx.Req.False(*jwtSignerDetail.UseExternalID) ctx.Req.Equal(persistence.DefaultClaimsProperty, *jwtSignerDetail.ClaimsProperty) ctx.Req.Nil(jwtSignerDetail.ExternalAuthURL) + ctx.Req.Nil(jwtSignerDetail.Issuer) + ctx.Req.Nil(jwtSignerDetail.Audience) }) }) }) @@ -377,10 +383,12 @@ func Test_ExternalJWTSigner(t *testing.T) { jwtSignerEnabledUpdated := true jwtSigner := &rest_model.ExternalJWTSignerCreate{ - CertPem: &jwtSignerCertPem, - Enabled: &jwtSignerEnabled, - Name: &jwtSignerName, - Kid: S(uuid.New().String()), + CertPem: &jwtSignerCertPem, + Enabled: &jwtSignerEnabled, + Name: &jwtSignerName, + Kid: S(uuid.New().String()), + Issuer: S("origIssues"), + Audience: S("origAudience"), } createResponseEnv := &rest_model.CreateEnvelope{} @@ -390,10 +398,12 @@ func Test_ExternalJWTSigner(t *testing.T) { ctx.Req.Equal(http.StatusCreated, resp.StatusCode()) jwtSignerUpdate := &rest_model.ExternalJWTSignerUpdate{ - CertPem: &jwtSignerCertPemUpdated, - Enabled: &jwtSignerEnabledUpdated, - Name: &jwtSignerNameUpdated, - Kid: S(uuid.New().String()), + CertPem: &jwtSignerCertPemUpdated, + Enabled: &jwtSignerEnabledUpdated, + Name: &jwtSignerNameUpdated, + Kid: S(uuid.New().String()), + Issuer: S(""), + Audience: S(""), } resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(jwtSignerUpdate).SetResult(createResponseEnv).Put("/external-jwt-signers/" + createResponseEnv.Data.ID) @@ -424,6 +434,8 @@ func Test_ExternalJWTSigner(t *testing.T) { ctx.Req.Equal(jwtSignerCertUpdated.NotAfter, time.Time(*jwtSignerDetail.NotAfter)) ctx.Req.Equal(fingerprint, *jwtSignerDetail.Fingerprint) ctx.Req.Equal(*jwtSignerUpdate.Kid, *jwtSignerDetail.Kid) + ctx.Req.Nil(jwtSignerDetail.Issuer) + ctx.Req.Nil(jwtSignerDetail.Audience) }) }) })