feat: jwt as idp (#2363)

* feat: jwt idp

* feat: command side

* feat: add tests

* fill idp views with jwt idps and return apis

* add jwtEndpoint to jwt idp

* begin jwt request handling

* merge

* handle jwt idp

* cleanup

* fixes

* autoregister

* get token from specific header name

* error handling

* fix texts

* handle renderExternalNotFoundOption

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
This commit is contained in:
Livio Amstutz 2021-09-14 15:15:01 +02:00 committed by GitHub
parent 4e1d42259c
commit b6b5b1b782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2575 additions and 71 deletions

View File

@ -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<br /> string.max_len: 200<br /> |
| styling_type | zitadel.idp.v1.IDPStylingType | - | enum.defined_only: true<br /> |
| jwt_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| issuer | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| keys_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| header_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| 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<br /> string.max_len: 200<br /> |
| jwt_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| issuer | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| keys_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| header_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### UpdateIDPJWTConfigResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### UpdateIDPOIDCConfigRequest

View File

@ -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<br /> string.max_len: 200<br /> |
| issuer | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| keys_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| header_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### 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 |

View File

@ -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<br /> string.max_len: 200<br /> |
| styling_type | zitadel.idp.v1.IDPStylingType | - | enum.defined_only: true<br /> |
| jwt_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| issuer | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| keys_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| header_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| 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<br /> string.max_len: 200<br /> |
| jwt_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| issuer | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| keys_endpoint | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| header_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### UpdateOrgIDPJWTConfigResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### UpdateOrgIDPOIDCConfigRequest

View File

@ -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

View File

@ -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
}

View File

@ -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{

View File

@ -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
)

View File

@ -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,7 +145,8 @@ func IDPStylingTypeToPb(stylingType domain.IDPConfigStylingType) idp_pb.IDPStyli
}
}
func ModelIDPViewToConfigPb(config *iam_model.IDPConfigView) *idp_pb.IDP_OidcConfig {
func ModelIDPViewToConfigPb(config *iam_model.IDPConfigView) idp_pb.IDPConfig {
if config.IsOIDC {
return &idp_pb.IDP_OidcConfig{
OidcConfig: &idp_pb.OIDCConfig{
ClientId: config.OIDCClientID,
@ -143,8 +157,18 @@ func ModelIDPViewToConfigPb(config *iam_model.IDPConfigView) *idp_pb.IDP_OidcCon
},
}
}
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 {
func IDPViewToConfigPb(config *domain.IDPConfigView) idp_pb.IDPConfig {
if config.IsOIDC {
return &idp_pb.IDP_OidcConfig{
OidcConfig: &idp_pb.OIDCConfig{
ClientId: config.OIDCClientID,
@ -155,6 +179,14 @@ func IDPViewToConfigPb(config *domain.IDPConfigView) *idp_pb.IDP_OidcConfig {
},
}
}
return &idp_pb.IDP_JwtConfig{
JwtConfig: &idp_pb.JWTConfig{
JwtEndpoint: config.JWTEndpoint,
Issuer: config.JWTIssuer,
KeysEndpoint: config.JWTKeysEndpoint,
},
}
}
func FieldNameToModel(fieldName idp_pb.IDPFieldName) iam_model.IDPConfigSearchKey {
switch fieldName {

View File

@ -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
}

View File

@ -16,6 +16,7 @@ func addOIDCIDPRequestToDomain(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.IDPCon
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{

View File

@ -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
)

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -13,6 +13,7 @@ type IDPConfig struct {
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

View File

@ -29,6 +29,10 @@ type IDPConfigView struct {
OIDCUsernameMapping OIDCMappingField
OAuthAuthorizationEndpoint string
OAuthTokenEndpoint string
JWTEndpoint string
JWTIssuer string
JWTKeysEndpoint string
JWTHeaderName string
}
type IDPConfigSearchRequest struct {

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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).

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -12,7 +12,7 @@ import (
const (
OIDCConfigAddedEventType eventstore.EventType = "oidc.config.added"
ConfigChangedEventType eventstore.EventType = "oidc.config.changed"
OIDCConfigChangedEventType eventstore.EventType = "oidc.config.changed"
)
type OIDCConfigAddedEvent struct {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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,6 +133,7 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
l.renderError(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 {
@ -116,6 +142,9 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
}
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 {
oidcClientSecret, err := crypto.DecryptString(idpConfig.OIDCClientSecret, l.IDPConfigAesCrypto)

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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;

3
pkg/grpc/idp/idp.go Normal file
View File

@ -0,0 +1,3 @@
package idp
type IDPConfig = isIDP_Config

View File

@ -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) {
@ -599,6 +634,52 @@ 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 {

View File

@ -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},

View File

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