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;
+}