diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 57e0fbe722..8d794931fc 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -118,6 +118,18 @@ Adds a new oidc identity provider configuration the IAM POST: /idps/oidc +### AddJWTIDP + +> **rpc** AddJWTIDP([AddJWTIDPRequest](#addjwtidprequest)) +[AddJWTIDPResponse](#addjwtidpresponse) + +Adds a new jwt identity provider configuration the IAM + + + + POST: /idps/jwt + + ### UpdateIDP > **rpc** UpdateIDP([UpdateIDPRequest](#updateidprequest)) @@ -182,6 +194,19 @@ all fields are updated. If no value is provided the field will be empty afterwar PUT: /idps/{idp_id}/oidc_config +### UpdateIDPJWTConfig + +> **rpc** UpdateIDPJWTConfig([UpdateIDPJWTConfigRequest](#updateidpjwtconfigrequest)) +[UpdateIDPJWTConfigResponse](#updateidpjwtconfigresponse) + +Updates the jwt configuration of the specified idp +all fields are updated. If no value is provided the field will be empty afterwards. + + + + PUT: /idps/{idp_id}/jwt_config + + ### GetDefaultFeatures > **rpc** GetDefaultFeatures([GetDefaultFeaturesRequest](#getdefaultfeaturesrequest)) @@ -1165,6 +1190,35 @@ This is an empty request +### AddJWTIDPRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| name | string | - | string.min_len: 1
string.max_len: 200
| +| styling_type | zitadel.idp.v1.IDPStylingType | - | enum.defined_only: true
| +| jwt_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| issuer | string | - | string.min_len: 1
string.max_len: 200
| +| keys_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| header_name | string | - | string.min_len: 1
string.max_len: 200
| +| auto_register | bool | - | | + + + + +### AddJWTIDPResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| idp_id | string | - | | + + + + ### AddMultiFactorToLoginPolicyRequest @@ -2851,6 +2905,32 @@ This is an empty request +### UpdateIDPJWTConfigRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| idp_id | string | - | string.min_len: 1
string.max_len: 200
| +| jwt_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| issuer | string | - | string.min_len: 1
string.max_len: 200
| +| keys_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| header_name | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### UpdateIDPJWTConfigResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateIDPOIDCConfigRequest diff --git a/docs/docs/apis/proto/idp.md b/docs/docs/apis/proto/idp.md index 8a23275ca5..1181f64e05 100644 --- a/docs/docs/apis/proto/idp.md +++ b/docs/docs/apis/proto/idp.md @@ -22,6 +22,7 @@ title: zitadel/idp.proto | styling_type | IDPStylingType | - | | | owner | IDPOwnerType | - | | | [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.oidc_config | OIDCConfig | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.jwt_config | JWTConfig | - | | | auto_register | bool | - | | @@ -90,6 +91,20 @@ title: zitadel/idp.proto +### JWTConfig + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| jwt_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| issuer | string | - | string.min_len: 1
string.max_len: 200
| +| keys_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| header_name | string | - | string.min_len: 1
string.max_len: 200
| + + + + ### OIDCConfig @@ -162,7 +177,8 @@ authorization framework of the identity provider | Name | Number | Description | | ---- | ------ | ----------- | | IDP_TYPE_UNSPECIFIED | 0 | - | -| IDP_TYPE_OIDC | 1 | PLANNED: IDP_TYPE_SAML | +| IDP_TYPE_OIDC | 1 | - | +| IDP_TYPE_JWT | 3 | PLANNED: IDP_TYPE_SAML | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index a59153af53..a4f2a22e61 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -2595,6 +2595,18 @@ Provider must be OIDC compliant POST: /idps/oidc +### AddOrgJWTIDP + +> **rpc** AddOrgJWTIDP([AddOrgJWTIDPRequest](#addorgjwtidprequest)) +[AddOrgJWTIDPResponse](#addorgjwtidpresponse) + +Add a new jwt identity provider configuration in the organisation + + + + POST: /idps/jwt + + ### DeactivateOrgIDP > **rpc** DeactivateOrgIDP([DeactivateOrgIDPRequest](#deactivateorgidprequest)) @@ -2659,6 +2671,18 @@ Change OIDC identity provider configuration of the organisation PUT: /idps/{idp_id}/oidc_config +### UpdateOrgIDPJWTConfig + +> **rpc** UpdateOrgIDPJWTConfig([UpdateOrgIDPJWTConfigRequest](#updateorgidpjwtconfigrequest)) +[UpdateOrgIDPJWTConfigResponse](#updateorgidpjwtconfigresponse) + +Change JWT identity provider configuration of the organisation + + + + PUT: /idps/{idp_id}/jwt_config + + @@ -3117,6 +3141,35 @@ This is an empty request +### AddOrgJWTIDPRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| name | string | - | string.min_len: 1
string.max_len: 200
| +| styling_type | zitadel.idp.v1.IDPStylingType | - | enum.defined_only: true
| +| jwt_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| issuer | string | - | string.min_len: 1
string.max_len: 200
| +| keys_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| header_name | string | - | string.min_len: 1
string.max_len: 200
| +| auto_register | bool | - | | + + + + +### AddOrgJWTIDPResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| idp_id | string | - | | + + + + ### AddOrgMemberRequest @@ -7343,6 +7396,32 @@ This is an empty request +### UpdateOrgIDPJWTConfigRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| idp_id | string | - | string.min_len: 1
string.max_len: 200
| +| jwt_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| issuer | string | - | string.min_len: 1
string.max_len: 200
| +| keys_endpoint | string | - | string.min_len: 1
string.max_len: 200
| +| header_name | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### UpdateOrgIDPJWTConfigResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateOrgIDPOIDCConfigRequest diff --git a/internal/admin/repository/eventsourcing/handler/idp_config.go b/internal/admin/repository/eventsourcing/handler/idp_config.go index d815ac5a24..f682d805a1 100644 --- a/internal/admin/repository/eventsourcing/handler/idp_config.go +++ b/internal/admin/repository/eventsourcing/handler/idp_config.go @@ -9,6 +9,7 @@ import ( iam_model "github.com/caos/zitadel/internal/iam/model" "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/repository/iam" ) const ( @@ -84,7 +85,9 @@ func (i *IDPConfig) processIDPConfig(event *es_models.Event) (err error) { err = idp.AppendEvent(iam_model.IDPProviderTypeSystem, event) case model.IDPConfigChanged, model.OIDCIDPConfigAdded, - model.OIDCIDPConfigChanged: + model.OIDCIDPConfigChanged, + es_models.EventType(iam.IDPJWTConfigAddedEventType), + es_models.EventType(iam.IDPJWTConfigChangedEventType): err = idp.SetData(event) if err != nil { return err diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index a1dc990a33..1d932425cb 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -42,6 +42,21 @@ func (s *Server) AddOIDCIDP(ctx context.Context, req *admin_pb.AddOIDCIDPRequest }, nil } +func (s *Server) AddJWTIDP(ctx context.Context, req *admin_pb.AddJWTIDPRequest) (*admin_pb.AddJWTIDPResponse, error) { + config, err := s.command.AddDefaultIDPConfig(ctx, addJWTIDPRequestToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.AddJWTIDPResponse{ + IdpId: config.IDPConfigID, + Details: object_pb.AddToDetailsPb( + config.Sequence, + config.ChangeDate, + config.ResourceOwner, + ), + }, nil +} + func (s *Server) UpdateIDP(ctx context.Context, req *admin_pb.UpdateIDPRequest) (*admin_pb.UpdateIDPResponse, error) { config, err := s.command.ChangeDefaultIDPConfig(ctx, updateIDPToDomain(req)) if err != nil { @@ -101,3 +116,17 @@ func (s *Server) UpdateIDPOIDCConfig(ctx context.Context, req *admin_pb.UpdateID ), }, nil } + +func (s *Server) UpdateIDPJWTConfig(ctx context.Context, req *admin_pb.UpdateIDPJWTConfigRequest) (*admin_pb.UpdateIDPJWTConfigResponse, error) { + config, err := s.command.ChangeDefaultIDPJWTConfig(ctx, updateJWTConfigToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateIDPJWTConfigResponse{ + Details: object_pb.ChangeToDetailsPb( + config.Sequence, + config.ChangeDate, + config.ResourceOwner, + ), + }, nil +} diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 98c918acf5..78a0fd89e2 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -31,6 +31,25 @@ func addOIDCIDPRequestToDomainOIDCIDPConfig(req *admin_pb.AddOIDCIDPRequest) *do } } +func addJWTIDPRequestToDomain(req *admin_pb.AddJWTIDPRequest) *domain.IDPConfig { + return &domain.IDPConfig{ + Name: req.Name, + JWTConfig: addJWTIDPRequestToDomainJWTIDPConfig(req), + StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType), + Type: domain.IDPConfigTypeJWT, + AutoRegister: req.AutoRegister, + } +} + +func addJWTIDPRequestToDomainJWTIDPConfig(req *admin_pb.AddJWTIDPRequest) *domain.JWTIDPConfig { + return &domain.JWTIDPConfig{ + JWTEndpoint: req.JwtEndpoint, + Issuer: req.Issuer, + KeysEndpoint: req.KeysEndpoint, + HeaderName: req.HeaderName, + } +} + func updateIDPToDomain(req *admin_pb.UpdateIDPRequest) *domain.IDPConfig { return &domain.IDPConfig{ IDPConfigID: req.IdpId, @@ -52,6 +71,16 @@ func updateOIDCConfigToDomain(req *admin_pb.UpdateIDPOIDCConfigRequest) *domain. } } +func updateJWTConfigToDomain(req *admin_pb.UpdateIDPJWTConfigRequest) *domain.JWTIDPConfig { + return &domain.JWTIDPConfig{ + IDPConfigID: req.IdpId, + JWTEndpoint: req.JwtEndpoint, + Issuer: req.Issuer, + KeysEndpoint: req.KeysEndpoint, + HeaderName: req.HeaderName, + } +} + func listIDPsToModel(req *admin_pb.ListIDPsRequest) *iam_model.IDPConfigSearchRequest { offset, limit, asc := object.ListQueryToModel(req.Query) return &iam_model.IDPConfigSearchRequest{ diff --git a/internal/api/grpc/admin/idp_converter_test.go b/internal/api/grpc/admin/idp_converter_test.go index d4f6255424..0ea13b89b7 100644 --- a/internal/api/grpc/admin/idp_converter_test.go +++ b/internal/api/grpc/admin/idp_converter_test.go @@ -46,6 +46,7 @@ func Test_addOIDCIDPRequestToDomain(t *testing.T) { "OIDCConfig.AuthorizationEndpoint", "OIDCConfig.TokenEndpoint", "Type", //TODO: default (0) is oidc + "JWTConfig", ) }) } @@ -113,6 +114,7 @@ func Test_updateIDPToDomain(t *testing.T) { test.AssertFieldsMapped(t, got, "ObjectRoot", "OIDCConfig", + "JWTConfig", "State", "Type", //TODO: type should not be changeable ) diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index a7974a9b21..ffcdc010bb 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -59,7 +59,7 @@ func ExternalIDPViewToLoginPolicyLinkPb(link *iam_model.IDPProviderView) *idp_pb return &idp_pb.IDPLoginPolicyLink{ IdpId: link.IDPConfigID, IdpName: link.Name, - IdpType: idp_pb.IDPType_IDP_TYPE_OIDC, + IdpType: IDPTypeToPb(link.IDPConfigType), } } @@ -79,7 +79,20 @@ func ExternalIDPViewToUserLinkPb(link *user_model.ExternalIDPView) *idp_pb.IDPUs ProvidedUserId: link.ExternalUserID, ProvidedUserName: link.UserDisplayName, //TODO: as soon as saml is implemented we need to switch here - IdpType: idp_pb.IDPType_IDP_TYPE_OIDC, + //IdpType: IDPTypeToPb(link.Type), + } +} + +func IDPTypeToPb(idpType iam_model.IdpConfigType) idp_pb.IDPType { + switch idpType { + case iam_model.IDPConfigTypeOIDC: + return idp_pb.IDPType_IDP_TYPE_OIDC + case iam_model.IDPConfigTypeSAML: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED + case iam_model.IDPConfigTypeJWT: + return idp_pb.IDPType_IDP_TYPE_JWT + default: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED } } @@ -132,26 +145,45 @@ func IDPStylingTypeToPb(stylingType domain.IDPConfigStylingType) idp_pb.IDPStyli } } -func ModelIDPViewToConfigPb(config *iam_model.IDPConfigView) *idp_pb.IDP_OidcConfig { - return &idp_pb.IDP_OidcConfig{ - OidcConfig: &idp_pb.OIDCConfig{ - ClientId: config.OIDCClientID, - Issuer: config.OIDCIssuer, - Scopes: config.OIDCScopes, - DisplayNameMapping: ModelMappingFieldToPb(config.OIDCIDPDisplayNameMapping), - UsernameMapping: ModelMappingFieldToPb(config.OIDCUsernameMapping), +func ModelIDPViewToConfigPb(config *iam_model.IDPConfigView) idp_pb.IDPConfig { + if config.IsOIDC { + return &idp_pb.IDP_OidcConfig{ + OidcConfig: &idp_pb.OIDCConfig{ + ClientId: config.OIDCClientID, + Issuer: config.OIDCIssuer, + Scopes: config.OIDCScopes, + DisplayNameMapping: ModelMappingFieldToPb(config.OIDCIDPDisplayNameMapping), + UsernameMapping: ModelMappingFieldToPb(config.OIDCUsernameMapping), + }, + } + } + return &idp_pb.IDP_JwtConfig{ + JwtConfig: &idp_pb.JWTConfig{ + JwtEndpoint: config.JWTEndpoint, + Issuer: config.JWTIssuer, + KeysEndpoint: config.JWTKeysEndpoint, + HeaderName: config.JWTHeaderName, }, } } -func IDPViewToConfigPb(config *domain.IDPConfigView) *idp_pb.IDP_OidcConfig { - return &idp_pb.IDP_OidcConfig{ - OidcConfig: &idp_pb.OIDCConfig{ - ClientId: config.OIDCClientID, - Issuer: config.OIDCIssuer, - Scopes: config.OIDCScopes, - DisplayNameMapping: MappingFieldToPb(config.OIDCIDPDisplayNameMapping), - UsernameMapping: MappingFieldToPb(config.OIDCUsernameMapping), +func IDPViewToConfigPb(config *domain.IDPConfigView) idp_pb.IDPConfig { + if config.IsOIDC { + return &idp_pb.IDP_OidcConfig{ + OidcConfig: &idp_pb.OIDCConfig{ + ClientId: config.OIDCClientID, + Issuer: config.OIDCIssuer, + Scopes: config.OIDCScopes, + DisplayNameMapping: MappingFieldToPb(config.OIDCIDPDisplayNameMapping), + UsernameMapping: MappingFieldToPb(config.OIDCUsernameMapping), + }, + } + } + return &idp_pb.IDP_JwtConfig{ + JwtConfig: &idp_pb.JWTConfig{ + JwtEndpoint: config.JWTEndpoint, + Issuer: config.JWTIssuer, + KeysEndpoint: config.JWTKeysEndpoint, }, } } diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 60d6619d51..cb92beb355 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -40,6 +40,22 @@ func (s *Server) AddOrgOIDCIDP(ctx context.Context, req *mgmt_pb.AddOrgOIDCIDPRe ), }, nil } + +func (s *Server) AddOrgJWTIDP(ctx context.Context, req *mgmt_pb.AddOrgJWTIDPRequest) (*mgmt_pb.AddOrgJWTIDPResponse, error) { + config, err := s.command.AddIDPConfig(ctx, addJWTIDPRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.AddOrgJWTIDPResponse{ + IdpId: config.IDPConfigID, + Details: object_pb.AddToDetailsPb( + config.Sequence, + config.ChangeDate, + config.ResourceOwner, + ), + }, nil +} + func (s *Server) DeactivateOrgIDP(ctx context.Context, req *mgmt_pb.DeactivateOrgIDPRequest) (*mgmt_pb.DeactivateOrgIDPResponse, error) { objectDetails, err := s.command.DeactivateIDPConfig(ctx, req.IdpId, authz.GetCtxData(ctx).OrgID) if err != nil { @@ -96,3 +112,17 @@ func (s *Server) UpdateOrgIDPOIDCConfig(ctx context.Context, req *mgmt_pb.Update ), }, nil } + +func (s *Server) UpdateOrgIDPJWTConfig(ctx context.Context, req *mgmt_pb.UpdateOrgIDPJWTConfigRequest) (*mgmt_pb.UpdateOrgIDPJWTConfigResponse, error) { + config, err := s.command.ChangeIDPJWTConfig(ctx, updateJWTConfigToDomain(req), authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateOrgIDPJWTConfigResponse{ + Details: object_pb.ChangeToDetailsPb( + config.Sequence, + config.ChangeDate, + config.ResourceOwner, + ), + }, nil +} diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index e90009a201..f0dae90c8c 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -12,10 +12,11 @@ import ( func addOIDCIDPRequestToDomain(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.IDPConfig { return &domain.IDPConfig{ - Name: req.Name, - OIDCConfig: addOIDCIDPRequestToDomainOIDCIDPConfig(req), - StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType), - Type: domain.IDPConfigTypeOIDC, + Name: req.Name, + OIDCConfig: addOIDCIDPRequestToDomainOIDCIDPConfig(req), + StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType), + Type: domain.IDPConfigTypeOIDC, + AutoRegister: req.AutoRegister, } } @@ -30,6 +31,25 @@ func addOIDCIDPRequestToDomainOIDCIDPConfig(req *mgmt_pb.AddOrgOIDCIDPRequest) * } } +func addJWTIDPRequestToDomain(req *mgmt_pb.AddOrgJWTIDPRequest) *domain.IDPConfig { + return &domain.IDPConfig{ + Name: req.Name, + JWTConfig: addJWTIDPRequestToDomainJWTIDPConfig(req), + StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType), + Type: domain.IDPConfigTypeJWT, + AutoRegister: req.AutoRegister, + } +} + +func addJWTIDPRequestToDomainJWTIDPConfig(req *mgmt_pb.AddOrgJWTIDPRequest) *domain.JWTIDPConfig { + return &domain.JWTIDPConfig{ + JWTEndpoint: req.JwtEndpoint, + Issuer: req.Issuer, + KeysEndpoint: req.KeysEndpoint, + HeaderName: req.HeaderName, + } +} + func updateIDPToDomain(req *mgmt_pb.UpdateOrgIDPRequest) *domain.IDPConfig { return &domain.IDPConfig{ IDPConfigID: req.IdpId, @@ -51,6 +71,16 @@ func updateOIDCConfigToDomain(req *mgmt_pb.UpdateOrgIDPOIDCConfigRequest) *domai } } +func updateJWTConfigToDomain(req *mgmt_pb.UpdateOrgIDPJWTConfigRequest) *domain.JWTIDPConfig { + return &domain.JWTIDPConfig{ + IDPConfigID: req.IdpId, + JWTEndpoint: req.JwtEndpoint, + Issuer: req.Issuer, + KeysEndpoint: req.KeysEndpoint, + HeaderName: req.HeaderName, + } +} + func listIDPsToModel(req *mgmt_pb.ListOrgIDPsRequest) *iam_model.IDPConfigSearchRequest { offset, limit, asc := object.ListQueryToModel(req.Query) return &iam_model.IDPConfigSearchRequest{ diff --git a/internal/api/grpc/management/idp_converter_test.go b/internal/api/grpc/management/idp_converter_test.go index fe5547aeae..109cf7229e 100644 --- a/internal/api/grpc/management/idp_converter_test.go +++ b/internal/api/grpc/management/idp_converter_test.go @@ -46,7 +46,7 @@ func Test_addOIDCIDPRequestToDomain(t *testing.T) { "OIDCConfig.AuthorizationEndpoint", "OIDCConfig.TokenEndpoint", "Type", //TODO: default (0) is oidc - "AutoRegister", + "JWTConfig", ) }) } @@ -114,6 +114,7 @@ func Test_updateIDPToDomain(t *testing.T) { test.AssertFieldsMapped(t, got, "ObjectRoot", "OIDCConfig", + "JWTConfig", "State", "Type", //TODO: type should not be changeable ) diff --git a/internal/auth/repository/eventsourcing/handler/idp_config.go b/internal/auth/repository/eventsourcing/handler/idp_config.go index 20798e26ee..a3b94d22a2 100644 --- a/internal/auth/repository/eventsourcing/handler/idp_config.go +++ b/internal/auth/repository/eventsourcing/handler/idp_config.go @@ -10,6 +10,8 @@ import ( iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/org" ) const ( @@ -88,7 +90,9 @@ func (i *IDPConfig) processIdpConfig(providerType iam_model.IDPProviderType, eve err = idp.AppendEvent(providerType, event) case model.IDPConfigChanged, iam_es_model.IDPConfigChanged, model.OIDCIDPConfigAdded, iam_es_model.OIDCIDPConfigAdded, - model.OIDCIDPConfigChanged, iam_es_model.OIDCIDPConfigChanged: + model.OIDCIDPConfigChanged, iam_es_model.OIDCIDPConfigChanged, + es_models.EventType(org.IDPJWTConfigAddedEventType), es_models.EventType(iam.IDPJWTConfigAddedEventType), + es_models.EventType(org.IDPJWTConfigChangedEventType), es_models.EventType(iam.IDPJWTConfigChangedEventType): err = idp.SetData(event) if err != nil { return err diff --git a/internal/command/iam_converter.go b/internal/command/iam_converter.go index eefbb8077f..62099ec3e7 100644 --- a/internal/command/iam_converter.go +++ b/internal/command/iam_converter.go @@ -153,6 +153,17 @@ func writeModelToIDPOIDCConfig(wm *OIDCConfigWriteModel) *domain.OIDCIDPConfig { } } +func writeModelToIDPJWTConfig(wm *JWTConfigWriteModel) *domain.JWTIDPConfig { + return &domain.JWTIDPConfig{ + ObjectRoot: writeModelToObjectRoot(wm.WriteModel), + IDPConfigID: wm.IDPConfigID, + JWTEndpoint: wm.JWTEndpoint, + Issuer: wm.Issuer, + KeysEndpoint: wm.KeysEndpoint, + HeaderName: wm.HeaderName, + } +} + func writeModelToIDPProvider(wm *IdentityProviderWriteModel) *domain.IDPProvider { return &domain.IDPProvider{ ObjectRoot: writeModelToObjectRoot(wm.WriteModel), diff --git a/internal/command/iam_idp_config.go b/internal/command/iam_idp_config.go index 86f630222a..4b65dddec0 100644 --- a/internal/command/iam_idp_config.go +++ b/internal/command/iam_idp_config.go @@ -2,6 +2,7 @@ package command import ( "context" + "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -13,7 +14,7 @@ import ( ) func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPConfig) (*domain.IDPConfig, error) { - if config.OIDCConfig == nil { + if config.OIDCConfig == nil && config.JWTConfig == nil { return nil, errors.ThrowInvalidArgument(nil, "IAM-eUpQU", "Errors.idp.config.notset") } @@ -23,11 +24,6 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo } addedConfig := NewIAMIDPConfigWriteModel(idpConfigID) - clientSecret, err := crypto.Encrypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto) - if err != nil { - return nil, err - } - iamAgg := IAMAggregateFromWriteModel(&addedConfig.WriteModel) events := []eventstore.EventPusher{ iam_repo.NewIDPConfigAddedEvent( @@ -39,7 +35,14 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo config.StylingType, config.AutoRegister, ), - iam_repo.NewIDPOIDCConfigAddedEvent( + } + if config.OIDCConfig != nil { + clientSecret, err := crypto.Encrypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto) + if err != nil { + return nil, err + } + + events = append(events, iam_repo.NewIDPOIDCConfigAddedEvent( ctx, iamAgg, config.OIDCConfig.ClientID, @@ -51,9 +54,18 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo config.OIDCConfig.IDPDisplayNameMapping, config.OIDCConfig.UsernameMapping, config.OIDCConfig.Scopes..., - ), + )) + } else if config.JWTConfig != nil { + events = append(events, iam_repo.NewIDPJWTConfigAddedEvent( + ctx, + iamAgg, + idpConfigID, + config.JWTConfig.JWTEndpoint, + config.JWTConfig.Issuer, + config.JWTConfig.KeysEndpoint, + config.JWTConfig.HeaderName, + )) } - pushedEvents, err := c.eventstore.PushEvents(ctx, events...) if err != nil { return nil, err diff --git a/internal/command/iam_idp_config_test.go b/internal/command/iam_idp_config_test.go index 7aa781e6ca..68fc5f0f5b 100644 --- a/internal/command/iam_idp_config_test.go +++ b/internal/command/iam_idp_config_test.go @@ -129,6 +129,65 @@ func TestCommandSide_AddDefaultIDPConfig(t *testing.T) { }, }, }, + { + name: "idp config jwt add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + iam.NewIDPJWTConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + }, + uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name1", "IAM")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "config1"), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{ + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + JWTConfig: &domain.JWTIDPConfig{ + JWTEndpoint: "jwt-endpoint", + Issuer: "issuer", + KeysEndpoint: "keys-endpoint", + HeaderName: "auth", + }, + }, + }, + res: res{ + want: &domain.IDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + IDPConfigID: "config1", + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + State: domain.IDPConfigStateActive, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/command/iam_idp_jwt_config.go b/internal/command/iam_idp_jwt_config.go new file mode 100644 index 0000000000..cafd69aae6 --- /dev/null +++ b/internal/command/iam_idp_jwt_config.go @@ -0,0 +1,49 @@ +package command + +import ( + "context" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" +) + +func (c *Commands) ChangeDefaultIDPJWTConfig(ctx context.Context, config *domain.JWTIDPConfig) (*domain.JWTIDPConfig, error) { + if config.IDPConfigID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-m9322", "Errors.IDMissing") + } + existingConfig := NewIAMIDPJWTConfigWriteModel(config.IDPConfigID) + err := c.eventstore.FilterToQueryReducer(ctx, existingConfig) + if err != nil { + return nil, err + } + + if existingConfig.State == domain.IDPConfigStateRemoved || existingConfig.State == domain.IDPConfigStateUnspecified { + return nil, caos_errs.ThrowNotFound(nil, "IAM-2m00d", "Errors.IAM.IDPConfig.AlreadyExists") + } + + iamAgg := IAMAggregateFromWriteModel(&existingConfig.WriteModel) + changedEvent, hasChanged, err := existingConfig.NewChangedEvent( + ctx, + iamAgg, + config.IDPConfigID, + config.JWTEndpoint, + config.Issuer, + config.KeysEndpoint, + config.HeaderName) + if err != nil { + return nil, err + } + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-3n9gg", "Errors.IAM.IDPConfig.NotChanged") + } + + pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingConfig, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToIDPJWTConfig(&existingConfig.JWTConfigWriteModel), nil +} diff --git a/internal/command/iam_idp_jwt_config_model.go b/internal/command/iam_idp_jwt_config_model.go new file mode 100644 index 0000000000..05871eb9cc --- /dev/null +++ b/internal/command/iam_idp_jwt_config_model.go @@ -0,0 +1,116 @@ +package command + +import ( + "context" + + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +type IAMIDPJWTConfigWriteModel struct { + JWTConfigWriteModel +} + +func NewIAMIDPJWTConfigWriteModel(idpConfigID string) *IAMIDPJWTConfigWriteModel { + return &IAMIDPJWTConfigWriteModel{ + JWTConfigWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: domain.IAMID, + ResourceOwner: domain.IAMID, + }, + IDPConfigID: idpConfigID, + }, + } +} + +func (wm *IAMIDPJWTConfigWriteModel) AppendEvents(events ...eventstore.EventReader) { + for _, event := range events { + switch e := event.(type) { + case *iam.IDPJWTConfigAddedEvent: + if wm.IDPConfigID != e.IDPConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigAddedEvent) + case *iam.IDPJWTConfigChangedEvent: + if wm.IDPConfigID != e.IDPConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigChangedEvent) + case *iam.IDPConfigReactivatedEvent: + if wm.IDPConfigID != e.ConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigReactivatedEvent) + case *iam.IDPConfigDeactivatedEvent: + if wm.IDPConfigID != e.ConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigDeactivatedEvent) + case *iam.IDPConfigRemovedEvent: + if wm.IDPConfigID != e.ConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigRemovedEvent) + default: + wm.JWTConfigWriteModel.AppendEvents(e) + } + } +} + +func (wm *IAMIDPJWTConfigWriteModel) Reduce() error { + if err := wm.JWTConfigWriteModel.Reduce(); err != nil { + return err + } + return wm.WriteModel.Reduce() +} + +func (wm *IAMIDPJWTConfigWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(iam.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + iam.IDPJWTConfigAddedEventType, + iam.IDPJWTConfigChangedEventType, + iam.IDPConfigReactivatedEventType, + iam.IDPConfigDeactivatedEventType, + iam.IDPConfigRemovedEventType). + Builder() +} + +func (wm *IAMIDPJWTConfigWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpConfigID, + jwtEndpoint, + issuer, + keysEndpoint, + headerName string, +) (*iam.IDPJWTConfigChangedEvent, bool, error) { + + changes := make([]idpconfig.JWTConfigChanges, 0) + if wm.JWTEndpoint != jwtEndpoint { + changes = append(changes, idpconfig.ChangeJWTEndpoint(jwtEndpoint)) + } + if wm.Issuer != issuer { + changes = append(changes, idpconfig.ChangeJWTIssuer(issuer)) + } + if wm.KeysEndpoint != keysEndpoint { + changes = append(changes, idpconfig.ChangeKeysEndpoint(keysEndpoint)) + } + if wm.HeaderName != headerName { + changes = append(changes, idpconfig.ChangeHeaderName(headerName)) + } + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := iam.NewIDPJWTConfigChangedEvent(ctx, aggregate, idpConfigID, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil +} diff --git a/internal/command/iam_idp_jwt_config_test.go b/internal/command/iam_idp_jwt_config_test.go new file mode 100644 index 0000000000..fd6e1798fd --- /dev/null +++ b/internal/command/iam_idp_jwt_config_test.go @@ -0,0 +1,264 @@ +package command + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +func TestCommandSide_ChangeDefaultIDPJWTConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type ( + args struct { + ctx context.Context + config *domain.JWTIDPConfig + } + ) + type res struct { + want *domain.JWTIDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "idp config not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "idp config removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeJWT, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + iam.NewIDPJWTConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + eventFromEventPusher( + iam.NewIDPConfigRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeJWT, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + iam.NewIDPJWTConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + JWTEndpoint: "jwt-endpoint", + Issuer: "issuer", + KeysEndpoint: "keys-endpoint", + HeaderName: "auth", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "idp config jwt add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeJWT, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + iam.NewIDPJWTConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultIDPJWTConfigChangedEvent(context.Background(), + "config1", + "jwt-endpoint-changed", + "issuer-changed", + "keys-endpoint-changed", + "auth-changed", + ), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + JWTEndpoint: "jwt-endpoint-changed", + Issuer: "issuer-changed", + KeysEndpoint: "keys-endpoint-changed", + HeaderName: "auth-changed", + }, + }, + res: res{ + want: &domain.JWTIDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + IDPConfigID: "config1", + JWTEndpoint: "jwt-endpoint-changed", + Issuer: "issuer-changed", + KeysEndpoint: "keys-endpoint-changed", + HeaderName: "auth-changed", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigSecretCrypto: tt.fields.secretCrypto, + } + got, err := r.ChangeDefaultIDPJWTConfig(tt.args.ctx, tt.args.config) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newDefaultIDPJWTConfigChangedEvent(ctx context.Context, configID, jwtEndpoint, issuer, keysEndpoint, headerName string) *iam.IDPJWTConfigChangedEvent { + event, _ := iam.NewIDPJWTConfigChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + configID, + []idpconfig.JWTConfigChanges{ + idpconfig.ChangeJWTEndpoint(jwtEndpoint), + idpconfig.ChangeJWTIssuer(issuer), + idpconfig.ChangeKeysEndpoint(keysEndpoint), + idpconfig.ChangeHeaderName(headerName), + }, + ) + return event +} diff --git a/internal/command/jwt_config_model.go b/internal/command/jwt_config_model.go new file mode 100644 index 0000000000..8ce0f9fb77 --- /dev/null +++ b/internal/command/jwt_config_model.go @@ -0,0 +1,61 @@ +package command + +import ( + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +type JWTConfigWriteModel struct { + eventstore.WriteModel + + IDPConfigID string + JWTEndpoint string + Issuer string + KeysEndpoint string + HeaderName string + State domain.IDPConfigState +} + +func (wm *JWTConfigWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *idpconfig.JWTConfigAddedEvent: + wm.reduceConfigAddedEvent(e) + case *idpconfig.JWTConfigChangedEvent: + wm.reduceConfigChangedEvent(e) + case *idpconfig.IDPConfigDeactivatedEvent: + wm.State = domain.IDPConfigStateInactive + case *idpconfig.IDPConfigReactivatedEvent: + wm.State = domain.IDPConfigStateActive + case *idpconfig.IDPConfigRemovedEvent: + wm.State = domain.IDPConfigStateRemoved + } + } + + return wm.WriteModel.Reduce() +} + +func (wm *JWTConfigWriteModel) reduceConfigAddedEvent(e *idpconfig.JWTConfigAddedEvent) { + wm.IDPConfigID = e.IDPConfigID + wm.JWTEndpoint = e.JWTEndpoint + wm.Issuer = e.Issuer + wm.KeysEndpoint = e.KeysEndpoint + wm.HeaderName = e.HeaderName + wm.State = domain.IDPConfigStateActive +} + +func (wm *JWTConfigWriteModel) reduceConfigChangedEvent(e *idpconfig.JWTConfigChangedEvent) { + if e.JWTEndpoint != nil { + wm.JWTEndpoint = *e.JWTEndpoint + } + if e.Issuer != nil { + wm.Issuer = *e.Issuer + } + if e.KeysEndpoint != nil { + wm.KeysEndpoint = *e.KeysEndpoint + } + if e.HeaderName != nil { + wm.HeaderName = *e.HeaderName + } +} diff --git a/internal/command/org_idp_config.go b/internal/command/org_idp_config.go index 2a15ab9076..73f8242414 100644 --- a/internal/command/org_idp_config.go +++ b/internal/command/org_idp_config.go @@ -2,6 +2,7 @@ package command import ( "context" + "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -16,7 +17,7 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-0j8gs", "Errors.ResourceOwnerMissing") } - if config.OIDCConfig == nil { + if config.OIDCConfig == nil && config.JWTConfig == nil { return nil, errors.ThrowInvalidArgument(nil, "Org-eUpQU", "Errors.idp.config.notset") } @@ -26,11 +27,6 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r } addedConfig := NewOrgIDPConfigWriteModel(idpConfigID, resourceOwner) - clientSecret, err := crypto.Crypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto) - if err != nil { - return nil, err - } - orgAgg := OrgAggregateFromWriteModel(&addedConfig.WriteModel) events := []eventstore.EventPusher{ org_repo.NewIDPConfigAddedEvent( @@ -42,7 +38,13 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r config.StylingType, config.AutoRegister, ), - org_repo.NewIDPOIDCConfigAddedEvent( + } + if config.OIDCConfig != nil { + clientSecret, err := crypto.Crypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto) + if err != nil { + return nil, err + } + events = append(events, org_repo.NewIDPOIDCConfigAddedEvent( ctx, orgAgg, config.OIDCConfig.ClientID, @@ -53,7 +55,17 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r clientSecret, config.OIDCConfig.IDPDisplayNameMapping, config.OIDCConfig.UsernameMapping, - config.OIDCConfig.Scopes...), + config.OIDCConfig.Scopes...)) + } else if config.JWTConfig != nil { + events = append(events, org_repo.NewIDPJWTConfigAddedEvent( + ctx, + orgAgg, + idpConfigID, + config.JWTConfig.JWTEndpoint, + config.JWTConfig.Issuer, + config.JWTConfig.KeysEndpoint, + config.JWTConfig.HeaderName, + )) } pushedEvents, err := c.eventstore.PushEvents(ctx, events...) if err != nil { diff --git a/internal/command/org_idp_config_test.go b/internal/command/org_idp_config_test.go index ffef7899d8..b19f418e6b 100644 --- a/internal/command/org_idp_config_test.go +++ b/internal/command/org_idp_config_test.go @@ -159,6 +159,66 @@ func TestCommandSide_AddIDPConfig(t *testing.T) { }, }, }, + { + name: "idp config jwt add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + org.NewIDPJWTConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + }, + uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name1", "org1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "config1"), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.IDPConfig{ + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + JWTConfig: &domain.JWTIDPConfig{ + JWTEndpoint: "jwt-endpoint", + Issuer: "issuer", + KeysEndpoint: "keys-endpoint", + HeaderName: "auth", + }, + }, + }, + res: res{ + want: &domain.IDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + IDPConfigID: "config1", + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + State: domain.IDPConfigStateActive, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/command/org_idp_jwt_config.go b/internal/command/org_idp_jwt_config.go new file mode 100644 index 0000000000..d96e9dd8db --- /dev/null +++ b/internal/command/org_idp_jwt_config.go @@ -0,0 +1,53 @@ +package command + +import ( + "context" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" +) + +func (c *Commands) ChangeIDPJWTConfig(ctx context.Context, config *domain.JWTIDPConfig, resourceOwner string) (*domain.JWTIDPConfig, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-ff8NF", "Errors.ResourceOwnerMissing") + } + if config.IDPConfigID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-2n99f", "Errors.IDMissing") + } + existingConfig := NewOrgIDPJWTConfigWriteModel(config.IDPConfigID, resourceOwner) + err := c.eventstore.FilterToQueryReducer(ctx, existingConfig) + if err != nil { + return nil, err + } + + if existingConfig.State == domain.IDPConfigStateRemoved || existingConfig.State == domain.IDPConfigStateUnspecified { + return nil, caos_errs.ThrowNotFound(nil, "Org-67J9d", "Errors.Org.IDPConfig.AlreadyExists") + } + + orgAgg := OrgAggregateFromWriteModel(&existingConfig.WriteModel) + changedEvent, hasChanged, err := existingConfig.NewChangedEvent( + ctx, + orgAgg, + config.IDPConfigID, + config.JWTEndpoint, + config.Issuer, + config.KeysEndpoint, + config.HeaderName) + if err != nil { + return nil, err + } + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-2k9fs", "Errors.Org.IDPConfig.NotChanged") + } + + pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingConfig, pushedEvents...) + if err != nil { + return nil, err + } + + return writeModelToIDPJWTConfig(&existingConfig.JWTConfigWriteModel), nil +} diff --git a/internal/command/org_idp_jwt_config_model.go b/internal/command/org_idp_jwt_config_model.go new file mode 100644 index 0000000000..00dd75d912 --- /dev/null +++ b/internal/command/org_idp_jwt_config_model.go @@ -0,0 +1,115 @@ +package command + +import ( + "context" + + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/repository/idpconfig" + "github.com/caos/zitadel/internal/repository/org" +) + +type IDPJWTConfigWriteModel struct { + JWTConfigWriteModel +} + +func NewOrgIDPJWTConfigWriteModel(idpConfigID, orgID string) *IDPJWTConfigWriteModel { + return &IDPJWTConfigWriteModel{ + JWTConfigWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + IDPConfigID: idpConfigID, + }, + } +} + +func (wm *IDPJWTConfigWriteModel) AppendEvents(events ...eventstore.EventReader) { + for _, event := range events { + switch e := event.(type) { + case *org.IDPJWTConfigAddedEvent: + if wm.IDPConfigID != e.IDPConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigAddedEvent) + case *org.IDPJWTConfigChangedEvent: + if wm.IDPConfigID != e.IDPConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigChangedEvent) + case *org.IDPConfigReactivatedEvent: + if wm.IDPConfigID != e.ConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigReactivatedEvent) + case *org.IDPConfigDeactivatedEvent: + if wm.IDPConfigID != e.ConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigDeactivatedEvent) + case *org.IDPConfigRemovedEvent: + if wm.IDPConfigID != e.ConfigID { + continue + } + wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigRemovedEvent) + default: + wm.JWTConfigWriteModel.AppendEvents(e) + } + } +} + +func (wm *IDPJWTConfigWriteModel) Reduce() error { + if err := wm.JWTConfigWriteModel.Reduce(); err != nil { + return err + } + return wm.WriteModel.Reduce() +} + +func (wm *IDPJWTConfigWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + org.IDPJWTConfigAddedEventType, + org.IDPJWTConfigChangedEventType, + org.IDPConfigReactivatedEventType, + org.IDPConfigDeactivatedEventType, + org.IDPConfigRemovedEventType). + Builder() +} + +func (wm *IDPJWTConfigWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpConfigID, + jwtEndpoint, + issuer, + keysEndpoint, + headerName string, +) (*org.IDPJWTConfigChangedEvent, bool, error) { + + changes := make([]idpconfig.JWTConfigChanges, 0) + if wm.JWTEndpoint != jwtEndpoint { + changes = append(changes, idpconfig.ChangeJWTEndpoint(jwtEndpoint)) + } + if wm.Issuer != issuer { + changes = append(changes, idpconfig.ChangeJWTIssuer(issuer)) + } + if wm.KeysEndpoint != keysEndpoint { + changes = append(changes, idpconfig.ChangeKeysEndpoint(keysEndpoint)) + } + if wm.HeaderName != headerName { + changes = append(changes, idpconfig.ChangeHeaderName(headerName)) + } + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := org.NewIDPJWTConfigChangedEvent(ctx, aggregate, idpConfigID, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil +} diff --git a/internal/command/org_idp_jwt_config_test.go b/internal/command/org_idp_jwt_config_test.go new file mode 100644 index 0000000000..bf50e0f549 --- /dev/null +++ b/internal/command/org_idp_jwt_config_test.go @@ -0,0 +1,288 @@ +package command + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/idpconfig" + "github.com/caos/zitadel/internal/repository/org" +) + +func TestCommandSide_ChangeIDPJWTConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type ( + args struct { + ctx context.Context + config *domain.JWTIDPConfig + resourceOwner string + } + ) + type res struct { + want *domain.JWTIDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "idp config not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "idp config removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeJWT, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + org.NewIDPJWTConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + eventFromEventPusher( + org.NewIDPConfigRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeJWT, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + org.NewIDPJWTConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + JWTEndpoint: "jwt-endpoint", + Issuer: "issuer", + KeysEndpoint: "keys-endpoint", + HeaderName: "auth", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "idp config jwt add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeJWT, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + eventFromEventPusher( + org.NewIDPJWTConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "jwt-endpoint", + "issuer", + "keys-endpoint", + "auth", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newIDPJWTConfigChangedEvent(context.Background(), + "org1", + "config1", + "jwt-endpoint-changed", + "issuer-changed", + "keys-endpoint-changed", + "auth-changed", + ), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.JWTIDPConfig{ + IDPConfigID: "config1", + JWTEndpoint: "jwt-endpoint-changed", + Issuer: "issuer-changed", + KeysEndpoint: "keys-endpoint-changed", + HeaderName: "auth-changed", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.JWTIDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + IDPConfigID: "config1", + JWTEndpoint: "jwt-endpoint-changed", + Issuer: "issuer-changed", + KeysEndpoint: "keys-endpoint-changed", + HeaderName: "auth-changed", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigSecretCrypto: tt.fields.secretCrypto, + } + got, err := r.ChangeIDPJWTConfig(tt.args.ctx, tt.args.config, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newIDPJWTConfigChangedEvent(ctx context.Context, orgID, configID, jwtEndpoint, issuer, keysEndpoint, headerName string) *org.IDPJWTConfigChangedEvent { + event, _ := org.NewIDPJWTConfigChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + configID, + []idpconfig.JWTConfigChanges{ + idpconfig.ChangeJWTEndpoint(jwtEndpoint), + idpconfig.ChangeJWTIssuer(issuer), + idpconfig.ChangeKeysEndpoint(keysEndpoint), + idpconfig.ChangeHeaderName(headerName), + }, + ) + return event +} diff --git a/internal/domain/idp_config.go b/internal/domain/idp_config.go index b1ce3178a0..e29337b332 100644 --- a/internal/domain/idp_config.go +++ b/internal/domain/idp_config.go @@ -15,6 +15,7 @@ type IDPConfig struct { StylingType IDPConfigStylingType State IDPConfigState OIDCConfig *OIDCIDPConfig + JWTConfig *JWTIDPConfig AutoRegister bool } @@ -39,6 +40,10 @@ type IDPConfigView struct { OIDCUsernameMapping OIDCMappingField OAuthAuthorizationEndpoint string OAuthTokenEndpoint string + + JWTEndpoint string + JWTIssuer string + JWTKeysEndpoint string } type OIDCIDPConfig struct { @@ -55,11 +60,21 @@ type OIDCIDPConfig struct { UsernameMapping OIDCMappingField } +type JWTIDPConfig struct { + es_models.ObjectRoot + IDPConfigID string + JWTEndpoint string + Issuer string + KeysEndpoint string + HeaderName string +} + type IDPConfigType int32 const ( IDPConfigTypeOIDC IDPConfigType = iota IDPConfigTypeSAML + IDPConfigTypeJWT //count is for validation idpConfigTypeCount diff --git a/internal/iam/model/idp_config.go b/internal/iam/model/idp_config.go index c02e15ecb6..51ba1bb243 100644 --- a/internal/iam/model/idp_config.go +++ b/internal/iam/model/idp_config.go @@ -7,12 +7,13 @@ import ( type IDPConfig struct { es_models.ObjectRoot - IDPConfigID string - Type IdpConfigType - Name string - StylingType IDPStylingType - State IDPConfigState - OIDCConfig *OIDCIDPConfig + IDPConfigID string + Type IdpConfigType + Name string + StylingType IDPStylingType + State IDPConfigState + OIDCConfig *OIDCIDPConfig + JWTIDPConfig *JWTIDPConfig } type OIDCIDPConfig struct { @@ -27,11 +28,20 @@ type OIDCIDPConfig struct { UsernameMapping OIDCMappingField } +type JWTIDPConfig struct { + es_models.ObjectRoot + IDPConfigID string + JWTEndpoint string + Issuer string + KeysEndpoint string +} + type IdpConfigType int32 const ( IDPConfigTypeOIDC IdpConfigType = iota IDPConfigTypeSAML + IDPConfigTypeJWT ) type IDPConfigState int32 diff --git a/internal/iam/model/idp_config_view.go b/internal/iam/model/idp_config_view.go index 9c7cbb25af..a681be3b1e 100644 --- a/internal/iam/model/idp_config_view.go +++ b/internal/iam/model/idp_config_view.go @@ -29,6 +29,10 @@ type IDPConfigView struct { OIDCUsernameMapping OIDCMappingField OAuthAuthorizationEndpoint string OAuthTokenEndpoint string + JWTEndpoint string + JWTIssuer string + JWTKeysEndpoint string + JWTHeaderName string } type IDPConfigSearchRequest struct { diff --git a/internal/iam/model/idp_provider_view.go b/internal/iam/model/idp_provider_view.go index 4b2be75eb1..b40760a6a3 100644 --- a/internal/iam/model/idp_provider_view.go +++ b/internal/iam/model/idp_provider_view.go @@ -100,6 +100,8 @@ func idpConfigTypeToDomain(idpType IdpConfigType) domain.IDPConfigType { return domain.IDPConfigTypeOIDC case IDPConfigTypeSAML: return domain.IDPConfigTypeSAML + case IDPConfigTypeJWT: + return domain.IDPConfigTypeJWT default: return domain.IDPConfigTypeOIDC } diff --git a/internal/iam/repository/view/model/idp_config.go b/internal/iam/repository/view/model/idp_config.go index 78cfab6cb9..49c44ae717 100644 --- a/internal/iam/repository/view/model/idp_config.go +++ b/internal/iam/repository/view/model/idp_config.go @@ -5,6 +5,8 @@ import ( "time" "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/org" es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" @@ -44,12 +46,15 @@ type IDPConfigView struct { OIDCUsernameMapping int32 `json:"usernameMapping" gorm:"column:oidc_idp_username_mapping"` OAuthAuthorizationEndpoint string `json:"authorizationEndpoint" gorm:"column:oauth_authorization_endpoint"` OAuthTokenEndpoint string `json:"tokenEndpoint" gorm:"column:oauth_token_endpoint"` + JWTEndpoint string `json:"jwtEndpoint" gorm:"jwt_endpoint"` + JWTKeysEndpoint string `json:"keysEndpoint" gorm:"jwt_keys_endpoint"` + JWTHeaderName string `json:"headerName" gorm:"jwt_header_name"` Sequence uint64 `json:"-" gorm:"column:sequence"` } func IDPConfigViewToModel(idp *IDPConfigView) *model.IDPConfigView { - return &model.IDPConfigView{ + view := &model.IDPConfigView{ IDPConfigID: idp.IDPConfigID, AggregateID: idp.AggregateID, State: model.IDPConfigState(idp.IDPState), @@ -63,13 +68,21 @@ func IDPConfigViewToModel(idp *IDPConfigView) *model.IDPConfigView { IsOIDC: idp.IsOIDC, OIDCClientID: idp.OIDCClientID, OIDCClientSecret: idp.OIDCClientSecret, - OIDCIssuer: idp.OIDCIssuer, OIDCScopes: idp.OIDCScopes, OIDCIDPDisplayNameMapping: model.OIDCMappingField(idp.OIDCIDPDisplayNameMapping), OIDCUsernameMapping: model.OIDCMappingField(idp.OIDCUsernameMapping), OAuthAuthorizationEndpoint: idp.OAuthAuthorizationEndpoint, OAuthTokenEndpoint: idp.OAuthTokenEndpoint, } + if idp.IsOIDC { + view.OIDCIssuer = idp.OIDCIssuer + return view + } + view.JWTEndpoint = idp.JWTEndpoint + view.JWTIssuer = idp.OIDCIssuer + view.JWTKeysEndpoint = idp.JWTKeysEndpoint + view.JWTHeaderName = idp.JWTHeaderName + return view } func IdpConfigViewsToModel(idps []*IDPConfigView) []*model.IDPConfigView { @@ -93,7 +106,9 @@ func (i *IDPConfigView) AppendEvent(providerType model.IDPProviderType, event *m i.IsOIDC = true err = i.SetData(event) case es_model.OIDCIDPConfigChanged, org_es_model.OIDCIDPConfigChanged, - es_model.IDPConfigChanged, org_es_model.IDPConfigChanged: + es_model.IDPConfigChanged, org_es_model.IDPConfigChanged, + models.EventType(org.IDPJWTConfigAddedEventType), models.EventType(iam.IDPJWTConfigAddedEventType), + models.EventType(org.IDPJWTConfigChangedEventType), models.EventType(iam.IDPJWTConfigChangedEventType): err = i.SetData(event) case es_model.IDPConfigDeactivated, org_es_model.IDPConfigDeactivated: i.IDPState = int32(model.IDPConfigStateInactive) diff --git a/internal/management/repository/eventsourcing/handler/idp_config.go b/internal/management/repository/eventsourcing/handler/idp_config.go index a900e56b25..4e510303e3 100644 --- a/internal/management/repository/eventsourcing/handler/idp_config.go +++ b/internal/management/repository/eventsourcing/handler/idp_config.go @@ -3,6 +3,8 @@ package handler import ( "github.com/caos/logging" "github.com/caos/zitadel/internal/eventstore/v1" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/org" es_models "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/eventstore/v1/query" @@ -89,7 +91,9 @@ func (m *IDPConfig) processIdpConfig(providerType iam_model.IDPProviderType, eve err = idp.AppendEvent(providerType, event) case model.IDPConfigChanged, iam_es_model.IDPConfigChanged, model.OIDCIDPConfigAdded, iam_es_model.OIDCIDPConfigAdded, - model.OIDCIDPConfigChanged, iam_es_model.OIDCIDPConfigChanged: + model.OIDCIDPConfigChanged, iam_es_model.OIDCIDPConfigChanged, + es_models.EventType(org.IDPJWTConfigAddedEventType), es_models.EventType(iam.IDPJWTConfigAddedEventType), + es_models.EventType(org.IDPJWTConfigChangedEventType), es_models.EventType(iam.IDPJWTConfigChangedEventType): err = idp.SetData(event) if err != nil { return err diff --git a/internal/query/converter.go b/internal/query/converter.go index 522d1587bc..31a80962c5 100644 --- a/internal/query/converter.go +++ b/internal/query/converter.go @@ -48,6 +48,11 @@ func readModelToIDPConfigView(rm *IAMIDPConfigReadModel) *domain.IDPConfigView { converted.OAuthAuthorizationEndpoint = rm.OIDCConfig.AuthorizationEndpoint converted.OAuthTokenEndpoint = rm.OIDCConfig.TokenEndpoint } + if rm.JWTConfig != nil { + converted.JWTEndpoint = rm.JWTConfig.JWTEndpoint + converted.JWTIssuer = rm.JWTConfig.Issuer + converted.JWTKeysEndpoint = rm.JWTConfig.KeysEndpoint + } return converted } @@ -138,14 +143,20 @@ func readModelToIDPConfigs(rm *IAMIDPConfigsReadModel) []*model.IDPConfig { } func readModelToIDPConfig(rm *IAMIDPConfigReadModel) *model.IDPConfig { - return &model.IDPConfig{ + config := &model.IDPConfig{ ObjectRoot: readModelToObjectRoot(rm.ReadModel), - OIDCConfig: readModelToIDPOIDCConfig(rm.OIDCConfig), IDPConfigID: rm.ConfigID, Name: rm.Name, State: model.IDPConfigState(rm.State), StylingType: model.IDPStylingType(rm.StylingType), } + if rm.OIDCConfig != nil { + config.OIDCConfig = readModelToIDPOIDCConfig(rm.OIDCConfig) + } + if rm.JWTConfig != nil { + config.JWTIDPConfig = readModelToIDPJWTConfig(rm.JWTConfig) + } + return config } func readModelToIDPOIDCConfig(rm *OIDCConfigReadModel) *model.OIDCIDPConfig { @@ -162,6 +173,16 @@ func readModelToIDPOIDCConfig(rm *OIDCConfigReadModel) *model.OIDCIDPConfig { } } +func readModelToIDPJWTConfig(rm *JWTConfigReadModel) *model.JWTIDPConfig { + return &model.JWTIDPConfig{ + ObjectRoot: readModelToObjectRoot(rm.ReadModel), + IDPConfigID: rm.IDPConfigID, + JWTEndpoint: rm.JWTEndpoint, + Issuer: rm.Issuer, + KeysEndpoint: rm.KeysEndpoint, + } +} + func readModelToObjectRoot(readModel eventstore.ReadModel) models.ObjectRoot { return models.ObjectRoot{ AggregateID: readModel.AggregateID, diff --git a/internal/query/iam_idp_config_model.go b/internal/query/iam_idp_config_model.go index fa4b75ad89..f303c5217b 100644 --- a/internal/query/iam_idp_config_model.go +++ b/internal/query/iam_idp_config_model.go @@ -36,6 +36,10 @@ func (rm *IAMIDPConfigReadModel) AppendEvents(events ...eventstore.EventReader) rm.IDPConfigReadModel.AppendEvents(&e.OIDCConfigAddedEvent) case *iam.IDPOIDCConfigChangedEvent: rm.IDPConfigReadModel.AppendEvents(&e.OIDCConfigChangedEvent) + case *iam.IDPJWTConfigAddedEvent: + rm.IDPConfigReadModel.AppendEvents(&e.JWTConfigAddedEvent) + case *iam.IDPJWTConfigChangedEvent: + rm.IDPConfigReadModel.AppendEvents(&e.JWTConfigChangedEvent) } } } diff --git a/internal/query/idp_config_model.go b/internal/query/idp_config_model.go index 0440699000..0ef9c7c9d7 100644 --- a/internal/query/idp_config_model.go +++ b/internal/query/idp_config_model.go @@ -17,6 +17,7 @@ type IDPConfigReadModel struct { ProviderType domain.IdentityProviderType OIDCConfig *OIDCConfigReadModel + JWTConfig *JWTConfigReadModel } func NewIDPConfigReadModel(configID string) *IDPConfigReadModel { @@ -45,6 +46,13 @@ func (rm *IDPConfigReadModel) AppendEvents(events ...eventstore.EventReader) { case *idpconfig.OIDCConfigChangedEvent: rm.ReadModel.AppendEvents(e) rm.OIDCConfig.AppendEvents(event) + case *idpconfig.JWTConfigAddedEvent: + rm.JWTConfig = &JWTConfigReadModel{} + rm.ReadModel.AppendEvents(e) + rm.JWTConfig.AppendEvents(event) + case *idpconfig.JWTConfigChangedEvent: + rm.ReadModel.AppendEvents(e) + rm.JWTConfig.AppendEvents(event) } } } @@ -70,6 +78,11 @@ func (rm *IDPConfigReadModel) Reduce() error { return err } } + if rm.JWTConfig != nil { + if err := rm.JWTConfig.Reduce(); err != nil { + return err + } + } return rm.ReadModel.Reduce() } diff --git a/internal/query/jwt_config_model.go b/internal/query/jwt_config_model.go new file mode 100644 index 0000000000..799fe0d08b --- /dev/null +++ b/internal/query/jwt_config_model.go @@ -0,0 +1,47 @@ +package query + +import ( + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +type JWTConfigReadModel struct { + eventstore.ReadModel + + IDPConfigID string + JWTEndpoint string + Issuer string + KeysEndpoint string +} + +func (rm *JWTConfigReadModel) Reduce() error { + for _, event := range rm.Events { + switch e := event.(type) { + case *idpconfig.JWTConfigAddedEvent: + rm.reduceConfigAddedEvent(e) + case *idpconfig.JWTConfigChangedEvent: + rm.reduceConfigChangedEvent(e) + } + } + + return rm.ReadModel.Reduce() +} + +func (rm *JWTConfigReadModel) reduceConfigAddedEvent(e *idpconfig.JWTConfigAddedEvent) { + rm.IDPConfigID = e.IDPConfigID + rm.JWTEndpoint = e.JWTEndpoint + rm.Issuer = e.Issuer + rm.KeysEndpoint = e.KeysEndpoint +} + +func (rm *JWTConfigReadModel) reduceConfigChangedEvent(e *idpconfig.JWTConfigChangedEvent) { + if e.JWTEndpoint != nil { + rm.JWTEndpoint = *e.JWTEndpoint + } + if e.Issuer != nil { + rm.Issuer = *e.Issuer + } + if e.KeysEndpoint != nil { + rm.KeysEndpoint = *e.KeysEndpoint + } +} diff --git a/internal/repository/iam/eventstore.go b/internal/repository/iam/eventstore.go index 587fbb6deb..c173a0eef3 100644 --- a/internal/repository/iam/eventstore.go +++ b/internal/repository/iam/eventstore.go @@ -47,6 +47,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(IDPConfigReactivatedEventType, IDPConfigReactivatedEventMapper). RegisterFilterEventMapper(IDPOIDCConfigAddedEventType, IDPOIDCConfigAddedEventMapper). RegisterFilterEventMapper(IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper). + RegisterFilterEventMapper(IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper). + RegisterFilterEventMapper(IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper). RegisterFilterEventMapper(LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper). RegisterFilterEventMapper(LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper). RegisterFilterEventMapper(LoginPolicyIDPProviderCascadeRemovedEventType, IdentityProviderCascadeRemovedEventMapper). diff --git a/internal/repository/iam/idp_jwt_config.go b/internal/repository/iam/idp_jwt_config.go new file mode 100644 index 0000000000..65dad0bd98 --- /dev/null +++ b/internal/repository/iam/idp_jwt_config.go @@ -0,0 +1,86 @@ +package iam + +import ( + "context" + + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +const ( + IDPJWTConfigAddedEventType eventstore.EventType = "iam.idp." + idpconfig.JWTConfigAddedEventType + IDPJWTConfigChangedEventType eventstore.EventType = "iam.idp." + idpconfig.JWTConfigChangedEventType +) + +type IDPJWTConfigAddedEvent struct { + idpconfig.JWTConfigAddedEvent +} + +func NewIDPJWTConfigAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpConfigID, + jwtEndpoint, + issuer, + keysEndpoint, + headerName string, +) *IDPJWTConfigAddedEvent { + return &IDPJWTConfigAddedEvent{ + JWTConfigAddedEvent: *idpconfig.NewJWTConfigAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + IDPJWTConfigAddedEventType, + ), + idpConfigID, + jwtEndpoint, + issuer, + keysEndpoint, + headerName, + ), + } +} + +func IDPJWTConfigAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := idpconfig.JWTConfigAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &IDPJWTConfigAddedEvent{JWTConfigAddedEvent: *e.(*idpconfig.JWTConfigAddedEvent)}, nil +} + +type IDPJWTConfigChangedEvent struct { + idpconfig.JWTConfigChangedEvent +} + +func NewIDPJWTConfigChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpConfigID string, + changes []idpconfig.JWTConfigChanges, +) (*IDPJWTConfigChangedEvent, error) { + changeEvent, err := idpconfig.NewJWTConfigChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + IDPJWTConfigChangedEventType), + idpConfigID, + changes, + ) + if err != nil { + return nil, err + } + return &IDPJWTConfigChangedEvent{JWTConfigChangedEvent: *changeEvent}, nil +} + +func IDPJWTConfigChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := idpconfig.JWTConfigChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &IDPJWTConfigChangedEvent{JWTConfigChangedEvent: *e.(*idpconfig.JWTConfigChangedEvent)}, nil +} diff --git a/internal/repository/iam/idp_oidc_config.go b/internal/repository/iam/idp_oidc_config.go index 19f8e2d137..02ed74cde6 100644 --- a/internal/repository/iam/idp_oidc_config.go +++ b/internal/repository/iam/idp_oidc_config.go @@ -12,7 +12,7 @@ import ( const ( IDPOIDCConfigAddedEventType eventstore.EventType = "iam.idp." + idpconfig.OIDCConfigAddedEventType - IDPOIDCConfigChangedEventType eventstore.EventType = "iam.idp." + idpconfig.ConfigChangedEventType + IDPOIDCConfigChangedEventType eventstore.EventType = "iam.idp." + idpconfig.OIDCConfigChangedEventType ) type IDPOIDCConfigAddedEvent struct { diff --git a/internal/repository/idpconfig/jwt_config.go b/internal/repository/idpconfig/jwt_config.go new file mode 100644 index 0000000000..1a27f693af --- /dev/null +++ b/internal/repository/idpconfig/jwt_config.go @@ -0,0 +1,140 @@ +package idpconfig + +import ( + "encoding/json" + + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/repository" +) + +const ( + JWTConfigAddedEventType eventstore.EventType = "jwt.config.added" + JWTConfigChangedEventType eventstore.EventType = "jwt.config.changed" +) + +type JWTConfigAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + IDPConfigID string `json:"idpConfigId"` + JWTEndpoint string `json:"jwtEndpoint,omitempty"` + Issuer string `json:"issuer,omitempty"` + KeysEndpoint string `json:"keysEndpoint,omitempty"` + HeaderName string `json:"headerName,omitempty"` +} + +func (e *JWTConfigAddedEvent) Data() interface{} { + return e +} + +func (e *JWTConfigAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewJWTConfigAddedEvent( + base *eventstore.BaseEvent, + idpConfigID, + jwtEndpoint, + issuer, + keysEndpoint, + headerName string, +) *JWTConfigAddedEvent { + return &JWTConfigAddedEvent{ + BaseEvent: *base, + IDPConfigID: idpConfigID, + JWTEndpoint: jwtEndpoint, + Issuer: issuer, + KeysEndpoint: keysEndpoint, + HeaderName: headerName, + } +} + +func JWTConfigAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e := &JWTConfigAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "JWT-m0fwf", "unable to unmarshal event") + } + + return e, nil +} + +type JWTConfigChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + IDPConfigID string `json:"idpConfigId"` + + JWTEndpoint *string `json:"jwtEndpoint,omitempty"` + Issuer *string `json:"issuer,omitempty"` + KeysEndpoint *string `json:"keysEndpoint,omitempty"` + HeaderName *string `json:"headerName,omitempty"` +} + +func (e *JWTConfigChangedEvent) Data() interface{} { + return e +} + +func (e *JWTConfigChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewJWTConfigChangedEvent( + base *eventstore.BaseEvent, + idpConfigID string, + changes []JWTConfigChanges, +) (*JWTConfigChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "IDPCONFIG-fn93s", "Errors.NoChangesFound") + } + changeEvent := &JWTConfigChangedEvent{ + BaseEvent: *base, + IDPConfigID: idpConfigID, + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type JWTConfigChanges func(*JWTConfigChangedEvent) + +func ChangeJWTEndpoint(jwtEndpoint string) func(*JWTConfigChangedEvent) { + return func(e *JWTConfigChangedEvent) { + e.JWTEndpoint = &jwtEndpoint + } +} + +func ChangeJWTIssuer(issuer string) func(*JWTConfigChangedEvent) { + return func(e *JWTConfigChangedEvent) { + e.Issuer = &issuer + } +} + +func ChangeKeysEndpoint(keysEndpoint string) func(*JWTConfigChangedEvent) { + return func(e *JWTConfigChangedEvent) { + e.KeysEndpoint = &keysEndpoint + } +} + +func ChangeHeaderName(headerName string) func(*JWTConfigChangedEvent) { + return func(e *JWTConfigChangedEvent) { + e.HeaderName = &headerName + } +} + +func JWTConfigChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e := &JWTConfigChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "JWT-fk3fs", "unable to unmarshal event") + } + + return e, nil +} diff --git a/internal/repository/idpconfig/oidc_config.go b/internal/repository/idpconfig/oidc_config.go index 9f04ab1afb..30040906e5 100644 --- a/internal/repository/idpconfig/oidc_config.go +++ b/internal/repository/idpconfig/oidc_config.go @@ -11,8 +11,8 @@ import ( ) const ( - OIDCConfigAddedEventType eventstore.EventType = "oidc.config.added" - ConfigChangedEventType eventstore.EventType = "oidc.config.changed" + OIDCConfigAddedEventType eventstore.EventType = "oidc.config.added" + OIDCConfigChangedEventType eventstore.EventType = "oidc.config.changed" ) type OIDCConfigAddedEvent struct { diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index ce4d76dba7..068906edea 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -75,6 +75,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(IDPConfigReactivatedEventType, IDPConfigReactivatedEventMapper). RegisterFilterEventMapper(IDPOIDCConfigAddedEventType, IDPOIDCConfigAddedEventMapper). RegisterFilterEventMapper(IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper). + RegisterFilterEventMapper(IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper). + RegisterFilterEventMapper(IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper). RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper). RegisterFilterEventMapper(FeaturesRemovedEventType, FeaturesRemovedEventMapper) } diff --git a/internal/repository/org/idp_jwt_config.go b/internal/repository/org/idp_jwt_config.go new file mode 100644 index 0000000000..74b4714d84 --- /dev/null +++ b/internal/repository/org/idp_jwt_config.go @@ -0,0 +1,87 @@ +package org + +import ( + "context" + + "github.com/caos/zitadel/internal/eventstore" + + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +const ( + IDPJWTConfigAddedEventType eventstore.EventType = "org.idp." + idpconfig.JWTConfigAddedEventType + IDPJWTConfigChangedEventType eventstore.EventType = "org.idp." + idpconfig.JWTConfigChangedEventType +) + +type IDPJWTConfigAddedEvent struct { + idpconfig.JWTConfigAddedEvent +} + +func NewIDPJWTConfigAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpConfigID, + jwtEndpoint, + issuer, + keysEndpoint, + headerName string, +) *IDPJWTConfigAddedEvent { + + return &IDPJWTConfigAddedEvent{ + JWTConfigAddedEvent: *idpconfig.NewJWTConfigAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + IDPJWTConfigAddedEventType, + ), + idpConfigID, + jwtEndpoint, + issuer, + keysEndpoint, + headerName, + ), + } +} + +func IDPJWTConfigAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := idpconfig.JWTConfigAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &IDPJWTConfigAddedEvent{JWTConfigAddedEvent: *e.(*idpconfig.JWTConfigAddedEvent)}, nil +} + +type IDPJWTConfigChangedEvent struct { + idpconfig.JWTConfigChangedEvent +} + +func NewIDPJWTConfigChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpConfigID string, + changes []idpconfig.JWTConfigChanges, +) (*IDPJWTConfigChangedEvent, error) { + changeEvent, err := idpconfig.NewJWTConfigChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + IDPJWTConfigChangedEventType), + idpConfigID, + changes, + ) + if err != nil { + return nil, err + } + return &IDPJWTConfigChangedEvent{JWTConfigChangedEvent: *changeEvent}, nil +} + +func IDPJWTConfigChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { + e, err := idpconfig.JWTConfigChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &IDPJWTConfigChangedEvent{JWTConfigChangedEvent: *e.(*idpconfig.JWTConfigChangedEvent)}, nil +} diff --git a/internal/repository/org/idp_oidc_config.go b/internal/repository/org/idp_oidc_config.go index 11bf562cfe..f5494bd078 100644 --- a/internal/repository/org/idp_oidc_config.go +++ b/internal/repository/org/idp_oidc_config.go @@ -12,7 +12,7 @@ import ( const ( IDPOIDCConfigAddedEventType eventstore.EventType = "org.idp." + idpconfig.OIDCConfigAddedEventType - IDPOIDCConfigChangedEventType eventstore.EventType = "org.idp." + idpconfig.ConfigChangedEventType + IDPOIDCConfigChangedEventType eventstore.EventType = "org.idp." + idpconfig.OIDCConfigChangedEventType ) type IDPOIDCConfigAddedEvent struct { diff --git a/internal/ui/login/handler/auth_request.go b/internal/ui/login/handler/auth_request.go index 693bd6cdbc..4eb62d3578 100644 --- a/internal/ui/login/handler/auth_request.go +++ b/internal/ui/login/handler/auth_request.go @@ -1,14 +1,16 @@ package handler import ( - "github.com/caos/zitadel/internal/domain" "net/http" + "github.com/caos/zitadel/internal/domain" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" ) const ( queryAuthRequestID = "authRequestID" + queryUserAgentID = "userAgentID" ) func (l *Login) getAuthRequest(r *http.Request) (*domain.AuthRequest, error) { diff --git a/internal/ui/login/handler/external_login_handler.go b/internal/ui/login/handler/external_login_handler.go index 2221d58a2d..dc6cbdaccf 100644 --- a/internal/ui/login/handler/external_login_handler.go +++ b/internal/ui/login/handler/external_login_handler.go @@ -1,7 +1,9 @@ package handler import ( + "encoding/base64" "net/http" + "net/url" "strings" "time" @@ -79,7 +81,7 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai return } if !idpConfig.IsOIDC { - l.renderError(w, r, authReq, caos_errors.ThrowInternal(nil, "LOGIN-Rio9s", "Errors.User.ExternalIDP.IDPTypeNotImplemented")) + l.handleJWTAuthorize(w, r, authReq, idpConfig) return } l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalLoginCallback) @@ -90,6 +92,29 @@ func (l *Login) handleOIDCAuthorize(w http.ResponseWriter, r *http.Request, auth http.Redirect(w, r, rp.AuthURL(authReq.ID, provider, rp.WithPrompt(oidc.PromptSelectAccount)), http.StatusFound) } +func (l *Login) handleJWTAuthorize(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) { + redirect, err := url.Parse(idpConfig.JWTEndpoint) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + q := redirect.Query() + q.Set(queryAuthRequestID, authReq.ID) + userAgentID, ok := http_mw.UserAgentIDFromCtx(r.Context()) + if !ok { + l.renderLogin(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "LOGIN-dsgg3", "Errors.AuthRequest.UserAgentNotFound")) + return + } + nonce, err := l.IDPConfigAesCrypto.Encrypt([]byte(userAgentID)) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) + redirect.RawQuery = q.Encode() + http.Redirect(w, r, redirect.String(), http.StatusFound) +} + func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) { data := new(externalIDPCallbackData) err := l.getParseData(r, data) @@ -108,13 +133,17 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque l.renderError(w, r, authReq, err) return } - provider := l.getRPConfig(w, r, authReq, idpConfig, EndpointExternalLoginCallback) - tokens, err := rp.CodeExchange(r.Context(), data.Code, provider) - if err != nil { - l.renderLogin(w, r, authReq, err) - return + if idpConfig.IsOIDC { + provider := l.getRPConfig(w, r, authReq, idpConfig, EndpointExternalLoginCallback) + tokens, err := rp.CodeExchange(r.Context(), data.Code, provider) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens) } - l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens) + l.renderError(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "RP-asff2", "Errors.ExternalIDP.IDPTypeNotImplemented")) + return } func (l *Login) getRPConfig(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) rp.RelyingParty { diff --git a/internal/ui/login/handler/external_register_handler.go b/internal/ui/login/handler/external_register_handler.go index bd49fc9397..578b85598e 100644 --- a/internal/ui/login/handler/external_register_handler.go +++ b/internal/ui/login/handler/external_register_handler.go @@ -10,7 +10,6 @@ import ( http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/domain" - caos_errors "github.com/caos/zitadel/internal/errors" iam_model "github.com/caos/zitadel/internal/iam/model" ) @@ -73,7 +72,7 @@ func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) { return } if !idpConfig.IsOIDC { - l.renderError(w, r, authReq, caos_errors.ThrowInternal(nil, "LOGIN-Rio9s", "Errors.User.ExternalIDP.IDPTypeNotImplemented")) + l.handleJWTAuthorize(w, r, authReq, idpConfig) return } l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalRegisterCallback) diff --git a/internal/ui/login/handler/jwt_handler.go b/internal/ui/login/handler/jwt_handler.go new file mode 100644 index 0000000000..f0db930f36 --- /dev/null +++ b/internal/ui/login/handler/jwt_handler.go @@ -0,0 +1,203 @@ +package handler + +import ( + "context" + "encoding/base64" + "net/http" + "net/url" + "strings" + "time" + + "github.com/caos/oidc/pkg/client/rp" + "github.com/caos/oidc/pkg/oidc" + http_util "github.com/caos/zitadel/internal/api/http" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + iam_model "github.com/caos/zitadel/internal/iam/model" +) + +type jwtRequest struct { + AuthRequestID string `schema:"authRequestID"` + UserAgentID string `schema:"userAgentID"` +} + +func (l *Login) handleJWTRequest(w http.ResponseWriter, r *http.Request) { + data := new(jwtRequest) + err := l.getParseData(r, data) + if err != nil { + l.renderError(w, r, nil, err) + return + } + if data.AuthRequestID == "" || data.UserAgentID == "" { + l.renderError(w, r, nil, errors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters")) + return + } + id, err := base64.RawURLEncoding.DecodeString(data.UserAgentID) + if err != nil { + l.renderError(w, r, nil, err) + return + } + userAgentID, err := l.IDPConfigAesCrypto.DecryptString(id, l.IDPConfigAesCrypto.EncryptionKeyID()) + if err != nil { + l.renderError(w, r, nil, err) + return + } + authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.AuthRequestID, userAgentID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if idpConfig.IsOIDC { + if err != nil { + l.renderError(w, r, nil, err) + return + } + } + l.handleJWTExtraction(w, r, authReq, idpConfig) +} + +func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) { + token, err := getToken(r, idpConfig.JWTHeaderName) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + tokenClaims, err := validateToken(r.Context(), token, idpConfig) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + tokens := &oidc.Tokens{IDToken: token, IDTokenClaims: tokenClaims} + externalUser := l.mapTokenToLoginUser(tokens, idpConfig) + err = l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r)) + if err != nil { + if errors.IsNotFound(err) { + err = nil + } + if !idpConfig.AutoRegister { + l.renderExternalNotFoundOption(w, r, authReq, err) + return + } + authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + resourceOwner := l.getOrgID(authReq) + orgIamPolicy, err := l.getOrgIamPolicy(r, resourceOwner) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + user, externalIDP := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig) + err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, domain.BrowserInfoFromRequest(r)) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + } + redirect, err := l.redirectToJWTCallback(authReq) + if err != nil { + l.renderError(w, r, nil, err) + return + } + http.Redirect(w, r, redirect, http.StatusFound) +} + +func (l *Login) redirectToJWTCallback(authReq *domain.AuthRequest) (string, error) { + redirect, err := url.Parse(l.baseURL + EndpointJWTCallback) + if err != nil { + return "", err + } + q := redirect.Query() + q.Set(queryAuthRequestID, authReq.ID) + nonce, err := l.IDPConfigAesCrypto.Encrypt([]byte(authReq.AgentID)) + if err != nil { + return "", err + } + q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) + redirect.RawQuery = q.Encode() + return redirect.String(), nil +} + +func (l *Login) handleJWTCallback(w http.ResponseWriter, r *http.Request) { + data := new(jwtRequest) + err := l.getParseData(r, data) + if err != nil { + l.renderError(w, r, nil, err) + return + } + id, err := base64.RawURLEncoding.DecodeString(data.UserAgentID) + if err != nil { + l.renderError(w, r, nil, err) + return + } + userAgentID, err := l.IDPConfigAesCrypto.DecryptString(id, l.IDPConfigAesCrypto.EncryptionKeyID()) + if err != nil { + l.renderError(w, r, nil, err) + return + } + authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.AuthRequestID, userAgentID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if idpConfig.IsOIDC { + l.renderLogin(w, r, authReq, err) + return + } + l.renderNextStep(w, r, authReq) +} + +func validateToken(ctx context.Context, token string, config *iam_model.IDPConfigView) (oidc.IDTokenClaims, error) { + offset := 3 * time.Second + maxAge := time.Hour + claims := oidc.EmptyIDTokenClaims() + payload, err := oidc.ParseToken(token, claims) + if err != nil { + return nil, err + } + + if err = oidc.CheckIssuer(claims, config.JWTIssuer); err != nil { + return nil, err + } + + keySet := rp.NewRemoteKeySet(http.DefaultClient, config.JWTKeysEndpoint) + if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil { + return nil, err + } + + if !claims.GetExpiration().IsZero() { + if err = oidc.CheckExpiration(claims, offset); err != nil { + return nil, err + } + } + + if !claims.GetIssuedAt().IsZero() { + if err = oidc.CheckIssuedAt(claims, maxAge, offset); err != nil { + return nil, err + } + } + return claims, nil +} + +func getToken(r *http.Request, headerName string) (string, error) { + if headerName == "" { + headerName = http_util.Authorization + } + auth := r.Header.Get(headerName) + if auth == "" { + return "", errors.ThrowInvalidArgument(nil, "LOGIN-adh42", "Errors.AuthRequest.TokenNotFound") + } + return strings.TrimPrefix(auth, oidc.PrefixBearer), nil +} diff --git a/internal/ui/login/handler/router.go b/internal/ui/login/handler/router.go index 68e408a567..ab441a759d 100644 --- a/internal/ui/login/handler/router.go +++ b/internal/ui/login/handler/router.go @@ -13,6 +13,8 @@ const ( EndpointLogin = "/login" EndpointExternalLogin = "/login/externalidp" EndpointExternalLoginCallback = "/login/externalidp/callback" + EndpointJWTAuthorize = "/login/jwt/authorize" + EndpointJWTCallback = "/login/jwt/callback" EndpointPasswordlessLogin = "/login/passwordless" EndpointPasswordlessRegistration = "/login/passwordless/init" EndpointPasswordlessPrompt = "/login/passwordless/prompt" @@ -53,6 +55,8 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost) router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) + router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet) + router.HandleFunc(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet) router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost) router.HandleFunc(EndpointPasswordlessRegistration, login.handlePasswordlessRegistration).Methods(http.MethodGet) router.HandleFunc(EndpointPasswordlessRegistration, login.handlePasswordlessRegistrationCheck).Methods(http.MethodPost) diff --git a/internal/ui/login/static/i18n/de.yaml b/internal/ui/login/static/i18n/de.yaml index c62bd91a31..e126271b51 100644 --- a/internal/ui/login/static/i18n/de.yaml +++ b/internal/ui/login/static/i18n/de.yaml @@ -298,7 +298,10 @@ Errors: AuthRequest: NotFound: AuthRequest konnte nicht gefunden werden UserAgentNotCorresponding: User Agent stimmt nicht überein + UserAgentNotFound: User Agent ID nicht gefunden + TokenNotFound: Token nicht gefunden RequestTypeNotSupported: Requesttyp wird nicht unterstürzt + MissingParameters: Benötigte Parameter fehlen User: NotFound: Benutzer konnte nicht gefunden werden Inactive: Benutzer ist inaktiv diff --git a/internal/ui/login/static/i18n/en.yaml b/internal/ui/login/static/i18n/en.yaml index 6a3b4256a7..bf6d3910fb 100644 --- a/internal/ui/login/static/i18n/en.yaml +++ b/internal/ui/login/static/i18n/en.yaml @@ -299,7 +299,10 @@ Errors: AuthRequest: NotFound: Could not find authrequest UserAgentNotCorresponding: User Agent does not correspond + UserAgentNotFound: User Agent ID not found + TokenNotFound: Token not found RequestTypeNotSupported: Request type is not supported + MissingParameters: Required parameters missing User: NotFound: User could not be found Inactive: User is inactive diff --git a/migrations/cockroach/V1.71__jwt_idp.sql b/migrations/cockroach/V1.71__jwt_idp.sql new file mode 100644 index 0000000000..fcd56c0989 --- /dev/null +++ b/migrations/cockroach/V1.71__jwt_idp.sql @@ -0,0 +1,11 @@ +ALTER TABLE auth.idp_configs ADD COLUMN jwt_endpoint TEXT; +ALTER TABLE auth.idp_configs ADD COLUMN jwt_keys_endpoint TEXT; +ALTER TABLE auth.idp_configs ADD COLUMN jwt_header_name TEXT; + +ALTER TABLE adminapi.idp_configs ADD COLUMN jwt_endpoint TEXT; +ALTER TABLE adminapi.idp_configs ADD COLUMN jwt_keys_endpoint TEXT; +ALTER TABLE adminapi.idp_configs ADD COLUMN jwt_header_name TEXT; + +ALTER TABLE management.idp_configs ADD COLUMN jwt_endpoint TEXT; +ALTER TABLE management.idp_configs ADD COLUMN jwt_keys_endpoint TEXT; +ALTER TABLE management.idp_configs ADD COLUMN jwt_header_name TEXT; diff --git a/pkg/grpc/idp/idp.go b/pkg/grpc/idp/idp.go new file mode 100644 index 0000000000..ac3ab5e769 --- /dev/null +++ b/pkg/grpc/idp/idp.go @@ -0,0 +1,3 @@ +package idp + +type IDPConfig = isIDP_Config diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index c1b1058656..6dada623c4 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -401,6 +401,41 @@ service AdminService { }; } + // Adds a new jwt identity provider configuration the IAM + rpc AddJWTIDP(AddJWTIDPRequest) returns (AddJWTIDPResponse) { + option (google.api.http) = { + post: "/idps/jwt"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "identity provider"; + tags: "jwt"; + + responses: { + key: "200"; + value: { + description: "idp created"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid argument"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + //Updates the specified idp // all fields are updated. If no value is provided the field will be empty afterwards. rpc UpdateIDP(UpdateIDPRequest) returns (UpdateIDPResponse) { @@ -598,7 +633,53 @@ service AdminService { }; }; } - + + //Updates the jwt configuration of the specified idp + // all fields are updated. If no value is provided the field will be empty afterwards. + rpc UpdateIDPJWTConfig(UpdateIDPJWTConfigRequest) returns (UpdateIDPJWTConfigResponse) { + option (google.api.http) = { + put: "/idps/{idp_id}/jwt_config"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "identity provider"; + tags: "jwt"; + responses: { + key: "200"; + value: { + description: "jwt config updated"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid argument"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + responses: { + key: "409"; + value: { + description: "precondition failed"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + rpc GetDefaultFeatures(GetDefaultFeaturesRequest) returns (GetDefaultFeaturesResponse) { option(google.api.http) = { get: "/features" @@ -2436,6 +2517,64 @@ message AddOIDCIDPResponse { string idp_id = 2; } +message AddJWTIDPRequest { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + required: ["name", "issuer", "keys_endpoint"] + }; + }; + + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"google\""; + min_length: 1; + max_length: 200; + } + ]; + zitadel.idp.v1.IDPStylingType styling_type = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "some identity providers specify the styling of the button to their login"; + } + ]; + string jwt_endpoint = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://custom.com/auth/jwt\""; + description: "the endpoint where the jwt can be extracted"; + } + ]; + string issuer = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.custom.com\""; + description: "the issuer of the jwt (for validation)"; + } + ]; + string keys_endpoint = 5 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.custom.com/keys\""; + description: "the endpoint to the key (JWK) which are used to sign the JWT with"; + } + ]; + string header_name = 6 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"x-auth-token\""; + description: "the name of the header where the JWT is sent in, default is authorization"; + max_length: 200; + } + ]; + bool auto_register = 7; +} + +message AddJWTIDPResponse { + zitadel.v1.ObjectDetails details = 1; + string idp_id = 2; +} + message UpdateIDPRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { @@ -2590,6 +2729,62 @@ message UpdateIDPOIDCConfigResponse { zitadel.v1.ObjectDetails details = 1; } +message UpdateIDPJWTConfigRequest { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + required: ["idp_id", "issuer", "keys_endpoint"] + }; + }; + + string idp_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; + string jwt_endpoint = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://custom.com/auth/jwt\""; + description: "the endpoint where the jwt can be extracted"; + min_length: 1; + max_length: 200; + } + ]; + string issuer = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.custom.com\""; + description: "the issuer of the jwt (for validation)"; + min_length: 1; + max_length: 200; + } + ]; + string keys_endpoint = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.custom.com/keys\""; + description: "the endpoint to the key (JWK) which are used to sign the JWT with"; + min_length: 1; + max_length: 200; + } + ]; + string header_name = 5 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"x-auth-token\""; + description: "the name of the header where the JWT is sent in, default is authorization"; + max_length: 200; + } + ]; +} + +message UpdateIDPJWTConfigResponse { + zitadel.v1.ObjectDetails details = 1; +} + message GetDefaultFeaturesRequest {} message GetDefaultFeaturesResponse { @@ -3637,4 +3832,4 @@ message FailedEvent { example: "\"ID=EXAMP-ID3ER Message=Example message\""; } ]; -} \ No newline at end of file +} diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 2d7b28c4d0..76f119c181 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -37,6 +37,7 @@ message IDP { ]; oneof config { OIDCConfig oidc_config = 7; + JWTConfig jwt_config = 9; } bool auto_register = 8; } @@ -115,6 +116,7 @@ enum IDPType { IDP_TYPE_UNSPECIFIED = 0; IDP_TYPE_OIDC = 1; //PLANNED: IDP_TYPE_SAML + IDP_TYPE_JWT = 3; } // the owner of the identity provider. @@ -162,6 +164,38 @@ enum OIDCMappingField { OIDC_MAPPING_FIELD_EMAIL = 2; } + +message JWTConfig { + string jwt_endpoint = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + description: "the endpoint where the jwt can be extracted"; + } + ]; + string issuer = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + description: "the issuer of the jwt (for validation)"; + } + ]; + string keys_endpoint = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/keys\""; + description: "the endpoint to the key (JWK) which are used to sign the JWT with"; + } + ]; + string header_name = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"x-auth-token\""; + description: "the name of the header where the JWT is sent in, default is authorization"; + } + ]; +} + message IDPIDQuery { string id = 1 [ (validate.rules).string = {max_len: 200}, diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index c9a90e01d1..10ae0ad9eb 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2617,6 +2617,19 @@ service ManagementService { }; } + // Add a new jwt identity provider configuration in the organisation + rpc AddOrgJWTIDP(AddOrgJWTIDPRequest) returns (AddOrgJWTIDPResponse) { + option (google.api.http) = { + post: "/idps/jwt" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + feature: "login_policy.idp" + }; + } + // Deactivate identity provider configuration // Users will not be able to use this provider for login (e.g Google, Microsoft, AD, etc) // Returns error if already deactivated @@ -2684,6 +2697,19 @@ service ManagementService { feature: "login_policy.idp" }; } + + // Change JWT identity provider configuration of the organisation + rpc UpdateOrgIDPJWTConfig(UpdateOrgIDPJWTConfigRequest) returns (UpdateOrgIDPJWTConfigResponse) { + option (google.api.http) = { + put: "/idps/{idp_id}/jwt_config" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + feature: "login_policy.idp" + }; + } } //This is an empty request @@ -4892,6 +4918,62 @@ message AddOrgOIDCIDPResponse { string idp_id = 2; } +message AddOrgJWTIDPRequest { + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"google\""; + } + ]; + zitadel.idp.v1.IDPStylingType styling_type = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "some identity providers specify the styling of the button to their login"; + } + ]; + string jwt_endpoint = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + description: "the endpoint where the jwt can be extracted"; + min_length: 1; + max_length: 200; + } + ]; + string issuer = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + description: "the issuer of the jwt (for validation)"; + min_length: 1; + max_length: 200; + } + ]; + string keys_endpoint = 5 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/keys\""; + description: "the endpoint to the key (JWK) which are used to sign the JWT with"; + min_length: 1; + max_length: 200; + } + ]; + string header_name = 6 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"x-auth-token\""; + description: "the name of the header where the JWT is sent in, default is authorization"; + max_length: 200; + } + ]; + bool auto_register = 7; +} + +message AddOrgJWTIDPResponse { + zitadel.v1.ObjectDetails details = 1; + string idp_id = 2; +} + message DeactivateOrgIDPRequest { string idp_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -4986,3 +5068,52 @@ message UpdateOrgIDPOIDCConfigRequest { message UpdateOrgIDPOIDCConfigResponse { zitadel.v1.ObjectDetails details = 1; } + + +message UpdateOrgIDPJWTConfigRequest { + string idp_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + string jwt_endpoint = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + description: "the endpoint where the jwt can be extracted"; + min_length: 1; + max_length: 200; + } + ]; + string issuer = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + description: "the issuer of the jwt (for validation)"; + min_length: 1; + max_length: 200; + } + ]; + string keys_endpoint = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/keys\""; + description: "the endpoint to the key (JWK) which are used to sign the JWT with"; + min_length: 1; + max_length: 200; + } + ]; + string header_name = 5 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"x-auth-token\""; + description: "the name of the header where the JWT is sent in, default is authorization"; + max_length: 200; + } + ]; +} + +message UpdateOrgIDPJWTConfigResponse { + zitadel.v1.ObjectDetails details = 1; +}