diff --git a/cmd/start/start.go b/cmd/start/start.go index 2ee0725b6d..4c6a92c4e8 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -41,7 +41,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/session/v2" "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" "github.com/zitadel/zitadel/internal/api/grpc/system" - "github.com/zitadel/zitadel/internal/api/grpc/user/v2" + user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/idp" @@ -357,7 +357,7 @@ func startAPIs( if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil { return err } - if err := apis.RegisterService(ctx, user.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure))); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure))); err != nil { return err } if err := apis.RegisterService(ctx, session.CreateServer(commands, queries, permissionCheck)); err != nil { @@ -376,7 +376,7 @@ func startAPIs( apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler)) - userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost) + userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS) if err != nil { return err } diff --git a/go.mod b/go.mod index a50b0341c7..f4e513fbb1 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/boombuler/barcode v1.0.1 github.com/cockroachdb/cockroach-go/v2 v2.3.5 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be + github.com/crewjam/saml v0.4.13 github.com/descope/virtualwebauthn v1.0.2 github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 @@ -87,11 +88,13 @@ require ( require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect + github.com/crewjam/httperr v0.2.0 // indirect github.com/dmarkham/enumer v1.5.8 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-webauthn/x v0.1.4 // indirect + github.com/golang-jwt/jwt/v4 v4.4.3 // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/glog v1.1.1 // indirect github.com/google/go-tpm v0.9.0 // indirect @@ -99,12 +102,14 @@ require ( github.com/google/s2a-go v0.1.5 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect github.com/pascaldekloe/name v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/smartystreets/assertions v1.0.0 // indirect + github.com/zenazn/goji v1.0.1 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect @@ -120,7 +125,7 @@ require ( cloud.google.com/go/trace v1.10.1 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/amdonov/xmlsig v0.1.0 // indirect - github.com/beevik/etree v1.2.0 // indirect + github.com/beevik/etree v1.2.0 github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/go.sum b/go.sum index afff809df8..9e7ae585c7 100644 --- a/go.sum +++ b/go.sum @@ -165,9 +165,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= +github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc= +github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/descope/virtualwebauthn v1.0.2 h1:cAvfS9wHh6On9HAE4Gjn3fJkf8MPQW2LzN8BPKEPs0M= @@ -324,6 +329,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= @@ -625,6 +632,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -769,6 +778,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= @@ -779,6 +789,7 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -868,6 +879,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA= github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc= github.com/zitadel/oidc/v2 v2.11.0 h1:Am4/yQr4iiM5bznRgF3FOp+wLdKx2gzSU73uyI9vvBE= @@ -947,6 +960,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= @@ -1389,6 +1403,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index d5bc2cef98..f382946ed7 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -426,6 +426,37 @@ func (s *Server) UpdateAppleProvider(ctx context.Context, req *admin_pb.UpdateAp }, nil } +func (s *Server) AddSAMLProvider(ctx context.Context, req *admin_pb.AddSAMLProviderRequest) (*admin_pb.AddSAMLProviderResponse, error) { + id, details, err := s.command.AddInstanceSAMLProvider(ctx, addSAMLProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.AddSAMLProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) UpdateSAMLProvider(ctx context.Context, req *admin_pb.UpdateSAMLProviderRequest) (*admin_pb.UpdateSAMLProviderResponse, error) { + details, err := s.command.UpdateInstanceSAMLProvider(ctx, req.Id, updateSAMLProviderToCommand(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateSAMLProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + +func (s *Server) RegenerateSAMLProviderCertificate(ctx context.Context, req *admin_pb.RegenerateSAMLProviderCertificateRequest) (*admin_pb.RegenerateSAMLProviderCertificateResponse, error) { + details, err := s.command.RegenerateInstanceSAMLProviderCertificate(ctx, req.Id) + if err != nil { + return nil, err + } + return &admin_pb.RegenerateSAMLProviderCertificateResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) { details, err := s.command.DeleteInstanceProvider(ctx, req.Id) if err != nil { diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 3ab176276c..c5b6ede88a 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -1,6 +1,8 @@ package admin import ( + "github.com/crewjam/saml" + idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp" "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/command" @@ -9,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" ) func addOIDCIDPRequestToDomain(req *admin_pb.AddOIDCIDPRequest) *domain.IDPConfig { @@ -464,3 +467,40 @@ func updateAppleProviderToCommand(req *admin_pb.UpdateAppleProviderRequest) comm IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } + +func addSAMLProviderToCommand(req *admin_pb.AddSAMLProviderRequest) command.SAMLProvider { + return command.SAMLProvider{ + Name: req.Name, + Metadata: req.GetMetadataXml(), + MetadataURL: req.GetMetadataUrl(), + Binding: bindingToCommand(req.Binding), + WithSignedRequest: req.WithSignedRequest, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateSAMLProviderToCommand(req *admin_pb.UpdateSAMLProviderRequest) command.SAMLProvider { + return command.SAMLProvider{ + Name: req.Name, + Metadata: req.GetMetadataXml(), + MetadataURL: req.GetMetadataUrl(), + Binding: bindingToCommand(req.Binding), + WithSignedRequest: req.WithSignedRequest, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func bindingToCommand(binding idp_pb.SAMLBinding) string { + switch binding { + case idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED: + return "" + case idp_pb.SAMLBinding_SAML_BINDING_POST: + return saml.HTTPPostBinding + case idp_pb.SAMLBinding_SAML_BINDING_REDIRECT: + return saml.HTTPRedirectBinding + case idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT: + return saml.HTTPArtifactBinding + default: + return "" + } +} diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 7ed3c4551e..11873d8480 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -418,6 +418,37 @@ func (s *Server) UpdateAppleProvider(ctx context.Context, req *mgmt_pb.UpdateApp }, nil } +func (s *Server) AddSAMLProvider(ctx context.Context, req *mgmt_pb.AddSAMLProviderRequest) (*mgmt_pb.AddSAMLProviderResponse, error) { + id, details, err := s.command.AddOrgSAMLProvider(ctx, authz.GetCtxData(ctx).OrgID, addSAMLProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.AddSAMLProviderResponse{ + Id: id, + Details: object_pb.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) UpdateSAMLProvider(ctx context.Context, req *mgmt_pb.UpdateSAMLProviderRequest) (*mgmt_pb.UpdateSAMLProviderResponse, error) { + details, err := s.command.UpdateOrgSAMLProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateSAMLProviderToCommand(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateSAMLProviderResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + +func (s *Server) RegenerateSAMLProviderCertificate(ctx context.Context, req *mgmt_pb.RegenerateSAMLProviderCertificateRequest) (*mgmt_pb.RegenerateSAMLProviderCertificateResponse, error) { + details, err := s.command.RegenerateOrgSAMLProviderCertificate(ctx, authz.GetCtxData(ctx).OrgID, req.Id) + if err != nil { + return nil, err + } + return &mgmt_pb.RegenerateSAMLProviderCertificateResponse{ + Details: object_pb.DomainToChangeDetailsPb(details), + }, nil +} + func (s *Server) DeleteProvider(ctx context.Context, req *mgmt_pb.DeleteProviderRequest) (*mgmt_pb.DeleteProviderResponse, error) { details, err := s.command.DeleteOrgProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id) if err != nil { diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index 7630be7ce8..e6df255f33 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -3,6 +3,8 @@ package management import ( "context" + "github.com/crewjam/saml" + "github.com/zitadel/zitadel/internal/api/authz" idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp" "github.com/zitadel/zitadel/internal/api/grpc/object" @@ -12,6 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/v1/models" iam_model "github.com/zitadel/zitadel/internal/iam/model" "github.com/zitadel/zitadel/internal/query" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) @@ -481,3 +484,40 @@ func updateAppleProviderToCommand(req *mgmt_pb.UpdateAppleProviderRequest) comma IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } + +func addSAMLProviderToCommand(req *mgmt_pb.AddSAMLProviderRequest) command.SAMLProvider { + return command.SAMLProvider{ + Name: req.Name, + Metadata: req.GetMetadataXml(), + MetadataURL: req.GetMetadataUrl(), + Binding: bindingToCommand(req.Binding), + WithSignedRequest: req.WithSignedRequest, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func updateSAMLProviderToCommand(req *mgmt_pb.UpdateSAMLProviderRequest) command.SAMLProvider { + return command.SAMLProvider{ + Name: req.Name, + Metadata: req.GetMetadataXml(), + MetadataURL: req.GetMetadataUrl(), + Binding: bindingToCommand(req.Binding), + WithSignedRequest: req.WithSignedRequest, + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + } +} + +func bindingToCommand(binding idp_pb.SAMLBinding) string { + switch binding { + case idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED: + return "" + case idp_pb.SAMLBinding_SAML_BINDING_POST: + return saml.HTTPPostBinding + case idp_pb.SAMLBinding_SAML_BINDING_REDIRECT: + return saml.HTTPRedirectBinding + case idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT: + return saml.HTTPArtifactBinding + default: + return "" + } +} diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 83cf58c5df..8e0f24d5dc 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -22,6 +22,7 @@ type Server struct { userCodeAlg crypto.EncryptionAlgorithm idpAlg crypto.EncryptionAlgorithm idpCallback func(ctx context.Context) string + samlRootURL func(ctx context.Context, idpID string) string } type Config struct{} @@ -32,6 +33,7 @@ func CreateServer( userCodeAlg crypto.EncryptionAlgorithm, idpAlg crypto.EncryptionAlgorithm, idpCallback func(ctx context.Context) string, + samlRootURL func(ctx context.Context, idpID string) string, ) *Server { return &Server{ command: command, @@ -39,6 +41,7 @@ func CreateServer( userCodeAlg: userCodeAlg, idpAlg: idpAlg, idpCallback: idpCallback, + samlRootURL: samlRootURL, } } diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 849a456312..5ee56f2b68 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -145,14 +145,23 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re if err != nil { return nil, err } - authURL, err := s.command.AuthURLFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx)) + content, redirect, err := s.command.AuthFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) if err != nil { return nil, err } - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: authURL}, - }, nil + if redirect { + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, + }, nil + } else { + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ + PostForm: []byte(content), + }, + }, nil + } } func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { @@ -206,7 +215,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse } func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { - provider, err := s.command.GetProvider(ctx, idpID, "") + provider, err := s.command.GetProvider(ctx, idpID, "", "") if err != nil { return nil, "", nil, err } @@ -279,6 +288,14 @@ func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.Encr information.IdpInformation.Access = access } + if intent.Assertion != nil { + assertion, err := crypto.Decrypt(intent.Assertion, alg) + if err != nil { + return nil, err + } + information.IdpInformation.Access = IDPSAMLResponseToPb(assertion) + } + return information, nil } @@ -330,6 +347,14 @@ func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInfor }, nil } +func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml { + return &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: assertion, + }, + } +} + func (s *Server) checkIntentToken(token string, intentID string) error { return crypto.CheckToken(s.idpAlg, token, intentID) } diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go index f037e50803..4bb1fe8710 100644 --- a/internal/api/grpc/user/v2/user_integration_test.go +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -5,8 +5,8 @@ package user_test import ( "context" "fmt" + "net/url" "os" - "strings" "testing" "time" @@ -624,14 +624,24 @@ func TestServer_AddIDPLink(t *testing.T) { func TestServer_StartIdentityProviderIntent(t *testing.T) { idpID := Tester.AddGenericOAuthProvider(t) + samlIdpID := Tester.AddSAMLProvider(t) + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) + samlPostIdpID := Tester.AddSAMLPostProvider(t) type args struct { ctx context.Context req *user.StartIdentityProviderIntentRequest } + type want struct { + details *object.Details + url string + parametersExisting []string + parametersEqual map[string]string + postForm bool + } tests := []struct { name string args args - want *user.StartIdentityProviderIntentResponse + want want wantErr bool }{ { @@ -642,11 +652,10 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { IdpId: idpID, }, }, - want: nil, wantErr: true, }, { - name: "next step auth url", + name: "next step oauth auth url", args: args{ CTX, &user.StartIdentityProviderIntentRequest{ @@ -659,14 +668,91 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { }, }, }, - want: &user.StartIdentityProviderIntentResponse{ - Details: &object.Details{ + want: want{ + details: &object.Details{ ChangeDate: timestamppb.Now(), ResourceOwner: Tester.Organisation.ID, }, - NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{ - AuthUrl: "https://example.com/oauth/v2/authorize?client_id=clientID&prompt=select_account&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fidps%2Fcallback&response_type=code&scope=openid+profile+email&state=", + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://localhost:8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step saml default", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: samlIdpID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + url: "http://localhost:8000/sso", + parametersExisting: []string{"RelayState", "SAMLRequest"}, + }, + wantErr: false, + }, + { + name: "next step saml auth url", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: samlRedirectIdpID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + url: "http://localhost:8000/sso", + parametersExisting: []string{"RelayState", "SAMLRequest"}, + }, + wantErr: false, + }, + { + name: "next step saml form", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: samlPostIdpID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + postForm: true, }, wantErr: false, }, @@ -680,12 +766,25 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { require.NoError(t, err) } - if nextStep := tt.want.GetNextStep(); nextStep != nil { - if !strings.HasPrefix(got.GetAuthUrl(), tt.want.GetAuthUrl()) { - assert.Failf(t, "auth url does not match", "expected: %s, but got: %s", tt.want.GetAuthUrl(), got.GetAuthUrl()) + if tt.want.url != "" { + authUrl, err := url.Parse(got.GetAuthUrl()) + assert.NoError(t, err) + + assert.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting)) + + for _, existing := range tt.want.parametersExisting { + assert.True(t, authUrl.Query().Has(existing)) + } + for key, equal := range tt.want.parametersEqual { + assert.Equal(t, equal, authUrl.Query().Get(key)) } } - integration.AssertDetails(t, tt.want, got) + if tt.want.postForm { + assert.NotEmpty(t, got.GetPostForm()) + } + integration.AssertDetails(t, &user.StartIdentityProviderIntentResponse{ + Details: tt.want.details, + }, got) }) } } @@ -697,6 +796,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { successfulWithUserID, WithUsertoken, WithUserchangeDate, WithUsersequence := Tester.CreateSuccessfulOAuthIntent(t, idpID, "user", "id") ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Tester.CreateSuccessfulLDAPIntent(t, idpID, "", "id") ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Tester.CreateSuccessfulLDAPIntent(t, idpID, "user", "id") + samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Tester.CreateSuccessfulSAMLIntent(t, idpID, "", "id") type args struct { ctx context.Context req *user.RetrieveIdentityProviderIntentRequest @@ -895,6 +995,44 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful saml intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulID, + IdpIntentToken: samlToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlChangeDate), + ResourceOwner: Tester.Organisation.ID, + Sequence: samlSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(""), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index 486d472389..f1fa32fc57 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -1,18 +1,23 @@ package idp import ( + "bytes" "context" + "encoding/xml" "errors" + "fmt" + "io" "net/http" + "github.com/crewjam/saml" "github.com/gorilla/mux" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" z_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/form" "github.com/zitadel/zitadel/internal/idp" @@ -25,19 +30,26 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + saml2 "github.com/zitadel/zitadel/internal/idp/providers/saml" "github.com/zitadel/zitadel/internal/query" ) const ( - HandlerPrefix = "/idps" - callbackPath = "/callback" - ldapCallbackPath = callbackPath + "/ldap" + HandlerPrefix = "/idps" + + idpPrefix = "/{" + varIDPID + ":[0-9]+}" + + callbackPath = "/callback" + metadataPath = idpPrefix + "/saml/metadata" + acsPath = idpPrefix + "/saml/acs" + certificatePath = idpPrefix + "/saml/certificate" paramIntentID = "id" paramToken = "token" paramUserID = "user" paramError = "error" paramErrorDescription = "error_description" + varIDPID = "idpid" ) type Handler struct { @@ -46,6 +58,8 @@ type Handler struct { parser *form.Parser encryptionAlgorithm crypto.EncryptionAlgorithm callbackURL func(ctx context.Context) string + samlRootURL func(ctx context.Context, idpID string) string + loginSAMLRootURL func(ctx context.Context) string } type externalIDPCallbackData struct { @@ -58,6 +72,12 @@ type externalIDPCallbackData struct { User string `schema:"user"` } +type externalSAMLIDPCallbackData struct { + IDPID string + Response string + RelayState string +} + // CallbackURL generates the instance specific URL to the IDP callback handler func CallbackURL(externalSecure bool) func(ctx context.Context) string { return func(ctx context.Context) string { @@ -65,6 +85,18 @@ func CallbackURL(externalSecure bool) func(ctx context.Context) string { } } +func SAMLRootURL(externalSecure bool) func(ctx context.Context, idpID string) string { + return func(ctx context.Context, idpID string) string { + return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + "/" + idpID + "/" + } +} + +func LoginSAMLRootURL(externalSecure bool) func(ctx context.Context) string { + return func(ctx context.Context) string { + return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + login.HandlerPrefix + login.EndpointSAMLACS + } +} + func NewHandler( commands *command.Commands, queries *query.Queries, @@ -78,14 +110,166 @@ func NewHandler( parser: form.NewParser(), encryptionAlgorithm: encryptionAlgorithm, callbackURL: CallbackURL(externalSecure), + samlRootURL: SAMLRootURL(externalSecure), + loginSAMLRootURL: LoginSAMLRootURL(externalSecure), } router := mux.NewRouter() router.Use(instanceInterceptor) router.HandleFunc(callbackPath, h.handleCallback) + router.HandleFunc(metadataPath, h.handleMetadata) + router.HandleFunc(certificatePath, h.handleCertificate) + router.HandleFunc(acsPath, h.handleACS) return router } +func parseSAMLRequest(r *http.Request) *externalSAMLIDPCallbackData { + vars := mux.Vars(r) + return &externalSAMLIDPCallbackData{ + IDPID: vars[varIDPID], + Response: r.FormValue("SAMLResponse"), + RelayState: r.FormValue("RelayState"), + } +} + +func (h *Handler) getProvider(ctx context.Context, idpID string) (idp.Provider, error) { + return h.commands.GetProvider(ctx, idpID, h.callbackURL(ctx), h.samlRootURL(ctx, idpID)) +} + +func (h *Handler) handleCertificate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + data := parseSAMLRequest(r) + + provider, err := h.getProvider(ctx, data.IDPID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + samlProvider, ok := provider.(*saml2.Provider) + if !ok { + http.Error(w, z_errs.ThrowInvalidArgument(nil, "SAML-lrud8s9coi", "Errors.Intent.IDPInvalid").Error(), http.StatusBadRequest) + return + } + + certPem := new(bytes.Buffer) + if _, err := certPem.Write(samlProvider.Certificate); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Disposition", "attachment; filename=idp.crt") + w.Header().Set("Content-Type", r.Header.Get("Content-Type")) + _, err = io.Copy(w, certPem) + if err != nil { + http.Error(w, fmt.Errorf("failed to response with certificate: %w", err).Error(), http.StatusInternalServerError) + return + } +} + +func (h *Handler) handleMetadata(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + data := parseSAMLRequest(r) + + provider, err := h.getProvider(ctx, data.IDPID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + samlProvider, ok := provider.(*saml2.Provider) + if !ok { + http.Error(w, z_errs.ThrowInvalidArgument(nil, "SAML-lrud8s9coi", "Errors.Intent.IDPInvalid").Error(), http.StatusBadRequest) + return + } + + sp, err := samlProvider.GetSP() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + metadata := sp.ServiceProvider.Metadata() + + for i, spDesc := range metadata.SPSSODescriptors { + spDesc.AssertionConsumerServices = append( + spDesc.AssertionConsumerServices, + saml.IndexedEndpoint{ + Binding: saml.HTTPPostBinding, + Location: h.loginSAMLRootURL(ctx), + Index: len(spDesc.AssertionConsumerServices) + 1, + }, saml.IndexedEndpoint{ + Binding: saml.HTTPArtifactBinding, + Location: h.loginSAMLRootURL(ctx), + Index: len(spDesc.AssertionConsumerServices) + 2, + }, + ) + metadata.SPSSODescriptors[i] = spDesc + } + + buf, _ := xml.MarshalIndent(metadata, "", " ") + w.Header().Set("Content-Type", "application/samlmetadata+xml") + _, err = w.Write(buf) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } +} + +func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + data := parseSAMLRequest(r) + + provider, err := h.getProvider(ctx, data.IDPID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + samlProvider, ok := provider.(*saml2.Provider) + if !ok { + err := z_errs.ThrowInvalidArgument(nil, "SAML-ui9wyux0hp", "Errors.Intent.IDPInvalid") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + sp, err := samlProvider.GetSP() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + intent, err := h.commands.GetActiveIntent(ctx, data.RelayState) + if err != nil { + if z_errs.IsNotFound(err) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + redirectToFailureURLErr(w, r, intent, err) + return + } + + session := saml2.Session{ + ServiceProvider: sp, + RequestID: intent.RequestID, + Request: r, + } + + idpUser, err := session.FetchUser(r.Context()) + if err != nil { + cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + + userID, err := h.checkExternalUser(ctx, intent.IDPID, idpUser.GetID()) + logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists") + + token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session.Assertion) + if err != nil { + redirectToFailureURLErr(w, r, intent, z_errs.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed")) + return + } + redirectToSuccessURL(w, r, intent, token, userID) +} + func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { ctx := r.Context() data, err := h.parseCallbackRequest(r) @@ -111,7 +295,7 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { return } - provider, err := h.commands.GetProvider(ctx, intent.IDPID, h.callbackURL(ctx)) + provider, err := h.getProvider(ctx, intent.IDPID) if err != nil { cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error()) logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") @@ -119,7 +303,7 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { return } - idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code, data.User) + idpUser, idpSession, err := h.fetchIDPUserFromCode(ctx, provider, data.Code, data.User) if err != nil { cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error()) logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") @@ -170,23 +354,6 @@ func (h *Handler) parseCallbackRequest(r *http.Request) (*externalIDPCallbackDat return data, nil } -func (h *Handler) getActiveIntent(w http.ResponseWriter, r *http.Request, state string) *command.IDPIntentWriteModel { - intent, err := h.commands.GetIntentWriteModel(r.Context(), state, "") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return nil - } - if intent.State == domain.IDPIntentStateUnspecified { - http.Error(w, reason("IDP-Hk38e", "Errors.Intent.NotStarted"), http.StatusBadRequest) - return nil - } - if intent.State != domain.IDPIntentStateStarted { - redirectToFailureURL(w, r, intent, "IDP-Sfrgs", "Errors.Intent.NotStarted") - return nil - } - return intent -} - func redirectToSuccessURL(w http.ResponseWriter, r *http.Request, intent *command.IDPIntentWriteModel, token, userID string) { queries := intent.SuccessURL.Query() queries.Set(paramIntentID, intent.AggregateID) @@ -218,7 +385,7 @@ func redirectToFailureURL(w http.ResponseWriter, r *http.Request, i *command.IDP http.Redirect(w, r, i.FailureURL.String(), http.StatusFound) } -func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provider, code string, appleUser string) (user idp.User, idpTokens idp.Session, err error) { +func (h *Handler) fetchIDPUserFromCode(ctx context.Context, identityProvider idp.Provider, code string, appleUser string) (user idp.User, idpTokens idp.Session, err error) { var session idp.Session switch provider := identityProvider.(type) { case *oauth.Provider: @@ -235,7 +402,7 @@ func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provide session = &openid.Session{Provider: provider.Provider, Code: code} case *apple.Provider: session = &apple.Session{Session: &openid.Session{Provider: provider.Provider, Code: code}, UserFormValue: appleUser} - case *jwt.Provider, *ldap.Provider: + case *jwt.Provider, *ldap.Provider, *saml2.Provider: return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented") default: return nil, nil, z_errs.ThrowUnimplemented(nil, "IDP-SSDg", "Errors.ExternalIDP.IDPTypeNotImplemented") diff --git a/internal/api/idp/idp_integration_test.go b/internal/api/idp/idp_integration_test.go new file mode 100644 index 0000000000..790f1926ae --- /dev/null +++ b/internal/api/idp/idp_integration_test.go @@ -0,0 +1,488 @@ +//go:build integration + +package idp_test + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "encoding/xml" + "io" + "net/http" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/beevik/etree" + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlidp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + saml_xml "github.com/zitadel/saml/pkg/provider/xml" + "golang.org/x/crypto/bcrypt" + + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/integration" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +var ( + CTX context.Context + ErrCTX context.Context + Tester *integration.Tester + Client user.UserServiceClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, errCtx, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + CTX, ErrCTX = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx + Client = Tester.Client.UserV2 + return m.Run() + }()) +} + +func TestServer_SAMLCertificate(t *testing.T) { + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) + oauthIdpID := Tester.AddGenericOAuthProvider(t) + + type args struct { + ctx context.Context + idpID string + } + tests := []struct { + name string + args args + want int + }{ + { + name: "saml certificate, invalid idp", + args: args{ + ctx: CTX, + idpID: "unknown", + }, + want: http.StatusNotFound, + }, + { + name: "saml certificate, invalid idp type", + args: args{ + ctx: CTX, + idpID: oauthIdpID, + }, + want: http.StatusBadRequest, + }, + { + name: "saml certificate, ok", + args: args{ + ctx: CTX, + idpID: samlRedirectIdpID, + }, + want: http.StatusOK, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + certificateURL := http_util.BuildOrigin(Tester.Host(), Tester.Server.Config.ExternalSecure) + "/idps/" + tt.args.idpID + "/saml/certificate" + resp, err := http.Get(certificateURL) + assert.NoError(t, err) + assert.Equal(t, tt.want, resp.StatusCode) + if tt.want == http.StatusOK { + b, err := io.ReadAll(resp.Body) + defer resp.Body.Close() + assert.NoError(t, err) + + block, _ := pem.Decode(b) + _, err = x509.ParseCertificate(block.Bytes) + assert.NoError(t, err) + } + }) + } +} + +func TestServer_SAMLMetadata(t *testing.T) { + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) + oauthIdpID := Tester.AddGenericOAuthProvider(t) + + type args struct { + ctx context.Context + idpID string + } + tests := []struct { + name string + args args + want int + }{ + { + name: "saml metadata, invalid idp", + args: args{ + ctx: CTX, + idpID: "unknown", + }, + want: http.StatusNotFound, + }, + { + name: "saml metadata, invalid idp type", + args: args{ + ctx: CTX, + idpID: oauthIdpID, + }, + want: http.StatusBadRequest, + }, + { + name: "saml metadata, ok", + args: args{ + ctx: CTX, + idpID: samlRedirectIdpID, + }, + want: http.StatusOK, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + metadataURL := http_util.BuildOrigin(Tester.Host(), Tester.Server.Config.ExternalSecure) + "/idps/" + tt.args.idpID + "/saml/metadata" + resp, err := http.Get(metadataURL) + assert.NoError(t, err) + assert.Equal(t, tt.want, resp.StatusCode) + if tt.want == http.StatusOK { + b, err := io.ReadAll(resp.Body) + defer resp.Body.Close() + assert.NoError(t, err) + + _, err = saml_xml.ParseMetadataXmlIntoStruct(b) + assert.NoError(t, err) + } + + }) + } +} + +func TestServer_SAMLACS(t *testing.T) { + userHuman := Tester.CreateHumanUser(CTX) + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) + externalUserID := "test1" + linkedExternalUserID := "test2" + Tester.CreateUserIDPlink(CTX, userHuman.UserId, linkedExternalUserID, samlRedirectIdpID, linkedExternalUserID) + idp, err := getIDP( + http_util.BuildOrigin(Tester.Host(), Tester.Server.Config.ExternalSecure), + []string{samlRedirectIdpID}, + externalUserID, + linkedExternalUserID, + ) + assert.NoError(t, err) + + type args struct { + ctx context.Context + successURL string + failureURL string + idpID string + username string + intentID string + response string + } + type want struct { + successful bool + user string + } + tests := []struct { + name string + args args + want want + wantErr bool + }{ + { + name: "intent invalid", + args: args{ + ctx: CTX, + successURL: "https://example.com/success", + failureURL: "https://example.com/failure", + idpID: samlRedirectIdpID, + username: externalUserID, + intentID: "notexisting", + }, + want: want{ + successful: false, + user: "", + }, + wantErr: true, + }, + { + name: "response invalid", + args: args{ + ctx: CTX, + successURL: "https://example.com/success", + failureURL: "https://example.com/failure", + idpID: samlRedirectIdpID, + username: externalUserID, + response: "invalid", + }, + want: want{ + successful: false, + user: "", + }, + }, + { + name: "saml flow redirect, ok", + args: args{ + ctx: CTX, + successURL: "https://example.com/success", + failureURL: "https://example.com/failure", + idpID: samlRedirectIdpID, + username: externalUserID, + }, + want: want{ + successful: true, + user: "", + }, + }, + { + name: "saml flow redirect with link, ok", + args: args{ + ctx: CTX, + successURL: "https://example.com/success", + failureURL: "https://example.com/failure", + idpID: samlRedirectIdpID, + username: linkedExternalUserID, + }, + want: want{ + successful: true, + user: userHuman.UserId, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.StartIdentityProviderFlow(tt.args.ctx, + &user.StartIdentityProviderFlowRequest{ + IdpId: tt.args.idpID, + Content: &user.StartIdentityProviderFlowRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: tt.args.successURL, + FailureUrl: tt.args.failureURL, + }, + }, + }, + ) + // can't fail as covered in other tests + require.NoError(t, err) + + //parse returned URL to continue flow to callback with the same intentID==RelayState + authURL, err := url.Parse(got.GetAuthUrl()) + require.NoError(t, err) + samlRequest := &http.Request{Method: http.MethodGet, URL: authURL} + assert.NotEmpty(t, authURL) + + //generate necessary information to create request to callback URL + relayState := authURL.Query().Get("RelayState") + //test purposes, use defined intentID + if tt.args.intentID != "" { + relayState = tt.args.intentID + } + callbackURL := http_util.BuildOrigin(Tester.Host(), Tester.Server.Config.ExternalSecure) + "/idps/" + tt.args.idpID + "/saml/acs" + response := createResponse(t, idp, samlRequest, tt.args.username) + //test purposes, use defined response + if tt.args.response != "" { + response = tt.args.response + } + req := httpPostFormRequest(t, callbackURL, relayState, response) + + //do request to callback URL and check redirect to either success or failure url + location, err := integration.CheckRedirect(req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, relayState, location.Query().Get("id")) + if tt.want.successful { + assert.True(t, strings.HasPrefix(location.String(), tt.args.successURL)) + assert.NotEmpty(t, location.Query().Get("token")) + assert.Equal(t, tt.want.user, location.Query().Get("user")) + } else { + assert.True(t, strings.HasPrefix(location.String(), tt.args.failureURL)) + } + } + }) + } +} + +var key = func() crypto.PrivateKey { + b, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0OhbMuizgtbFOfwbK7aURuXhZx6VRuAs3nNibiuifwCGz6u9 +yy7bOR0P+zqN0YkjxaokqFgra7rXKCdeABmoLqCC0U+cGmLNwPOOA0PaD5q5xKhQ +4Me3rt/R9C4Ca6k3/OnkxnKwnogcsmdgs2l8liT3qVHP04Oc7Uymq2v09bGb6nPu +fOrkXS9F6mSClxHG/q59AGOWsXK1xzIRV1eu8W2SNdyeFVU1JHiQe444xLoPul5t +InWasKayFsPlJfWNc8EoU8COjNhfo/GovFTHVjh9oUR/gwEFVwifIHihRE0Hazn2 +EQSLaOr2LM0TsRsQroFjmwSGgI+X2bfbMTqWOQIDAQABAoIBAFWZwDTeESBdrLcT +zHZe++cJLxE4AObn2LrWANEv5AeySYsyzjRBYObIN9IzrgTb8uJ900N/zVr5VkxH +xUa5PKbOcowd2NMfBTw5EEnaNbILLm+coHdanrNzVu59I9TFpAFoPavrNt/e2hNo +NMGPSdOkFi81LLl4xoadz/WR6O/7N2famM+0u7C2uBe+TrVwHyuqboYoidJDhO8M +w4WlY9QgAUhkPyzZqrl+VfF1aDTGVf4LJgaVevfFCas8Ws6DQX5q4QdIoV6/0vXi +B1M+aTnWjHuiIzjBMWhcYW2+I5zfwNWRXaxdlrYXRukGSdnyO+DH/FhHePJgmlkj +NInADDkCgYEA6MEQFOFSCc/ELXYWgStsrtIlJUcsLdLBsy1ocyQa2lkVUw58TouW +RciE6TjW9rp31pfQUnO2l6zOUC6LT9Jvlb9PSsyW+rvjtKB5PjJI6W0hjX41wEO6 +fshFELMJd9W+Ezao2AsP2hZJ8McCF8no9e00+G4xTAyxHsNI2AFTCQcCgYEA5cWZ +JwNb4t7YeEajPt9xuYNUOQpjvQn1aGOV7KcwTx5ELP/Hzi723BxHs7GSdrLkkDmi +Gpb+mfL4wxCt0fK0i8GFQsRn5eusyq9hLqP/bmjpHoXe/1uajFbE1fZQR+2LX05N +3ATlKaH2hdfCJedFa4wf43+cl6Yhp6ZA0Yet1r8CgYEAwiu1j8W9G+RRA5/8/DtO +yrUTOfsbFws4fpLGDTA0mq0whf6Soy/96C90+d9qLaC3srUpnG9eB0CpSOjbXXbv +kdxseLkexwOR3bD2FHX8r4dUM2bzznZyEaxfOaQypN8SV5ME3l60Fbr8ajqLO288 +wlTmGM5Mn+YCqOg/T7wjGmcCgYBpzNfdl/VafOROVbBbhgXWtzsz3K3aYNiIjbp+ +MunStIwN8GUvcn6nEbqOaoiXcX4/TtpuxfJMLw4OvAJdtxUdeSmEee2heCijV6g3 +ErrOOy6EqH3rNWHvlxChuP50cFQJuYOueO6QggyCyruSOnDDuc0BM0SGq6+5g5s7 +H++S/wKBgQDIkqBtFr9UEf8d6JpkxS0RXDlhSMjkXmkQeKGFzdoJcYVFIwq8jTNB +nJrVIGs3GcBkqGic+i7rTO1YPkquv4dUuiIn+vKZVoO6b54f+oPBXd4S0BnuEqFE +rdKNuCZhiaE2XD9L/O9KP1fh5bfEcKwazQ23EvpJHBMm8BGC+/YZNw== +-----END RSA PRIVATE KEY-----`)) + k, _ := x509.ParsePKCS1PrivateKey(b.Bytes) + return k +}() + +var cert = func() *x509.Certificate { + b, _ := pem.Decode([]byte(`-----BEGIN CERTIFICATE----- +MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV +BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5 +NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8A +hs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+a +ucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWx +m+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6 +D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURN +B2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0O +BBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56 +zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5 +pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uv +NONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEf +y/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL +/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsb +GFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTL +UzreO96WzlBBMtY= +-----END CERTIFICATE-----`)) + c, _ := x509.ParseCertificate(b.Bytes) + return c +}() + +func getIDP(zitadelBaseURL string, idpIDs []string, user1, user2 string) (*saml.IdentityProvider, error) { + baseURL, err := url.Parse("http://localhost:8000") + if err != nil { + return nil, err + } + + store := &samlidp.MemoryStore{} + hashedPassword1, _ := bcrypt.GenerateFromPassword([]byte("test"), bcrypt.DefaultCost) + err = store.Put("/users/"+user1, samlidp.User{ + Name: user1, + HashedPassword: hashedPassword1, + Groups: []string{"Administrators", "Users"}, + Email: "test@example.com", + CommonName: "Test Test", + Surname: "Test", + GivenName: "Test", + }) + if err != nil { + return nil, err + } + hashedPassword2, _ := bcrypt.GenerateFromPassword([]byte("test"), bcrypt.DefaultCost) + err = store.Put("/users/"+user2, samlidp.User{ + Name: user2, + HashedPassword: hashedPassword2, + Groups: []string{"Administrators", "Users"}, + Email: "test@example.com", + CommonName: "Test Test", + Surname: "Test", + GivenName: "Test", + }) + if err != nil { + return nil, err + } + for _, idpID := range idpIDs { + metadata, err := saml_xml.ReadMetadataFromURL(http.DefaultClient, zitadelBaseURL+"/idps/"+idpID+"/saml/metadata") + if err != nil { + return nil, err + } + entity := new(saml.EntityDescriptor) + if err := xml.Unmarshal(metadata, entity); err != nil { + return nil, err + } + + if err := store.Put("/services/"+idpID, samlidp.Service{ + Name: idpID, + Metadata: *entity, + }); err != nil { + return nil, err + } + } + + idpServer, err := samlidp.New(samlidp.Options{ + URL: *baseURL, + Key: key, + Certificate: cert, + Store: store, + }) + if err != nil { + return nil, err + } + if idpServer.IDP.AssertionMaker == nil { + idpServer.IDP.AssertionMaker = &saml.DefaultAssertionMaker{} + } + return &idpServer.IDP, nil +} + +func createResponse(t *testing.T, idp *saml.IdentityProvider, req *http.Request, username string) string { + authnReq, err := saml.NewIdpAuthnRequest(idp, req) + assert.NoError(t, authnReq.Validate()) + + err = idp.AssertionMaker.MakeAssertion(authnReq, &saml.Session{ + CreateTime: time.Now().UTC(), + Index: "", + NameID: username, + }) + assert.NoError(t, err) + err = authnReq.MakeResponse() + assert.NoError(t, err) + + doc := etree.NewDocument() + doc.SetRoot(authnReq.ResponseEl) + responseBuf, err := doc.WriteToBytes() + assert.NoError(t, err) + responseBuf = append([]byte(""), responseBuf...) + + return base64.StdEncoding.EncodeToString(responseBuf) +} + +func httpGETRequest(t *testing.T, callbackURL string, relayState, response, sig, sigAlg string) *http.Request { + req, err := http.NewRequest("GET", callbackURL, nil) + require.NoError(t, err) + + q := req.URL.Query() + q.Add("RelayState", relayState) + q.Add("SAMLResponse", response) + if sig != "" { + q.Add("Sig", sig) + } + if sigAlg != "" { + q.Add("SigAlg", sigAlg) + } + req.URL.RawQuery = q.Encode() + return req +} + +func httpPostFormRequest(t *testing.T, callbackURL, relayState, response string) *http.Request { + body := url.Values{ + "SAMLResponse": {response}, + "RelayState": {relayState}, + } + + req, err := http.NewRequest("POST", callbackURL, strings.NewReader(body.Encode())) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.ParseForm() + return req +} diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 475798e83e..c9cc94203f 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" + "github.com/crewjam/saml/samlsp" "github.com/zitadel/logging" "github.com/zitadel/oidc/v2/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/oidc" @@ -12,6 +13,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_utils "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -27,6 +29,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" + "github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker" "github.com/zitadel/zitadel/internal/query" ) @@ -43,6 +47,9 @@ type externalIDPCallbackData struct { State string `schema:"state"` Code string `schema:"code"` + RelayState string `schema:"RelayState"` + Method string `schema:"Method"` + // Apple returns a user on first registration User string `schema:"user"` } @@ -167,6 +174,8 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai provider, err = l.appleProvider(r.Context(), identityProvider) case domain.IDPTypeLDAP: provider, err = l.ldapProvider(r.Context(), identityProvider) + case domain.IDPTypeSAML: + provider, err = l.samlProvider(r.Context(), identityProvider) case domain.IDPTypeUnspecified: fallthrough default: @@ -183,7 +192,17 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai l.renderLogin(w, r, authReq, err) return } - http.Redirect(w, r, session.GetAuthURL(), http.StatusFound) + + content, redirect := session.GetAuth(r.Context()) + if redirect { + http.Redirect(w, r, content, http.StatusFound) + return + } + _, err = w.Write([]byte(content)) + if err != nil { + l.renderError(w, r, authReq, err) + return + } } // handleExternalLoginCallbackForm handles the callback from a IDP with form_post. @@ -195,6 +214,7 @@ func (l *Login) handleExternalLoginCallbackForm(w http.ResponseWriter, r *http.R l.renderLogin(w, r, nil, err) return } + r.Form.Add("Method", http.MethodPost) http.Redirect(w, r, HandlerPrefix+EndpointExternalLoginCallback+"?"+r.Form.Encode(), 302) } @@ -207,6 +227,15 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque l.renderLogin(w, r, nil, err) return } + if data.State == "" { + data.State = data.RelayState + } + // workaround because of CSRF on external identity provider flows + if data.Method == http.MethodPost { + r.Method = http.MethodPost + r.PostForm = r.Form + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID) if err != nil { @@ -284,6 +313,18 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque return } session = &apple.Session{Session: &openid.Session{Provider: provider.(*apple.Provider).Provider, Code: data.Code}, UserFormValue: data.User} + case domain.IDPTypeSAML: + provider, err = l.samlProvider(r.Context(), identityProvider) + if err != nil { + l.externalAuthFailed(w, r, authReq, nil, nil, err) + return + } + sp, err := provider.(*saml.Provider).GetSP() + if err != nil { + l.externalAuthFailed(w, r, authReq, nil, nil, err) + return + } + session = &saml.Session{ServiceProvider: sp, RequestID: authReq.SAMLRequestID, Request: r} case domain.IDPTypeJWT, domain.IDPTypeLDAP, domain.IDPTypeUnspecified: @@ -881,6 +922,49 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe ) } +func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*saml.Provider, error) { + key, err := crypto.Decrypt(identityProvider.SAMLIDPTemplate.Key, l.idpConfigAlg) + if err != nil { + return nil, err + } + opts := make([]saml.ProviderOpts, 0, 2) + if identityProvider.SAMLIDPTemplate.WithSignedRequest { + opts = append(opts, saml.WithSignedRequest()) + } + if identityProvider.SAMLIDPTemplate.Binding != "" { + opts = append(opts, saml.WithBinding(identityProvider.SAMLIDPTemplate.Binding)) + } + opts = append(opts, + saml.WithEntityID(http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure)+"/idps/"+identityProvider.ID+"/saml/metadata"), + saml.WithCustomRequestTracker( + requesttracker.New( + func(ctx context.Context, authRequestID, samlRequestID string) error { + useragent, _ := http_mw.UserAgentIDFromCtx(ctx) + return l.authRepo.SaveSAMLRequestID(ctx, authRequestID, samlRequestID, useragent) + }, + func(ctx context.Context, authRequestID string) (*samlsp.TrackedRequest, error) { + useragent, _ := http_mw.UserAgentIDFromCtx(ctx) + auhRequest, err := l.authRepo.AuthRequestByID(ctx, authRequestID, useragent) + if err != nil { + return nil, err + } + return &samlsp.TrackedRequest{ + SAMLRequestID: auhRequest.SAMLRequestID, + Index: authRequestID, + }, nil + }, + ), + )) + return saml.New( + identityProvider.Name, + l.baseURL(ctx)+EndpointExternalLogin+"/", + identityProvider.SAMLIDPTemplate.Metadata, + identityProvider.SAMLIDPTemplate.Certificate, + key, + opts..., + ) +} + func (l *Login) azureProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*azuread.Provider, error) { secret, err := crypto.DecryptString(identityProvider.AzureADIDPTemplate.ClientSecret, l.idpConfigAlg) if err != nil { diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 4c5bd0d608..06a7787b59 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -126,7 +126,7 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu } // ignore form post callback // it will redirect to the "normal" callback, where the cookie is set again - if r.URL.Path == EndpointExternalLoginCallbackFormPost && r.Method == http.MethodPost { + if (r.URL.Path == EndpointExternalLoginCallbackFormPost || r.URL.Path == EndpointSAMLACS) && r.Method == http.MethodPost { handler.ServeHTTP(w, r) return } diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index 1bef0debd5..34a0092607 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -14,6 +14,7 @@ const ( EndpointExternalLogin = "/login/externalidp" EndpointExternalLoginCallback = "/login/externalidp/callback" EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form" + EndpointSAMLACS = "/login/externalidp/saml/acs" EndpointJWTAuthorize = "/login/jwt/authorize" EndpointJWTCallback = "/login/jwt/callback" EndpointLDAPLogin = "/login/ldap" @@ -73,6 +74,8 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallbackFormPost, login.handleExternalLoginCallbackForm).Methods(http.MethodPost) + router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallback).Methods(http.MethodGet) + router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallbackForm).Methods(http.MethodPost) router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet) router.HandleFunc(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet) router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost) diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index db22705050..d101a436ff 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -12,6 +12,7 @@ type AuthRequestRepository interface { AuthRequestByIDCheckLoggedIn(ctx context.Context, id, userAgentID string) (*domain.AuthRequest, error) AuthRequestByCode(ctx context.Context, code string) (*domain.AuthRequest, error) SaveAuthCode(ctx context.Context, id, code, userAgentID string) error + SaveSAMLRequestID(ctx context.Context, id, requestID, userAgentID string) error DeleteAuthRequest(ctx context.Context, id string) error CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index dde6034954..763d36f45f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -193,6 +193,17 @@ func (repo *AuthRequestRepo) SaveAuthCode(ctx context.Context, id, code, userAge return repo.AuthRequests.UpdateAuthRequest(ctx, request) } +func (repo *AuthRequestRepo) SaveSAMLRequestID(ctx context.Context, id, requestID, userAgentID string) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + request, err := repo.getAuthRequest(ctx, id, userAgentID) + if err != nil { + return err + } + request.SAMLRequestID = requestID + return repo.AuthRequests.UpdateAuthRequest(ctx, request) +} + func (repo *AuthRequestRepo) AuthRequestByCode(ctx context.Context, code string) (_ *domain.AuthRequest, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/command.go b/internal/command/command.go index 9e8e78e398..6288bbd234 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -2,7 +2,13 @@ package command import ( "context" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" "net/http" + "strconv" "time" "github.com/zitadel/zitadel/internal/api/authz" @@ -74,6 +80,8 @@ type Commands struct { publicKeyLifetime time.Duration certificateLifetime time.Duration defaultSecretGenerators *SecretGenerators + + samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error) } func StartCommands( @@ -131,6 +139,7 @@ func StartCommands( defaultRefreshTokenLifetime: defaultRefreshTokenLifetime, defaultRefreshTokenIdleLifetime: defaultRefreshTokenIdleLifetime, defaultSecretGenerators: defaultSecretGenerators, + samlCertificateAndKeyGenerator: samlCertificateAndKeyGenerator(defaults.KeyConfig.Size), } instance_repo.RegisterEventMappers(repo.eventstore) @@ -211,3 +220,36 @@ func exists(ctx context.Context, filter preparation.FilterToQueryReducer, wm exi } return wm.Exists(), nil } + +func samlCertificateAndKeyGenerator(keySize int) func(id string) ([]byte, []byte, error) { + return func(id string) ([]byte, []byte, error) { + priv, pub, err := crypto.GenerateKeyPair(keySize) + if err != nil { + return nil, nil, err + } + + serial, err := strconv.Atoi(id) + if err != nil { + return nil, nil, err + } + template := x509.Certificate{ + SerialNumber: big.NewInt(int64(serial)), + Subject: pkix.Name{ + Organization: []string{"ZITADEL"}, + SerialNumber: id, + }, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv) + if err != nil { + return nil, nil, errors.ThrowInternalf(err, "COMMAND-x92u101j", "failed to create certificate") + } + + keyBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)} + certBlock := &pem.Block{Type: "CERTIFICATE", Bytes: derBytes} + return pem.EncodeToMemory(keyBlock), pem.EncodeToMemory(certBlock), nil + } +} diff --git a/internal/command/idp.go b/internal/command/idp.go index 5a21596a29..3dc8f1ad50 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -110,6 +110,15 @@ type LDAPProvider struct { IDPOptions idp.Options } +type SAMLProvider struct { + Name string + Metadata []byte + MetadataURL string + Binding string + WithSignedRequest bool + IDPOptions idp.Options +} + type AppleProvider struct { Name string ClientID string diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 21b4cd7d94..265293a3a8 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -4,8 +4,11 @@ import ( "context" "encoding/base64" "encoding/json" + "encoding/xml" "net/url" + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" "github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/zitadel/internal/command/preparation" @@ -76,12 +79,36 @@ func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureU return writeModel, writeModelToObjectDetails(&writeModel.WriteModel), nil } -func (c *Commands) GetProvider(ctx context.Context, idpID string, callbackURL string) (idp.Provider, error) { +func (c *Commands) GetProvider(ctx context.Context, idpID string, idpCallback string, samlRootURL string) (idp.Provider, error) { writeModel, err := IDPProviderWriteModel(ctx, c.eventstore.Filter, idpID) if err != nil { return nil, err } - return writeModel.ToProvider(callbackURL, c.idpConfigEncryption) + if writeModel.IDPType != domain.IDPTypeSAML { + return writeModel.ToProvider(idpCallback, c.idpConfigEncryption) + } + return writeModel.ToSAMLProvider( + samlRootURL, + c.idpConfigEncryption, + func(ctx context.Context, intentID string) (*samlsp.TrackedRequest, error) { + intent, err := c.GetActiveIntent(ctx, intentID) + if err != nil { + return nil, err + } + return &samlsp.TrackedRequest{ + SAMLRequestID: intent.RequestID, + Index: intentID, + URI: intent.SuccessURL.String(), + }, nil + }, + func(ctx context.Context, intentID, samlRequestID string) error { + intent, err := c.GetActiveIntent(ctx, intentID) + if err != nil { + return err + } + return c.RequestSAMLIDPIntent(ctx, intent, samlRequestID) + }, + ) } func (c *Commands) GetActiveIntent(ctx context.Context, intentID string) (*IDPIntentWriteModel, error) { @@ -98,16 +125,18 @@ func (c *Commands) GetActiveIntent(ctx context.Context, intentID string) (*IDPIn return intent, nil } -func (c *Commands) AuthURLFromProvider(ctx context.Context, idpID, state string, callbackURL string) (string, error) { - provider, err := c.GetProvider(ctx, idpID, callbackURL) +func (c *Commands) AuthFromProvider(ctx context.Context, idpID, state string, idpCallback, samlRootURL string) (string, bool, error) { + provider, err := c.GetProvider(ctx, idpID, idpCallback, samlRootURL) if err != nil { - return "", err + return "", false, err } session, err := provider.BeginAuth(ctx, state) if err != nil { - return "", err + return "", false, err } - return session.GetAuthURL(), nil + + content, redirect := session.GetAuth(ctx) + return content, redirect, nil } func getIDPIntentWriteModel(ctx context.Context, writeModel *IDPIntentWriteModel, filter preparation.FilterToQueryReducer) error { @@ -152,6 +181,47 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr return token, nil } +func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, assertion *saml.Assertion) (string, error) { + token, err := c.generateIntentToken(writeModel.AggregateID) + if err != nil { + return "", err + } + idpInfo, err := json.Marshal(idpUser) + if err != nil { + return "", err + } + assertionData, err := xml.Marshal(assertion) + if err != nil { + return "", err + } + assertionEnc, err := crypto.Encrypt(assertionData, c.idpConfigEncryption) + if err != nil { + return "", err + } + cmd := idpintent.NewSAMLSucceededEvent( + ctx, + &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + idpInfo, + idpUser.GetID(), + idpUser.GetPreferredUsername(), + userID, + assertionEnc, + ) + err = c.pushAppendAndReduce(ctx, writeModel, cmd) + if err != nil { + return "", err + } + return token, nil +} + +func (c *Commands) RequestSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, requestID string) error { + return c.pushAppendAndReduce(ctx, writeModel, idpintent.NewSAMLRequestEvent( + ctx, + &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + requestID, + )) +} + func (c *Commands) generateIntentToken(intentID string) (string, error) { token, err := c.idpConfigEncryption.Encrypt([]byte(intentID)) if err != nil { diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go index bf70c78a8c..b2242c3832 100644 --- a/internal/command/idp_intent_model.go +++ b/internal/command/idp_intent_model.go @@ -25,6 +25,9 @@ type IDPIntentWriteModel struct { IDPEntryAttributes map[string][]string + RequestID string + Assertion *crypto.CryptoValue + State domain.IDPIntentState aggregate *eventstore.Aggregate } @@ -46,6 +49,10 @@ func (wm *IDPIntentWriteModel) Reduce() error { wm.reduceStartedEvent(e) case *idpintent.SucceededEvent: wm.reduceOAuthSucceededEvent(e) + case *idpintent.SAMLSucceededEvent: + wm.reduceSAMLSucceededEvent(e) + case *idpintent.SAMLRequestEvent: + wm.reduceSAMLRequestEvent(e) case *idpintent.LDAPSucceededEvent: wm.reduceLDAPSucceededEvent(e) case *idpintent.FailedEvent: @@ -64,6 +71,8 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( idpintent.StartedEventType, idpintent.SucceededEventType, + idpintent.SAMLSucceededEventType, + idpintent.SAMLRequestEventType, idpintent.LDAPSucceededEventType, idpintent.FailedEventType, ). @@ -77,6 +86,15 @@ func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) { wm.State = domain.IDPIntentStateStarted } +func (wm *IDPIntentWriteModel) reduceSAMLSucceededEvent(e *idpintent.SAMLSucceededEvent) { + wm.UserID = e.UserID + wm.IDPUser = e.IDPUser + wm.IDPUserID = e.IDPUserID + wm.IDPUserName = e.IDPUserName + wm.Assertion = e.Assertion + wm.State = domain.IDPIntentStateSucceeded +} + func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) { wm.UserID = e.UserID wm.IDPUser = e.IDPUser @@ -96,6 +114,10 @@ func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededE wm.State = domain.IDPIntentStateSucceeded } +func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEvent) { + wm.RequestID = e.RequestID +} + func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) { wm.State = domain.IDPIntentStateFailed } diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 5591c7d648..aeffe72eb5 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -5,6 +5,7 @@ import ( "net/url" "testing" + "github.com/crewjam/saml" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -212,7 +213,7 @@ func TestCommands_CreateIntent(t *testing.T) { } } -func TestCommands_AuthURLFromProvider(t *testing.T) { +func TestCommands_AuthFromProvider(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore secretCrypto crypto.EncryptionAlgorithm @@ -222,10 +223,12 @@ func TestCommands_AuthURLFromProvider(t *testing.T) { idpID string state string callbackURL string + samlRootURL string } type res struct { - authURL string - err error + content string + redirect bool + err error } tests := []struct { name string @@ -296,7 +299,7 @@ func TestCommands_AuthURLFromProvider(t *testing.T) { }, }, { - "push", + "oauth auth redirect", fields{ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), eventstore: eventstoreExpect(t, @@ -351,7 +354,8 @@ func TestCommands_AuthURLFromProvider(t *testing.T) { callbackURL: "url", }, res{ - authURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state", + content: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state", + redirect: true, }, }, { @@ -440,7 +444,8 @@ func TestCommands_AuthURLFromProvider(t *testing.T) { callbackURL: "url", }, res{ - authURL: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=state", + content: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=state", + redirect: true, }, }, } @@ -450,9 +455,142 @@ func TestCommands_AuthURLFromProvider(t *testing.T) { eventstore: tt.fields.eventstore, idpConfigEncryption: tt.fields.secretCrypto, } - authURL, err := c.AuthURLFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL) + content, redirect, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL) require.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.authURL, authURL) + assert.Equal(t, tt.res.redirect, redirect) + assert.Equal(t, tt.res.content, content) + }) + } +} + +func TestCommands_AuthFromProvider_SAML(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + idpID string + state string + callbackURL string + samlRootURL string + } + type res struct { + url string + values map[string]string + err error + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "saml auth default redirect", + fields{ + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusherWithInstanceID( + "instance", + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate, + "idp", + "name", + []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "", + false, + rep_idp.Options{}, + )), + ), + expectFilter( + eventFromEventPusherWithInstanceID( + "instance", + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate, + "idp", + "name", + []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + }, []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + "", + false, + rep_idp.Options{}, + )), + ), + expectFilter( + eventFromEventPusherWithInstanceID( + "instance", + func() eventstore.Command { + success, _ := url.Parse("https://success.url") + failure, _ := url.Parse("https://failure.url") + return idpintent.NewStartedEvent( + context.Background(), + &idpintent.NewAggregate("id", "ro").Aggregate, + success, + failure, + "idp", + ) + }(), + ), + ), + expectRandomPush( + eventPusherToEvents( + idpintent.NewSAMLRequestEvent( + context.Background(), + &idpintent.NewAggregate("id", "ro").Aggregate, + "request", + ), + ), + ), + ), + }, + args{ + ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + idpID: "idp", + state: "id", + callbackURL: "url", + samlRootURL: "samlurl", + }, + res{ + url: "http://localhost:8000/sso", + values: map[string]string{ + "SAMLRequest": "", // generated IDs so not assertable + "RelayState": "id", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + } + content, _, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL) + require.ErrorIs(t, err, tt.res.err) + + authURL, err := url.Parse(content) + require.NoError(t, err) + + assert.Equal(t, tt.res.url, authURL.Scheme+"://"+authURL.Host+authURL.Path) + query := authURL.Query() + for k, v := range tt.res.values { + assert.True(t, query.Has(k)) + if v != "" { + assert.Equal(t, v, query.Get(k)) + } + } }) } } @@ -585,6 +723,193 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { } } +func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idpConfigEncryption crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + writeModel *IDPIntentWriteModel + idpUser idp.User + assertion *saml.Assertion + userID string + } + type res struct { + token string + err error + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "encryption fails", + fields{ + idpConfigEncryption: func() crypto.EncryptionAlgorithm { + m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) + m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed")) + return m + }(), + }, + args{ + ctx: context.Background(), + writeModel: NewIDPIntentWriteModel("id", "ro"), + }, + res{ + err: z_errors.ThrowInternal(nil, "id", "encryption failed"), + }, + }, + { + "push", + fields{ + idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: eventstoreExpect(t, + expectPush( + eventPusherToEvents( + idpintent.NewSAMLSucceededEvent( + context.Background(), + &idpintent.NewAggregate("id", "ro").Aggregate, + []byte(`{"sub":"id","preferred_username":"username"}`), + "id", + "username", + "", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + writeModel: NewIDPIntentWriteModel("id", "ro"), + assertion: &saml.Assertion{ID: "id"}, + idpUser: openid.NewUser(&oidc.UserInfo{ + Subject: "id", + UserInfoProfile: oidc.UserInfoProfile{ + PreferredUsername: "username", + }, + }), + }, + res{ + token: "aWQ", + }, + }, + { + "push with userID", + fields{ + idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: eventstoreExpect(t, + expectPush( + eventPusherToEvents( + idpintent.NewSAMLSucceededEvent( + context.Background(), + &idpintent.NewAggregate("id", "ro").Aggregate, + []byte(`{"sub":"id","preferred_username":"username"}`), + "id", + "username", + "user", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + writeModel: NewIDPIntentWriteModel("id", "ro"), + assertion: &saml.Assertion{ID: "id"}, + idpUser: openid.NewUser(&oidc.UserInfo{ + Subject: "id", + UserInfoProfile: oidc.UserInfoProfile{ + PreferredUsername: "username", + }, + }), + userID: "user", + }, + res{ + token: "aWQ", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.idpConfigEncryption, + } + got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.assertion) + require.ErrorIs(t, err, tt.res.err) + assert.Equal(t, tt.res.token, got) + }) + } +} + +func TestCommands_RequestSAMLIDPIntent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + writeModel *IDPIntentWriteModel + request string + } + type res struct { + err error + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "push", + fields{ + eventstore: eventstoreExpect(t, + expectPush( + eventPusherToEvents( + idpintent.NewSAMLRequestEvent( + context.Background(), + &idpintent.NewAggregate("id", "ro").Aggregate, + "request", + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + writeModel: NewIDPIntentWriteModel("id", "ro"), + request: "request", + }, + res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + } + err := c.RequestSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.request) + require.ErrorIs(t, err, tt.res.err) + require.Equal(t, tt.args.writeModel.RequestID, tt.args.request) + }) + } +} + func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index fea291c44c..2736087de9 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -24,6 +24,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" "github.com/zitadel/zitadel/internal/idp/providers/oidc" + saml2 "github.com/zitadel/zitadel/internal/idp/providers/saml" + "github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker" "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/repository/idpconfig" "github.com/zitadel/zitadel/internal/repository/instance" @@ -1721,6 +1723,153 @@ func (wm *AppleIDPWriteModel) GetProviderOptions() idp.Options { return wm.Options } +type SAMLIDPWriteModel struct { + eventstore.WriteModel + + Name string + ID string + Metadata []byte + Key *crypto.CryptoValue + Certificate []byte + Binding string + WithSignedRequest bool + idp.Options + + State domain.IDPState +} + +func (wm *SAMLIDPWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *idp.SAMLIDPAddedEvent: + wm.reduceAddedEvent(e) + case *idp.SAMLIDPChangedEvent: + wm.reduceChangedEvent(e) + case *idp.RemovedEvent: + wm.State = domain.IDPStateRemoved + } + } + return wm.WriteModel.Reduce() +} + +func (wm *SAMLIDPWriteModel) reduceAddedEvent(e *idp.SAMLIDPAddedEvent) { + wm.Name = e.Name + wm.Metadata = e.Metadata + wm.Key = e.Key + wm.Certificate = e.Certificate + wm.Binding = e.Binding + wm.WithSignedRequest = e.WithSignedRequest + wm.Options = e.Options + wm.State = domain.IDPStateActive +} + +func (wm *SAMLIDPWriteModel) reduceChangedEvent(e *idp.SAMLIDPChangedEvent) { + if e.Key != nil { + wm.Key = e.Key + } + if e.Certificate != nil { + wm.Certificate = e.Certificate + } + if e.Name != nil { + wm.Name = *e.Name + } + if e.Metadata != nil { + wm.Metadata = e.Metadata + } + if e.Binding != nil { + wm.Binding = *e.Binding + } + if e.WithSignedRequest != nil { + wm.WithSignedRequest = *e.WithSignedRequest + } + wm.Options.ReduceChanges(e.OptionChanges) +} + +func (wm *SAMLIDPWriteModel) NewChanges( + name string, + metadata, + key, + certificate []byte, + secretCrypto crypto.Crypto, + binding string, + withSignedRequest bool, + options idp.Options, +) ([]idp.SAMLIDPChanges, error) { + changes := make([]idp.SAMLIDPChanges, 0) + if key != nil { + keyEnc, err := crypto.Crypt(key, secretCrypto) + if err != nil { + return nil, err + } + changes = append(changes, idp.ChangeSAMLKey(keyEnc)) + } + if certificate != nil { + changes = append(changes, idp.ChangeSAMLCertificate(certificate)) + } + if wm.Name != name { + changes = append(changes, idp.ChangeSAMLName(name)) + } + if !reflect.DeepEqual(wm.Metadata, metadata) { + changes = append(changes, idp.ChangeSAMLMetadata(metadata)) + } + if wm.Binding != binding { + changes = append(changes, idp.ChangeSAMLBinding(binding)) + } + if wm.WithSignedRequest != withSignedRequest { + changes = append(changes, idp.ChangeSAMLWithSignedRequest(withSignedRequest)) + } + opts := wm.Options.Changes(options) + if !opts.IsZero() { + changes = append(changes, idp.ChangeSAMLOptions(opts)) + } + return changes, nil +} + +func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) { + key, err := crypto.Decrypt(wm.Key, idpAlg) + if err != nil { + return nil, err + } + + opts := make([]saml2.ProviderOpts, 0, 7) + if wm.IsCreationAllowed { + opts = append(opts, saml2.WithCreationAllowed()) + } + if wm.IsLinkingAllowed { + opts = append(opts, saml2.WithLinkingAllowed()) + } + if wm.IsAutoCreation { + opts = append(opts, saml2.WithAutoCreation()) + } + if wm.IsAutoUpdate { + opts = append(opts, saml2.WithAutoUpdate()) + } + if wm.WithSignedRequest { + opts = append(opts, saml2.WithSignedRequest()) + } + if wm.Binding != "" { + opts = append(opts, saml2.WithBinding(wm.Binding)) + } + opts = append(opts, saml2.WithCustomRequestTracker( + requesttracker.New( + addRequest, + getRequest, + ), + )) + return saml2.New( + wm.Name, + callbackURL, + wm.Metadata, + wm.Certificate, + key, + opts..., + ) +} + +func (wm *SAMLIDPWriteModel) GetProviderOptions() idp.Options { + return wm.Options +} + type IDPRemoveWriteModel struct { eventstore.WriteModel @@ -1753,6 +1902,8 @@ func (wm *IDPRemoveWriteModel) Reduce() error { wm.reduceAdded(e.ID) case *idp.AppleIDPAddedEvent: wm.reduceAdded(e.ID) + case *idp.SAMLIDPAddedEvent: + wm.reduceAdded(e.ID) case *idp.RemovedEvent: wm.reduceRemoved(e.ID) case *idpconfig.IDPConfigAddedEvent: @@ -1839,6 +1990,10 @@ func (wm *IDPTypeWriteModel) Reduce() error { wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate()) case *org.AppleIDPAddedEvent: wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate()) + case *instance.SAMLIDPAddedEvent: + wm.reduceAdded(e.ID, domain.IDPTypeSAML, e.Aggregate()) + case *org.SAMLIDPAddedEvent: + wm.reduceAdded(e.ID, domain.IDPTypeSAML, e.Aggregate()) case *instance.OIDCIDPMigratedAzureADEvent: wm.reduceChanged(e.ID, domain.IDPTypeAzureAD) case *org.OIDCIDPMigratedAzureADEvent: @@ -1915,6 +2070,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder { instance.GoogleIDPAddedEventType, instance.LDAPIDPAddedEventType, instance.AppleIDPAddedEventType, + instance.SAMLIDPAddedEventType, instance.OIDCIDPMigratedAzureADEventType, instance.OIDCIDPMigratedGoogleEventType, instance.IDPRemovedEventType, @@ -1934,6 +2090,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder { org.GoogleIDPAddedEventType, org.LDAPIDPAddedEventType, org.AppleIDPAddedEventType, + org.SAMLIDPAddedEventType, org.OIDCIDPMigratedAzureADEventType, org.OIDCIDPMigratedGoogleEventType, org.IDPRemovedEventType, @@ -1962,8 +2119,15 @@ type IDP interface { GetProviderOptions() idp.Options } +type SAMLIDP interface { + eventstore.QueryReducer + ToProvider(string, crypto.EncryptionAlgorithm, requesttracker.GetRequest, requesttracker.AddRequest) (providers.Provider, error) + GetProviderOptions() idp.Options +} + type AllIDPWriteModel struct { - model IDP + model IDP + samlModel SAMLIDP ID string IDPType domain.IDPType @@ -2003,6 +2167,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id) case domain.IDPTypeApple: writeModel.model = NewAppleInstanceIDPWriteModel(resourceOwner, id) + case domain.IDPTypeSAML: + writeModel.samlModel = NewSAMLInstanceIDPWriteModel(resourceOwner, id) case domain.IDPTypeUnspecified: fallthrough default: @@ -2032,6 +2198,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id) case domain.IDPTypeApple: writeModel.model = NewAppleOrgIDPWriteModel(resourceOwner, id) + case domain.IDPTypeSAML: + writeModel.samlModel = NewSAMLOrgIDPWriteModel(resourceOwner, id) case domain.IDPTypeUnspecified: fallthrough default: @@ -2042,21 +2210,44 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp } func (wm *AllIDPWriteModel) Reduce() error { - return wm.model.Reduce() + if wm.model != nil { + return wm.model.Reduce() + } + return wm.samlModel.Reduce() } func (wm *AllIDPWriteModel) Query() *eventstore.SearchQueryBuilder { - return wm.model.Query() + if wm.model != nil { + return wm.model.Query() + } + return wm.samlModel.Query() } func (wm *AllIDPWriteModel) AppendEvents(events ...eventstore.Event) { - wm.model.AppendEvents(events...) + if wm.model != nil { + wm.model.AppendEvents(events...) + return + } + wm.samlModel.AppendEvents(events...) } func (wm *AllIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) { + if wm.model == nil { + return nil, errors.ThrowInternal(nil, "COMMAND-afvf0gc9sa", "ErrorsIDPConfig.NotExisting") + } return wm.model.ToProvider(callbackURL, idpAlg) } func (wm *AllIDPWriteModel) GetProviderOptions() idp.Options { - return wm.model.GetProviderOptions() + if wm.model != nil { + return wm.model.GetProviderOptions() + } + return wm.samlModel.GetProviderOptions() +} + +func (wm *AllIDPWriteModel) ToSAMLProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) { + if wm.samlModel == nil { + return nil, errors.ThrowInternal(nil, "COMMAND-csi30hdscv", "ErrorsIDPConfig.NotExisting") + } + return wm.samlModel.ToProvider(callbackURL, idpAlg, getRequest, addRequest) } diff --git a/internal/command/idp_model_test.go b/internal/command/idp_model_test.go index 86626d4b42..42c2112bf9 100644 --- a/internal/command/idp_model_test.go +++ b/internal/command/idp_model_test.go @@ -18,8 +18,9 @@ func TestCommands_AllIDPWriteModel(t *testing.T) { idpType domain.IDPType } type res struct { - writeModelType interface{} - err error + writeModelType interface{} + samlWriteModelType interface{} + err error } tests := []struct { name string @@ -156,6 +157,19 @@ func TestCommands_AllIDPWriteModel(t *testing.T) { err: nil, }, }, + { + name: "writemodel instance saml", + args: args{ + resourceOwner: "owner", + instanceBool: true, + id: "id", + idpType: domain.IDPTypeSAML, + }, + res: res{ + samlWriteModelType: &InstanceSAMLIDPWriteModel{}, + err: nil, + }, + }, { name: "writemodel instance unspecified", args: args{ @@ -298,6 +312,19 @@ func TestCommands_AllIDPWriteModel(t *testing.T) { err: nil, }, }, + { + name: "writemodel org saml", + args: args{ + resourceOwner: "owner", + instanceBool: false, + id: "id", + idpType: domain.IDPTypeSAML, + }, + res: res{ + samlWriteModelType: &OrgSAMLIDPWriteModel{}, + err: nil, + }, + }, { name: "writemodel org unspecified", args: args{ @@ -316,7 +343,12 @@ func TestCommands_AllIDPWriteModel(t *testing.T) { wm, err := NewAllIDPWriteModel(tt.args.resourceOwner, tt.args.instanceBool, tt.args.id, tt.args.idpType) require.ErrorIs(t, err, tt.res.err) if wm != nil { - assert.IsType(t, tt.res.writeModelType, wm.model) + if tt.res.writeModelType != nil { + assert.IsType(t, tt.res.writeModelType, wm.model) + } + if tt.res.samlWriteModelType != nil { + assert.IsType(t, tt.res.samlWriteModelType, wm.samlModel) + } } }) } diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index a6d429f6c1..31b284f8c5 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -4,6 +4,8 @@ import ( "context" "strings" + "github.com/zitadel/saml/pkg/provider/xml" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" @@ -509,6 +511,71 @@ func (c *Commands) UpdateInstanceAppleProvider(ctx context.Context, id string, p return pushedEventsToObjectDetails(pushedEvents), nil } +func (c *Commands) AddInstanceSAMLProvider(ctx context.Context, provider SAMLProvider) (string, *domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + writeModel := NewSAMLInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceSAMLProvider(instanceAgg, writeModel, provider)) + if err != nil { + return "", nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return "", nil, err + } + return id, pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) UpdateInstanceSAMLProvider(ctx context.Context, id string, provider SAMLProvider) (*domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + writeModel := NewSAMLInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceSAMLProvider(instanceAgg, writeModel, provider)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) RegenerateInstanceSAMLProviderCertificate(ctx context.Context, id string) (*domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + writeModel := NewSAMLInstanceIDPWriteModel(instanceID, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRegenerateInstanceSAMLProviderCertificate(instanceAgg, writeModel)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteInstanceProvider(instanceAgg, id)) @@ -1652,6 +1719,151 @@ func (c *Commands) prepareUpdateInstanceAppleProvider(a *instance.Aggregate, wri } } +func (c *Commands) prepareAddInstanceSAMLProvider(a *instance.Aggregate, writeModel *InstanceSAMLIDPWriteModel, provider SAMLProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-o07zjotgnd", "Errors.Invalid.Argument") + } + if provider.Metadata == nil && provider.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, provider.MetadataURL) + if err != nil { + return nil, caos_errs.ThrowInvalidArgument(err, "INST-8vam1khq22", "Errors.Project.App.SAMLMetadataMissing") + } + provider.Metadata = data + } + if provider.Metadata == nil { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-3bi3esi16t", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + + key, cert, err := c.samlCertificateAndKeyGenerator(writeModel.ID) + if err != nil { + return nil, err + } + keyEnc, err := crypto.Encrypt(key, c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + instance.NewSAMLIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.Metadata, + keyEnc, + cert, + provider.Binding, + provider.WithSignedRequest, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateInstanceSAMLProvider(a *instance.Aggregate, writeModel *InstanceSAMLIDPWriteModel, provider SAMLProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-7o3rq1owpm", "Errors.Invalid.Argument") + } + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-q2s9rak7o9", "Errors.Invalid.Argument") + } + if provider.Metadata == nil && provider.MetadataURL == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-iw1rxnf4sf", "Errors.Invalid.Argument") + } + if provider.Metadata == nil && provider.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, provider.MetadataURL) + if err != nil { + return nil, caos_errs.ThrowInvalidArgument(err, "INST-iijz4h01if", "Errors.Project.App.SAMLMetadataMissing") + } + provider.Metadata = data + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INST-D3r1s", "Errors.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.Metadata, + nil, + nil, + c.idpConfigEncryption, + provider.Binding, + provider.WithSignedRequest, + provider.IDPOptions, + ) + if err != nil || event == nil { + return nil, err + } + return []eventstore.Command{event}, nil + }, nil + } +} + +func (c *Commands) prepareRegenerateInstanceSAMLProviderCertificate(a *instance.Aggregate, writeModel *InstanceSAMLIDPWriteModel) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-7de108gqya", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INST-76dbwsv9vm", "Errors.IDPConfig.NotExisting") + } + + key, cert, err := c.samlCertificateAndKeyGenerator(writeModel.ID) + if err != nil { + return nil, err + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + writeModel.Name, + writeModel.Metadata, + key, + cert, + c.idpConfigEncryption, + writeModel.Binding, + writeModel.WithSignedRequest, + writeModel.Options, + ) + if err != nil || event == nil { + return nil, err + } + return []eventstore.Command{event}, nil + }, nil + } +} + func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id string) preparation.Validation { return func() (preparation.CreateCommands, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index 1d1d33d313..87bd8de3c8 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -860,6 +860,79 @@ func (wm *InstanceAppleIDPWriteModel) NewChangedEvent( return instance.NewAppleIDPChangedEvent(ctx, aggregate, id, changes) } +type InstanceSAMLIDPWriteModel struct { + SAMLIDPWriteModel +} + +func NewSAMLInstanceIDPWriteModel(instanceID, id string) *InstanceSAMLIDPWriteModel { + return &InstanceSAMLIDPWriteModel{ + SAMLIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + }, + ID: id, + }, + } +} + +func (wm *InstanceSAMLIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *instance.SAMLIDPAddedEvent: + wm.SAMLIDPWriteModel.AppendEvents(&e.SAMLIDPAddedEvent) + case *instance.SAMLIDPChangedEvent: + wm.SAMLIDPWriteModel.AppendEvents(&e.SAMLIDPChangedEvent) + case *instance.IDPRemovedEvent: + wm.SAMLIDPWriteModel.AppendEvents(&e.RemovedEvent) + } + } +} + +func (wm *InstanceSAMLIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.SAMLIDPAddedEventType, + instance.SAMLIDPChangedEventType, + instance.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name string, + metadata, + key, + certificate []byte, + secretCrypto crypto.Crypto, + binding string, + withSignedRequest bool, + options idp.Options, +) (*instance.SAMLIDPChangedEvent, error) { + changes, err := wm.SAMLIDPWriteModel.NewChanges( + name, + metadata, + key, + certificate, + secretCrypto, + binding, + withSignedRequest, + options, + ) + if err != nil || len(changes) == 0 { + return nil, err + } + return instance.NewSAMLIDPChangedEvent(ctx, aggregate, id, changes) +} + type InstanceIDPRemoveWriteModel struct { IDPRemoveWriteModel } @@ -897,6 +970,8 @@ func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) wm.IDPRemoveWriteModel.AppendEvents(&e.GitLabSelfHostedIDPAddedEvent) case *instance.GoogleIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) + case *instance.SAMLIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.SAMLIDPAddedEvent) case *instance.LDAPIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) case *instance.AppleIDPAddedEvent: @@ -931,6 +1006,7 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { instance.GoogleIDPAddedEventType, instance.LDAPIDPAddedEventType, instance.AppleIDPAddedEventType, + instance.SAMLIDPAddedEventType, instance.IDPRemovedEventType, ). EventData(map[string]interface{}{"id": wm.ID}). diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 794109cfab..a0be4798c6 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -5318,3 +5318,527 @@ func TestCommandSide_UpdateInstanceAppleIDP(t *testing.T) { }) } } + +func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + certificateAndKeyGenerator func(id string) ([]byte, []byte, error) + } + type args struct { + ctx context.Context + provider SAMLProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: SAMLProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-o07zjotgnd", "")) + }, + }, + }, + { + "invalid metadata", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: SAMLProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-3bi3esi16t", "Errors.Invalid.Argument")) + }, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "", + false, + idp.Options{}, + )), + }, + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + certificateAndKeyGenerator: func(id string) ([]byte, []byte, error) { return []byte("key"), []byte("certificate"), nil }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "ok all set", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "binding", + true, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + )), + }, + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + certificateAndKeyGenerator: func(id string) ([]byte, []byte, error) { return []byte("key"), []byte("certificate"), nil }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + Binding: "binding", + WithSignedRequest: true, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + idpConfigEncryption: tt.fields.secretCrypto, + samlCertificateAndKeyGenerator: tt.fields.certificateAndKeyGenerator, + } + id, got, err := c.AddInstanceSAMLProvider(tt.args.ctx, tt.args.provider) + 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.id, id) + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + id string + provider SAMLProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: SAMLProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-7o3rq1owpm", "")) + }, + }, + }, + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: SAMLProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-q2s9rak7o9", "")) + }, + }, + }, + { + "invalid metadata", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: SAMLProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-iw1rxnf4sf", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "", + false, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "binding", + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + func() eventstore.Command { + t := true + event, _ := instance.NewSAMLIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + []idp.SAMLIDPChanges{ + idp.ChangeSAMLName("new name"), + idp.ChangeSAMLMetadata([]byte("new metadata")), + idp.ChangeSAMLBinding("new binding"), + idp.ChangeSAMLWithSignedRequest(true), + idp.ChangeSAMLOptions(idp.OptionChanges{ + IsCreationAllowed: &t, + IsLinkingAllowed: &t, + IsAutoCreation: &t, + IsAutoUpdate: &t, + }), + }, + ) + return event + }(), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: SAMLProvider{ + Name: "new name", + Metadata: []byte("new metadata"), + Binding: "new binding", + WithSignedRequest: true, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.UpdateInstanceSAMLProvider(tt.args.ctx, tt.args.id, tt.args.provider) + 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 TestCommandSide_RegenerateInstanceSAMLProviderCertificate(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + certificateAndKeyGenerator func(id string) ([]byte, []byte, error) + } + type args struct { + ctx context.Context + id string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-7de108gqya", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "binding", + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + func() eventstore.Command { + event, _ := instance.NewSAMLIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + []idp.SAMLIDPChanges{ + idp.ChangeSAMLKey(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("new key"), + }), + idp.ChangeSAMLCertificate([]byte("new certificate")), + }, + ) + return event + }(), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + certificateAndKeyGenerator: func(id string) ([]byte, []byte, error) { + return []byte("new key"), []byte("new certificate"), nil + }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + samlCertificateAndKeyGenerator: tt.fields.certificateAndKeyGenerator, + } + got, err := c.RegenerateInstanceSAMLProviderCertificate(tt.args.ctx, tt.args.id) + 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) + } + }) + } +} diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index 66afee2ec0..22479465c1 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -4,6 +4,8 @@ import ( "context" "strings" + "github.com/zitadel/saml/pkg/provider/xml" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -444,6 +446,68 @@ func (c *Commands) UpdateOrgLDAPProvider(ctx context.Context, resourceOwner, id return pushedEventsToObjectDetails(pushedEvents), nil } +func (c *Commands) AddOrgSAMLProvider(ctx context.Context, resourceOwner string, provider SAMLProvider) (string, *domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + id, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + writeModel := NewSAMLOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgSAMLProvider(orgAgg, writeModel, provider)) + if err != nil { + return "", nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return "", nil, err + } + return id, pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) UpdateOrgSAMLProvider(ctx context.Context, resourceOwner, id string, provider SAMLProvider) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + writeModel := NewSAMLOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgSAMLProvider(orgAgg, writeModel, provider)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) RegenerateOrgSAMLProviderCertificate(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + writeModel := NewSAMLOrgIDPWriteModel(resourceOwner, id) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRegenerateOrgSAMLProviderCertificate(orgAgg, writeModel)) + if err != nil { + return nil, err + } + if len(cmds) == 0 { + // no change, so return directly + return &domain.ObjectDetails{ + Sequence: writeModel.ProcessedSequence, + EventDate: writeModel.ChangeDate, + ResourceOwner: writeModel.ResourceOwner, + }, nil + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) AddOrgAppleProvider(ctx context.Context, resourceOwner string, provider AppleProvider) (string, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) id, err := c.idGenerator.Next() @@ -1639,6 +1703,150 @@ func (c *Commands) prepareUpdateOrgAppleProvider(a *org.Aggregate, writeModel *O } } +func (c *Commands) prepareAddOrgSAMLProvider(a *org.Aggregate, writeModel *OrgSAMLIDPWriteModel, provider SAMLProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-957lr0f8u3", "Errors.Invalid.Argument") + } + if provider.Metadata == nil && provider.MetadataURL == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-78isv6m53a", "Errors.Invalid.Argument") + } + if provider.Metadata == nil && provider.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, provider.MetadataURL) + if err != nil { + return nil, caos_errs.ThrowInvalidArgument(err, "ORG-ipzxvf3cv2", "Errors.Project.App.SAMLMetadataMissing") + } + provider.Metadata = data + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + key, cert, err := c.samlCertificateAndKeyGenerator(writeModel.ID) + if err != nil { + return nil, err + } + keyEnc, err := crypto.Encrypt(key, c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + org.NewSAMLIDPAddedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.Metadata, + keyEnc, + cert, + provider.Binding, + provider.WithSignedRequest, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareUpdateOrgSAMLProvider(a *org.Aggregate, writeModel *OrgSAMLIDPWriteModel, provider SAMLProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-wwdwdlaya0", "Errors.Invalid.Argument") + } + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-egixaofgyl", "Errors.Invalid.Argument") + } + if provider.Metadata == nil && provider.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, provider.MetadataURL) + if err != nil { + return nil, caos_errs.ThrowInvalidArgument(err, "ORG-bkaiyd3rfo", "Errors.Project.App.SAMLMetadataMissing") + } + provider.Metadata = data + } + if provider.Metadata == nil { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-j6spncd74m", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "ORG-z82dddndql", "Errors.Org.IDPConfig.NotExisting") + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.Metadata, + nil, + nil, + c.idpConfigEncryption, + provider.Binding, + provider.WithSignedRequest, + provider.IDPOptions, + ) + if err != nil || event == nil { + return nil, err + } + return []eventstore.Command{event}, nil + }, nil + } +} + +func (c *Commands) prepareRegenerateOrgSAMLProviderCertificate(a *org.Aggregate, writeModel *OrgSAMLIDPWriteModel) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-arv4vdrb6c", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "ORG-4dw21ch9o9", "Errors.Org.IDPConfig.NotExisting") + } + + key, cert, err := c.samlCertificateAndKeyGenerator(writeModel.ID) + if err != nil { + return nil, err + } + event, err := writeModel.NewChangedEvent( + ctx, + &a.Aggregate, + writeModel.ID, + writeModel.Name, + writeModel.Metadata, + key, + cert, + c.idpConfigEncryption, + writeModel.Binding, + writeModel.WithSignedRequest, + writeModel.Options, + ) + if err != nil || event == nil { + return nil, err + } + return []eventstore.Command{event}, nil + }, nil + } +} + func (c *Commands) prepareDeleteOrgProvider(a *org.Aggregate, resourceOwner, id string) preparation.Validation { return func() (preparation.CreateCommands, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index a543de5079..04eeeb8ac2 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -870,6 +870,81 @@ func (wm *OrgAppleIDPWriteModel) NewChangedEvent( return org.NewAppleIDPChangedEvent(ctx, aggregate, id, changes) } +type OrgSAMLIDPWriteModel struct { + SAMLIDPWriteModel +} + +func NewSAMLOrgIDPWriteModel(orgID, id string) *OrgSAMLIDPWriteModel { + return &OrgSAMLIDPWriteModel{ + SAMLIDPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + ID: id, + }, + } +} + +func (wm *OrgSAMLIDPWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *org.SAMLIDPAddedEvent: + wm.SAMLIDPWriteModel.AppendEvents(&e.SAMLIDPAddedEvent) + case *org.SAMLIDPChangedEvent: + wm.SAMLIDPWriteModel.AppendEvents(&e.SAMLIDPChangedEvent) + case *org.IDPRemovedEvent: + wm.SAMLIDPWriteModel.AppendEvents(&e.RemovedEvent) + default: + wm.SAMLIDPWriteModel.AppendEvents(e) + } + } +} + +func (wm *OrgSAMLIDPWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + org.SAMLIDPAddedEventType, + org.SAMLIDPChangedEventType, + org.IDPRemovedEventType, + ). + EventData(map[string]interface{}{"id": wm.ID}). + Builder() +} + +func (wm *OrgSAMLIDPWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name string, + metadata, + key, + certificate []byte, + secretCrypto crypto.Crypto, + binding string, + withSignedRequest bool, + options idp.Options, +) (*org.SAMLIDPChangedEvent, error) { + changes, err := wm.SAMLIDPWriteModel.NewChanges( + name, + metadata, + key, + certificate, + secretCrypto, + binding, + withSignedRequest, + options, + ) + if err != nil || len(changes) == 0 { + return nil, err + } + return org.NewSAMLIDPChangedEvent(ctx, aggregate, id, changes) +} + type OrgIDPRemoveWriteModel struct { IDPRemoveWriteModel } @@ -911,6 +986,8 @@ func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) { wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) case *org.AppleIDPAddedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.AppleIDPAddedEvent) + case *org.SAMLIDPAddedEvent: + wm.IDPRemoveWriteModel.AppendEvents(&e.SAMLIDPAddedEvent) case *org.IDPRemovedEvent: wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent) case *org.IDPConfigAddedEvent: @@ -941,6 +1018,7 @@ func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder { org.GoogleIDPAddedEventType, org.LDAPIDPAddedEventType, org.AppleIDPAddedEventType, + org.SAMLIDPAddedEventType, org.IDPRemovedEventType, ). EventData(map[string]interface{}{"id": wm.ID}). diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 641abb90b2..f85fb1c216 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -5396,3 +5396,534 @@ func TestCommandSide_UpdateOrgAppleIDP(t *testing.T) { func stringPointer(s string) *string { return &s } + +func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + certificateAndKeyGenerator func(id string) ([]byte, []byte, error) + } + type args struct { + ctx context.Context + resourceOwner string + provider SAMLProvider + } + type res struct { + id string + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: SAMLProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-957lr0f8u3", "")) + }, + }, + }, + { + "invalid metadata", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: SAMLProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-78isv6m53a", "")) + }, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + eventPusherToEvents( + org.NewSAMLIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "", + false, + idp.Options{}, + )), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), certificateAndKeyGenerator: func(id string) ([]byte, []byte, error) { return []byte("key"), []byte("certificate"), nil }, + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "ok all set", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + expectPush( + eventPusherToEvents( + org.NewSAMLIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "binding", + true, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + )), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + certificateAndKeyGenerator: func(id string) ([]byte, []byte, error) { return []byte("key"), []byte("certificate"), nil }, + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + Binding: "binding", + WithSignedRequest: true, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + id: "id1", + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + idpConfigEncryption: tt.fields.secretCrypto, + samlCertificateAndKeyGenerator: tt.fields.certificateAndKeyGenerator, + } + id, got, err := c.AddOrgSAMLProvider(tt.args.ctx, tt.args.resourceOwner, tt.args.provider) + 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.id, id) + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + id string + provider SAMLProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: SAMLProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-wwdwdlaya0", "")) + }, + }, + }, + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: SAMLProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-egixaofgyl", "")) + }, + }, + }, + { + "invalid metadata", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: SAMLProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-j6spncd74m", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowNotFound(nil, "ORG-z82dddndql", "")) + }, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewSAMLIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "", + false, + idp.Options{}, + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: SAMLProvider{ + Name: "name", + Metadata: []byte("metadata"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewSAMLIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "binding", + false, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + func() eventstore.Command { + t := true + event, _ := org.NewSAMLIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.SAMLIDPChanges{ + idp.ChangeSAMLName("new name"), + idp.ChangeSAMLMetadata([]byte("new metadata")), + idp.ChangeSAMLBinding("new binding"), + idp.ChangeSAMLWithSignedRequest(true), + idp.ChangeSAMLOptions(idp.OptionChanges{ + IsCreationAllowed: &t, + IsLinkingAllowed: &t, + IsAutoCreation: &t, + IsAutoUpdate: &t, + }), + }, + ) + return event + }(), + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: SAMLProvider{ + Name: "new name", + Metadata: []byte("new metadata"), + Binding: "new binding", + WithSignedRequest: true, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.UpdateOrgSAMLProvider(tt.args.ctx, tt.args.resourceOwner, tt.args.id, tt.args.provider) + 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 TestCommandSide_RegenerateOrgSAMLProviderCertificate(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + certificateAndKeyGenerator func(id string) ([]byte, []byte, error) + } + type args struct { + ctx context.Context + resourceOwner string + id string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-arv4vdrb6c", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowNotFound(nil, "ORG-4dw21ch9o9", "")) + }, + }, + }, + { + name: "change ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewSAMLIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + []byte("metadata"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "binding", + false, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + func() eventstore.Command { + event, _ := org.NewSAMLIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + []idp.SAMLIDPChanges{ + idp.ChangeSAMLKey(&crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("new key"), + }), + idp.ChangeSAMLCertificate([]byte("new certificate")), + }, + ) + return event + }(), + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + certificateAndKeyGenerator: func(id string) ([]byte, []byte, error) { + return []byte("new key"), []byte("new certificate"), nil + }, + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + samlCertificateAndKeyGenerator: tt.fields.certificateAndKeyGenerator, + } + got, err := c.RegenerateOrgSAMLProviderCertificate(tt.args.ctx, tt.args.resourceOwner, tt.args.id) + 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) + } + }) + } +} diff --git a/internal/command/quota.go b/internal/command/quota.go index 442772ae57..d3b1ab7e89 100644 --- a/internal/command/quota.go +++ b/internal/command/quota.go @@ -167,9 +167,6 @@ func (q *SetQuota) validate() error { if q.Unit.Enum() == quota.Unimplemented { return errors.ThrowInvalidArgument(nil, "QUOTA-OTeSh", "Errors.Quota.Invalid.Unimplemented") } - if q.Amount < 0 { - return errors.ThrowInvalidArgument(nil, "QUOTA-hOKSJ", "Errors.Quota.Invalid.Amount") - } if q.ResetInterval < time.Minute { return errors.ThrowInvalidArgument(nil, "QUOTA-R5otd", "Errors.Quota.Invalid.ResetInterval") } diff --git a/internal/command/quota_model.go b/internal/command/quota_model.go index 7336be41a6..23893ea6a2 100644 --- a/internal/command/quota_model.go +++ b/internal/command/quota_model.go @@ -178,7 +178,7 @@ func sortSetEventNotifications(notifications []*quota.SetEventNotification) (err } if i.Percent < j.Percent || i.Percent == j.Percent && i.CallURL < j.CallURL || - i.Percent == j.Percent && i.CallURL == j.CallURL && i.Repeat == false && j.Repeat == true { + i.Percent == j.Percent && i.CallURL == j.CallURL && !i.Repeat && j.Repeat { return -1 } return +1 diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index acd9e95ed4..7d776b6d30 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -55,6 +55,7 @@ type AuthRequest struct { LockoutPolicy *LockoutPolicy DefaultTranslations []*CustomText OrgTranslations []*CustomText + SAMLRequestID string } type ExternalUser struct { diff --git a/internal/domain/idp.go b/internal/domain/idp.go index 1378b6ac02..76c2e38cf9 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -37,6 +37,7 @@ const ( IDPTypeGitLabSelfHosted IDPTypeGoogle IDPTypeApple + IDPTypeSAML ) func (t IDPType) GetCSSClass() string { @@ -57,7 +58,8 @@ func (t IDPType) GetCSSClass() string { IDPTypeOIDC, IDPTypeJWT, IDPTypeOAuth, - IDPTypeLDAP: + IDPTypeLDAP, + IDPTypeSAML: fallthrough default: return "" @@ -90,7 +92,8 @@ func (t IDPType) DisplayName() string { IDPTypeLDAP, IDPTypeAzureAD, IDPTypeGitHubEnterprise, - IDPTypeGitLabSelfHosted: + IDPTypeGitLabSelfHosted, + IDPTypeSAML: fallthrough default: // we should never get here, so log it diff --git a/internal/idp/providers/apple/apple_test.go b/internal/idp/providers/apple/apple_test.go index 6356cca2ef..f3b7e81a1a 100644 --- a/internal/idp/providers/apple/apple_test.go +++ b/internal/idp/providers/apple/apple_test.go @@ -59,11 +59,13 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes) r.NoError(err) - - session, err := provider.BeginAuth(context.Background(), "testState") + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - - a.Equal(tt.want.GetAuthURL(), session.GetAuthURL()) + content, redirect := session.GetAuth(ctx) + contentExpected, redirectExpected := tt.want.GetAuth(ctx) + a.Equal(redirectExpected, redirect) + a.Equal(contentExpected, content) }) } } diff --git a/internal/idp/providers/azuread/azuread_test.go b/internal/idp/providers/azuread/azuread_test.go index 87b048ae9b..3febb43f95 100644 --- a/internal/idp/providers/azuread/azuread_test.go +++ b/internal/idp/providers/azuread/azuread_test.go @@ -77,10 +77,14 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...) r.NoError(err) - session, err := provider.BeginAuth(context.Background(), "testState") + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - a.Equal(tt.want.GetAuthURL(), session.GetAuthURL()) + wantHeaders, wantContent := tt.want.GetAuth(ctx) + gotHeaders, gotContent := session.GetAuth(ctx) + a.Equal(wantHeaders, gotHeaders) + a.Equal(wantContent, gotContent) }) } } diff --git a/internal/idp/providers/github/github_test.go b/internal/idp/providers/github/github_test.go index 63244f68d5..6274b51841 100644 --- a/internal/idp/providers/github/github_test.go +++ b/internal/idp/providers/github/github_test.go @@ -44,10 +44,14 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...) r.NoError(err) - session, err := provider.BeginAuth(context.Background(), "testState") + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - a.Equal(tt.want.GetAuthURL(), session.GetAuthURL()) + wantHeaders, wantContent := tt.want.GetAuth(ctx) + gotHeaders, gotContent := session.GetAuth(ctx) + a.Equal(wantHeaders, gotHeaders) + a.Equal(wantContent, gotContent) }) } } diff --git a/internal/idp/providers/gitlab/gitlab_test.go b/internal/idp/providers/gitlab/gitlab_test.go index 74fd66ccbc..24b813bc81 100644 --- a/internal/idp/providers/gitlab/gitlab_test.go +++ b/internal/idp/providers/gitlab/gitlab_test.go @@ -55,11 +55,14 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.opts...) r.NoError(err) - - session, err := provider.BeginAuth(context.Background(), "testState") + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - a.Equal(tt.want.GetAuthURL(), session.GetAuthURL()) + wantHeaders, wantContent := tt.want.GetAuth(ctx) + gotHeaders, gotContent := session.GetAuth(ctx) + a.Equal(wantHeaders, gotHeaders) + a.Equal(wantContent, gotContent) }) } } diff --git a/internal/idp/providers/google/google_test.go b/internal/idp/providers/google/google_test.go index d7fb3ecb81..b95f8eaf9f 100644 --- a/internal/idp/providers/google/google_test.go +++ b/internal/idp/providers/google/google_test.go @@ -44,10 +44,14 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes) r.NoError(err) - session, err := provider.BeginAuth(context.Background(), "testState") + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - a.Equal(tt.want.GetAuthURL(), session.GetAuthURL()) + wantHeaders, wantContent := tt.want.GetAuth(ctx) + gotHeaders, gotContent := session.GetAuth(ctx) + a.Equal(wantHeaders, gotHeaders) + a.Equal(wantContent, gotContent) }) } } diff --git a/internal/idp/providers/jwt/jwt_test.go b/internal/idp/providers/jwt/jwt_test.go index 2c8725fc97..7f9f8b0fc9 100644 --- a/internal/idp/providers/jwt/jwt_test.go +++ b/internal/idp/providers/jwt/jwt_test.go @@ -112,13 +112,17 @@ func TestProvider_BeginAuth(t *testing.T) { ) require.NoError(t, err) - session, err := provider.BeginAuth(context.Background(), "testState", tt.args.params...) + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState", tt.args.params...) if tt.want.err != nil && !tt.want.err(err) { a.Fail("invalid error", err) } if tt.want.err == nil { a.NoError(err) - a.Equal(tt.want.session.GetAuthURL(), session.GetAuthURL()) + wantHeaders, wantContent := tt.want.session.GetAuth(ctx) + gotHeaders, gotContent := session.GetAuth(ctx) + a.Equal(wantHeaders, gotHeaders) + a.Equal(wantContent, gotContent) } }) } diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index b0b69293e8..be3ffc0531 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -30,9 +30,9 @@ type Session struct { Tokens *oidc.Tokens[*oidc.IDTokenClaims] } -// GetAuthURL implements the [idp.Session] interface -func (s *Session) GetAuthURL() string { - return s.AuthURL +// GetAuth implements the [idp.Session] interface. +func (s *Session) GetAuth(ctx context.Context) (string, bool) { + return idp.Redirect(s.AuthURL) } // FetchUser implements the [idp.Session] interface. diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index e6422b5d26..6bd32525dd 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -29,8 +29,9 @@ type Session struct { Entry *ldap.Entry } -func (s *Session) GetAuthURL() string { - return s.loginUrl +// GetAuth implements the [idp.Session] interface. +func (s *Session) GetAuth(ctx context.Context) (string, bool) { + return idp.Redirect(s.loginUrl) } func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { diff --git a/internal/idp/providers/oauth/oauth2_test.go b/internal/idp/providers/oauth/oauth2_test.go index d145745918..5fdfbc2185 100644 --- a/internal/idp/providers/oauth/oauth2_test.go +++ b/internal/idp/providers/oauth/oauth2_test.go @@ -49,10 +49,14 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper) r.NoError(err) - session, err := provider.BeginAuth(context.Background(), "testState") + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - a.Equal(tt.want.GetAuthURL(), session.GetAuthURL()) + wantHeaders, wantContent := tt.want.GetAuth(ctx) + gotHeaders, gotContent := session.GetAuth(ctx) + a.Equal(wantHeaders, gotHeaders) + a.Equal(wantContent, gotContent) }) } } diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index 760fcefcfa..e85116afac 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -25,9 +25,9 @@ type Session struct { Provider *Provider } -// GetAuthURL implements the [idp.Session] interface. -func (s *Session) GetAuthURL() string { - return s.AuthURL +// GetAuth implements the [idp.Session] interface. +func (s *Session) GetAuth(ctx context.Context) (string, bool) { + return idp.Redirect(s.AuthURL) } // FetchUser implements the [idp.Session] interface. diff --git a/internal/idp/providers/oidc/oidc_test.go b/internal/idp/providers/oidc/oidc_test.go index c2310ac0d8..bbe08155c8 100644 --- a/internal/idp/providers/oidc/oidc_test.go +++ b/internal/idp/providers/oidc/oidc_test.go @@ -66,10 +66,14 @@ func TestProvider_BeginAuth(t *testing.T) { provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...) r.NoError(err) - session, err := provider.BeginAuth(context.Background(), "testState") + ctx := context.Background() + session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - a.Equal(tt.want.GetAuthURL(), session.GetAuthURL()) + wantHeaders, wantContent := tt.want.GetAuth(ctx) + gotHeaders, gotContent := session.GetAuth(ctx) + a.Equal(wantHeaders, gotHeaders) + a.Equal(wantContent, gotContent) }) } } diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index 215c24dab6..366e42643a 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -24,9 +24,9 @@ type Session struct { Tokens *oidc.Tokens[*oidc.IDTokenClaims] } -// GetAuthURL implements the [idp.Session] interface. -func (s *Session) GetAuthURL() string { - return s.AuthURL +// GetAuth implements the [idp.Session] interface. +func (s *Session) GetAuth(ctx context.Context) (string, bool) { + return idp.Redirect(s.AuthURL) } // FetchUser implements the [idp.Session] interface. diff --git a/internal/idp/providers/saml/mapper.go b/internal/idp/providers/saml/mapper.go new file mode 100644 index 0000000000..506d9b3a03 --- /dev/null +++ b/internal/idp/providers/saml/mapper.go @@ -0,0 +1,90 @@ +package saml + +import ( + "github.com/crewjam/saml" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp" +) + +var _ idp.User = (*UserMapper)(nil) + +// UserMapper is an implementation of [idp.User]. +type UserMapper struct { + ID string `json:"id,omitempty"` + Attributes map[string][]string `json:"attributes,omitempty"` +} + +func NewUser() *UserMapper { + return &UserMapper{Attributes: map[string][]string{}} +} + +func (u *UserMapper) SetID(id *saml.NameID) { + u.ID = id.Value +} + +// GetID is an implementation of the [idp.User] interface. +func (u *UserMapper) GetID() string { + return u.ID +} + +// GetFirstName is an implementation of the [idp.User] interface. +func (u *UserMapper) GetFirstName() string { + return "" +} + +// GetLastName is an implementation of the [idp.User] interface. +func (u *UserMapper) GetLastName() string { + return "" +} + +// GetDisplayName is an implementation of the [idp.User] interface. +func (u *UserMapper) GetDisplayName() string { + return "" +} + +// GetNickname is an implementation of the [idp.User] interface. +func (u *UserMapper) GetNickname() string { + return "" +} + +// GetPreferredUsername is an implementation of the [idp.User] interface. +func (u *UserMapper) GetPreferredUsername() string { + return "" +} + +// GetEmail is an implementation of the [idp.User] interface. +func (u *UserMapper) GetEmail() domain.EmailAddress { + return "" +} + +// IsEmailVerified is an implementation of the [idp.User] interface. +func (u *UserMapper) IsEmailVerified() bool { + return false +} + +// GetPhone is an implementation of the [idp.User] interface. +func (u *UserMapper) GetPhone() domain.PhoneNumber { + return "" +} + +// IsPhoneVerified is an implementation of the [idp.User] interface. +func (u *UserMapper) IsPhoneVerified() bool { + return false +} + +// GetPreferredLanguage is an implementation of the [idp.User] interface. +func (u *UserMapper) GetPreferredLanguage() language.Tag { + return language.Und +} + +// GetAvatarURL is an implementation of the [idp.User] interface. +func (u *UserMapper) GetAvatarURL() string { + return "" +} + +// GetProfile is an implementation of the [idp.User] interface. +func (u *UserMapper) GetProfile() string { + return "" +} diff --git a/internal/idp/providers/saml/requesttracker/request_tracker.go b/internal/idp/providers/saml/requesttracker/request_tracker.go new file mode 100644 index 0000000000..6c57386d61 --- /dev/null +++ b/internal/idp/providers/saml/requesttracker/request_tracker.go @@ -0,0 +1,58 @@ +package requesttracker + +import ( + "context" + "net/http" + + "github.com/crewjam/saml/samlsp" +) + +type GetRequest func(ctx context.Context, intentID string) (*samlsp.TrackedRequest, error) +type AddRequest func(ctx context.Context, intentID, requestID string) error + +type RequestTracker struct { + addRequest AddRequest + getRequest GetRequest +} + +func New(addRequestF AddRequest, getRequestF GetRequest) samlsp.RequestTracker { + return &RequestTracker{ + addRequest: addRequestF, + getRequest: getRequestF, + } +} + +func (rt *RequestTracker) TrackRequest(w http.ResponseWriter, r *http.Request, samlRequestID string) (index string, err error) { + // intentID is stored in r.URL + intentID := r.URL.String() + if err := rt.addRequest(r.Context(), intentID, samlRequestID); err != nil { + return "", err + } + return intentID, nil +} + +func (rt *RequestTracker) StopTrackingRequest(w http.ResponseWriter, r *http.Request, index string) error { + // error is not handled in SP logic + return nil +} + +func (rt *RequestTracker) GetTrackedRequests(r *http.Request) []samlsp.TrackedRequest { + // RelayState is the context of the auth flow and as such contains the intentID + intentID := r.FormValue("RelayState") + + request, err := rt.getRequest(r.Context(), intentID) + if err != nil { + return nil + } + return []samlsp.TrackedRequest{ + { + Index: request.Index, + SAMLRequestID: request.SAMLRequestID, + URI: request.URI, + }, + } +} + +func (rt *RequestTracker) GetTrackedRequest(r *http.Request, index string) (*samlsp.TrackedRequest, error) { + return rt.getRequest(r.Context(), index) +} diff --git a/internal/idp/providers/saml/saml.go b/internal/idp/providers/saml/saml.go new file mode 100644 index 0000000000..9e6623df7f --- /dev/null +++ b/internal/idp/providers/saml/saml.go @@ -0,0 +1,175 @@ +package saml + +import ( + "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/xml" + "net/url" + + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/idp" +) + +var _ idp.Provider = (*Provider)(nil) + +// Provider is the [idp.Provider] implementation for a generic SAML provider +type Provider struct { + name string + + requestTracker samlsp.RequestTracker + Certificate []byte + + spOptions *samlsp.Options + + binding string + + isLinkingAllowed bool + isCreationAllowed bool + isAutoCreation bool + isAutoUpdate bool +} + +type ProviderOpts func(provider *Provider) + +// WithLinkingAllowed allows end users to link the federated user to an existing one. +func WithLinkingAllowed() ProviderOpts { + return func(p *Provider) { + p.isLinkingAllowed = true + } +} + +// WithCreationAllowed allows end users to create a new user using the federated information. +func WithCreationAllowed() ProviderOpts { + return func(p *Provider) { + p.isCreationAllowed = true + } +} + +// WithAutoCreation enables that federated users are automatically created if not already existing. +func WithAutoCreation() ProviderOpts { + return func(p *Provider) { + p.isAutoCreation = true + } +} + +// WithAutoUpdate enables that information retrieved from the provider is automatically used to update +// the existing user on each authentication. +func WithAutoUpdate() ProviderOpts { + return func(p *Provider) { + p.isAutoUpdate = true + } +} + +func WithSignedRequest() ProviderOpts { + return func(p *Provider) { + p.spOptions.SignRequest = true + } +} + +func WithBinding(binding string) ProviderOpts { + return func(p *Provider) { + p.binding = binding + } +} + +func WithCustomRequestTracker(tracker samlsp.RequestTracker) ProviderOpts { + return func(p *Provider) { + p.requestTracker = tracker + } +} + +func WithEntityID(entityID string) ProviderOpts { + return func(p *Provider) { + p.spOptions.EntityID = entityID + } +} + +func New( + name string, + rootURLStr string, + metadata []byte, + certificate []byte, + key []byte, + options ...ProviderOpts, +) (*Provider, error) { + entityDescriptor := new(saml.EntityDescriptor) + if err := xml.Unmarshal(metadata, entityDescriptor); err != nil { + return nil, err + } + keyPair, err := tls.X509KeyPair(certificate, key) + if err != nil { + return nil, err + } + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + return nil, err + } + rootURL, err := url.Parse(rootURLStr) + if err != nil { + return nil, err + } + opts := samlsp.Options{ + URL: *rootURL, + Key: keyPair.PrivateKey.(*rsa.PrivateKey), + Certificate: keyPair.Leaf, + IDPMetadata: entityDescriptor, + SignRequest: false, + } + provider := &Provider{ + name: name, + spOptions: &opts, + Certificate: certificate, + } + for _, option := range options { + option(provider) + } + return provider, nil +} + +func (p *Provider) Name() string { + return p.name +} + +func (p *Provider) IsLinkingAllowed() bool { + return p.isLinkingAllowed +} + +func (p *Provider) IsCreationAllowed() bool { + return p.isCreationAllowed +} + +func (p *Provider) IsAutoCreation() bool { + return p.isAutoCreation +} + +func (p *Provider) IsAutoUpdate() bool { + return p.isAutoUpdate +} + +func (p *Provider) GetSP() (*samlsp.Middleware, error) { + sp, err := samlsp.New(*p.spOptions) + if err != nil { + return nil, errors.ThrowInternal(err, "SAML-qee09ffuq5", "Errors.Intent.IDPInvalid") + } + if p.requestTracker != nil { + sp.RequestTracker = p.requestTracker + } + return sp, nil +} + +func (p *Provider) BeginAuth(ctx context.Context, state string, params ...any) (idp.Session, error) { + m, err := p.GetSP() + if err != nil { + return nil, err + } + + return &Session{ + ServiceProvider: m, + state: state, + }, nil +} diff --git a/internal/idp/providers/saml/saml_test.go b/internal/idp/providers/saml/saml_test.go new file mode 100644 index 0000000000..28dc921681 --- /dev/null +++ b/internal/idp/providers/saml/saml_test.go @@ -0,0 +1,161 @@ +package saml + +import ( + "testing" + + "github.com/crewjam/saml/samlsp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker" +) + +func TestProvider_Options(t *testing.T) { + type fields struct { + name string + rootURL string + metadata []byte + key []byte + certificate []byte + options []ProviderOpts + } + type want struct { + err bool + name string + linkingAllowed bool + creationAllowed bool + autoCreation bool + autoUpdate bool + binding string + withSignedRequest bool + requesttracker samlsp.RequestTracker + entityID string + } + tests := []struct { + name string + fields fields + want want + }{{ + name: "failed metadata", + fields: fields{ + name: "saml", + rootURL: "https://localhost:8080", + metadata: []byte(">xml<"), + options: nil, + }, + want: want{ + err: true, + }, + }, + { + name: "failed keypair cert", + fields: fields{ + name: "saml", + rootURL: "https://localhost:8080", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: nil, + }, + want: want{ + err: true, + }, + }, + { + name: "failed keypair key", + fields: fields{ + name: "saml", + rootURL: "https://localhost:8080", + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: nil, + }, + want: want{ + err: true, + }, + }, + { + name: "failed url", + fields: fields{ + name: "saml", + rootURL: "%%", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: nil, + }, + want: want{ + err: true, + }, + }, + { + name: "default", + fields: fields{ + name: "saml", + rootURL: "https://localhost:8080", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: nil, + }, + want: want{ + name: "saml", + linkingAllowed: false, + creationAllowed: false, + autoCreation: false, + autoUpdate: false, + }, + }, + { + name: "all true", + fields: fields{ + name: "saml", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: []ProviderOpts{ + WithLinkingAllowed(), + WithCreationAllowed(), + WithAutoCreation(), + WithAutoUpdate(), + WithBinding("binding"), + WithSignedRequest(), + WithCustomRequestTracker(&requesttracker.RequestTracker{}), + WithEntityID("entityID"), + }, + }, + want: want{ + name: "saml", + linkingAllowed: true, + creationAllowed: true, + autoCreation: true, + autoUpdate: true, + binding: "binding", + withSignedRequest: true, + requesttracker: &requesttracker.RequestTracker{}, + entityID: "entityID", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, tt.fields.options...) + if tt.want.err { + require.Error(t, err) + } else { + require.NoError(t, err) + + a.Equal(tt.want.name, provider.Name()) + a.Equal(tt.want.linkingAllowed, provider.IsLinkingAllowed()) + a.Equal(tt.want.creationAllowed, provider.IsCreationAllowed()) + a.Equal(tt.want.autoCreation, provider.IsAutoCreation()) + a.Equal(tt.want.autoUpdate, provider.IsAutoUpdate()) + a.Equal(tt.want.binding, provider.binding) + a.Equal(tt.want.withSignedRequest, provider.spOptions.SignRequest) + a.Equal(tt.want.requesttracker, provider.requestTracker) + a.Equal(tt.want.entityID, provider.spOptions.EntityID) + } + }) + } +} diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go new file mode 100644 index 0000000000..c795493adc --- /dev/null +++ b/internal/idp/providers/saml/session.go @@ -0,0 +1,93 @@ +package saml + +import ( + "bytes" + "context" + "net/http" + "net/url" + + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/idp" +) + +var _ idp.Session = (*Session)(nil) + +// Session is the [idp.Session] implementation for the SAML provider. +type Session struct { + ServiceProvider *samlsp.Middleware + state string + + RequestID string + Request *http.Request + + Assertion *saml.Assertion +} + +// GetAuth implements the [idp.Session] interface. +func (s *Session) GetAuth(ctx context.Context) (string, bool) { + url, _ := url.Parse(s.state) + resp := NewTempResponseWriter() + + request := &http.Request{ + URL: url, + } + s.ServiceProvider.HandleStartAuthFlow( + resp, + request.WithContext(ctx), + ) + + if location := resp.Header().Get("Location"); location != "" { + return idp.Redirect(location) + } + return idp.Form(resp.content.String()) +} + +// FetchUser implements the [idp.Session] interface. +func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { + if s.RequestID == "" || s.Request == nil { + return nil, errors.ThrowInvalidArgument(nil, "SAML-d09hy0wkex", "Errors.Intent.ResponseInvalid") + } + + s.Assertion, err = s.ServiceProvider.ServiceProvider.ParseResponse(s.Request, []string{s.RequestID}) + if err != nil { + return nil, errors.ThrowInvalidArgument(err, "SAML-nuo0vphhh9", "Errors.Intent.ResponseInvalid") + } + + userMapper := NewUser() + userMapper.SetID(s.Assertion.Subject.NameID) + for _, statement := range s.Assertion.AttributeStatements { + for _, attribute := range statement.Attributes { + values := make([]string, len(attribute.Values)) + for i := range attribute.Values { + values[i] = attribute.Values[i].Value + } + userMapper.Attributes[attribute.Name] = values + } + } + return userMapper, nil +} + +type TempResponseWriter struct { + header http.Header + content *bytes.Buffer +} + +func (w *TempResponseWriter) Header() http.Header { + return w.header +} + +func (w *TempResponseWriter) Write(content []byte) (int, error) { + return w.content.Write(content) +} + +func (w *TempResponseWriter) WriteHeader(statusCode int) {} + +func NewTempResponseWriter() *TempResponseWriter { + return &TempResponseWriter{ + header: map[string][]string{}, + content: bytes.NewBuffer([]byte{}), + } +} diff --git a/internal/idp/providers/saml/session_test.go b/internal/idp/providers/saml/session_test.go new file mode 100644 index 0000000000..34126ff7ff --- /dev/null +++ b/internal/idp/providers/saml/session_test.go @@ -0,0 +1,214 @@ +package saml + +import ( + "context" + "errors" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/crewjam/saml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker" +) + +func TestSession_FetchUser(t *testing.T) { + type fields struct { + name string + rootURL string + metadata []byte + key []byte + certificate []byte + options []ProviderOpts + } + type args struct { + intentID string + requestID string + request *http.Request + } + type want struct { + err error + id string + attributes map[string][]string + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "requestID empty", + fields: fields{ + name: "saml", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: []ProviderOpts{ + WithLinkingAllowed(), + WithCreationAllowed(), + WithAutoCreation(), + WithAutoUpdate(), + WithBinding(saml.HTTPRedirectBinding), + WithSignedRequest(), + WithCustomRequestTracker(&requesttracker.RequestTracker{}), + }, + rootURL: "http://localhost:8080/idps/228968792372281708/", + }, + args: args{ + request: httpPostFormRequest(t, + "http://localhost:8080/idps/228968792372281708/saml/acs", + "232881438356144492", + "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9ImlkLTA4ZTA3MTFhYzYwZjE2Mzc2MTdhYjZhNDZkZDk0ZTZkMWQ3MDgzMWQiIEluUmVzcG9uc2VUbz0iaWQtYjIyYzkwZGI4OGJmMDFkODJmZmIwYTdiNmZlMjVhYzlmY2IyYzY3OSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMjMtMDktMjFUMTM6NDk6MjMuOTM4WiIgRGVzdGluYXRpb249Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9pZHBzLzIyODk2ODc5MjM3MjI4MTcwOC9zYW1sL2FjcyI+PHNhbWw6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwOi8vbG9jYWxob3N0OjgwMDAvbWV0YWRhdGE8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPjxkczpSZWZlcmVuY2UgVVJJPSIjaWQtMDhlMDcxMWFjNjBmMTYzNzYxN2FiNmE0NmRkOTRlNmQxZDcwODMxZCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+OWF2ektOOWhpazE4ZkFRdnZNZzJBZFoyYm9VPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5VMkFJOFNzMng5TDAwV3RkaVFlbldCSVFCcGJLNnU4ZU9XVGE2WExKR0lWMWg4d3VOdURqM2luVGpMVUhITkNUSHhTdTFXSE9OSW1CL3QwWlE1Z3EvYXlkcVVqaEVkNG50LysyaXBKelZjZHVHUG5nYjlMWjJ5R1Rsd1JiQkcyMzd4eCtlRWhUMUcrTUFGa3BtbnUrei9RN09vUjdQWHVZOWt6NTRCb0tVM1htK1UyWm9GVy9pVjhId01kYTJMajVLT0pjcnppSWVtNHF0dHlIZXBqcjI3NUhPM2hybzgvVW0xMm8wdk10OUhwaHJua0RNVzgzM3Q5c0k2aW5GRndiOUJkdm5ORkVxYkhCZ2RsemR5T0NqaWdreVNlTzZQNzhQQlhUTWlhM3RVaGxEL0dlZ2hmbTJ4NVI1Q2QrOXJ5RktjYlBKLzlUaFhwbTlIYUJ4R1RZNEE9PTwvZHM6U2lnbmF0dXJlVmFsdWU+PGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJREJ6Q0NBZStnQXdJQkFnSUpBUHIvTXJsYzhFR2hNQTBHQ1NxR1NJYjNEUUVCQlFVQU1Cb3hHREFXQmdOVkJBTU1EM2QzZHk1bGVHRnRjR3hsTG1OdmJUQWVGdzB4TlRFeU1qZ3hPVEU1TkRWYUZ3MHlOVEV5TWpVeE9URTVORFZhTUJveEdEQVdCZ05WQkFNTUQzZDNkeTVsZUdGdGNHeGxMbU52YlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTkRvV3pMb3M0TFd4VG44R3l1MmxFYmw0V2NlbFViZ0xONXpZbTRyb244QWhzK3J2Y3N1MnprZEQvczZqZEdKSThXcUpLaFlLMnU2MXlnblhnQVpxQzZnZ3RGUG5CcGl6Y0R6amdORDJnK2F1Y1NvVU9ESHQ2N2YwZlF1QW11cE4venA1TVp5c0o2SUhMSm5ZTE5wZkpZazk2bFJ6OU9Ebk8xTXBxdHI5UFd4bStwejduenE1RjB2UmVwa2dwY1J4djZ1ZlFCamxyRnl0Y2N5RVZkWHJ2RnRralhjbmhWVk5TUjRrSHVPT01TNkQ3cGViU0oxbXJDbXNoYkQ1U1gxalhQQktGUEFqb3pZWDZQeHFMeFV4MVk0ZmFGRWY0TUJCVmNJbnlCNG9VUk5CMnM1OWhFRWkyanE5aXpORTdFYkVLNkJZNXNFaG9DUGw5bTMyekU2bGprQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZCOVprbEMxT3JrMnpsNTZ6ZzA4ZWk3c3MvK2lNQjhHQTFVZEl3UVlNQmFBRkI5WmtsQzFPcmsyemw1NnpnMDhlaTdzcy8raU1Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFRkJRQURnZ0VCQUFWb1RTUTVwQWlydzhPUjlGWjFiUlN1VERoWTl1eHpsL09MN2xVbXN2MmNNTmVDQjNCUlpxbTNtRnQrY3dOOEdzSDZmM3V2Tk9OSWhnRnBUR041TEVjWFF6ODl6SkV6QitxYUhxbWJGcEhRbC9zeDJCOGV6TmdULzg4MkgySUgwMGRYRVNFZnkvKzFnSGcycHhqR25oUkJONmVsL2dTYURpeVNJTUtiaWxEcmZmdXZ4aUNmYnBQTjBOUlJpUEpoZDJheTlLdUwvUnhRUmwxZ2w5Y0hhV2lvdVdXYmExYlNCYjJaUGh2MnJQTVVzRm85OG50a0dDT2JEWDZZMVNwa3Ftb1RicnNiR0ZzVEcyREx4bnZyNEdkTjFCU3IwVXUvS1YzYWRqNDdXa1hWUGVNWVF0aS9iUW14UUI4dFJGaHJ3ODBxYWtUTFV6cmVPOTZXemxCQk10WT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9ImlkLTQ1ZTcyMTkyNjU4NmNkZGUzNDQ2NmI0MzQ5NDg4YjA5MDZhNTU3OGIiIElzc3VlSW5zdGFudD0iMjAyMy0wOS0yMVQxMzo0OToyMy45NDFaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI+aHR0cDovL2xvY2FsaG9zdDo4MDAwL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz48ZHM6UmVmZXJlbmNlIFVSST0iI2lkLTQ1ZTcyMTkyNjU4NmNkZGUzNDQ2NmI0MzQ5NDg4YjA5MDZhNTU3OGIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmVHeTMzcWpjZjYrT3R4YlVpVm5DUjFNdnlaaz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cDBEekU3Q0lTVTVNQjNvclpReGtUWHFsQXJRMjlFb0tXOUZuVWx5aUhYWGtmK2lXNGZ5SW9wYjcrTUk1VUk5TjVTQVdiVzV4U0R3NFJMMWNDaVNxdWJyU29pRi83MXdiM29naWlQOHhyYmJiajY3RHZMcUU3OVJDQXdySDlEU0FlZFlReVBOQ0tFQ0U0L3NvdnFEeUg5OTAwZEI4aTQ3VHdQRkhIclBlWE8wUGVoR0NWNTVEeTZJdC92Ull1VFRqU0tiVTczV014c1A5Mk9yVEdqamN1Smx1bFdSclBxNDk0aEpNMFJxV3gwN2RoWHJEbzlJY1JpK2RCOVhRN3UwbGJicEthQ0p6R2RiUG9JanJ1ZDhaWnh2djdiaDFtV1lEeGFodExvd2I1eGtDWlQrNFMybklrSWgwNEoxUURsZm1WSVdlRWljVW5odXpIcVBkU1RldklnPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURCekNDQWUrZ0F3SUJBZ0lKQVByL01ybGM4RUdoTUEwR0NTcUdTSWIzRFFFQkJRVUFNQm94R0RBV0JnTlZCQU1NRDNkM2R5NWxlR0Z0Y0d4bExtTnZiVEFlRncweE5URXlNamd4T1RFNU5EVmFGdzB5TlRFeU1qVXhPVEU1TkRWYU1Cb3hHREFXQmdOVkJBTU1EM2QzZHk1bGVHRnRjR3hsTG1OdmJUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5Eb1d6TG9zNExXeFRuOEd5dTJsRWJsNFdjZWxVYmdMTjV6WW00cm9uOEFocytydmNzdTJ6a2REL3M2amRHSkk4V3FKS2hZSzJ1NjF5Z25YZ0FacUM2Z2d0RlBuQnBpemNEempnTkQyZythdWNTb1VPREh0NjdmMGZRdUFtdXBOL3pwNU1aeXNKNklITEpuWUxOcGZKWWs5NmxSejlPRG5PMU1wcXRyOVBXeG0rcHo3bnpxNUYwdlJlcGtncGNSeHY2dWZRQmpsckZ5dGNjeUVWZFhydkZ0a2pYY25oVlZOU1I0a0h1T09NUzZEN3BlYlNKMW1yQ21zaGJENVNYMWpYUEJLRlBBam96WVg2UHhxTHhVeDFZNGZhRkVmNE1CQlZjSW55QjRvVVJOQjJzNTloRUVpMmpxOWl6TkU3RWJFSzZCWTVzRWhvQ1BsOW0zMnpFNmxqa0NBd0VBQWFOUU1FNHdIUVlEVlIwT0JCWUVGQjlaa2xDMU9yazJ6bDU2emcwOGVpN3NzLytpTUI4R0ExVWRJd1FZTUJhQUZCOVprbEMxT3JrMnpsNTZ6ZzA4ZWk3c3MvK2lNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ2dFQkFBVm9UU1E1cEFpcnc4T1I5RloxYlJTdVREaFk5dXh6bC9PTDdsVW1zdjJjTU5lQ0IzQlJacW0zbUZ0K2N3TjhHc0g2ZjN1dk5PTkloZ0ZwVEdONUxFY1hRejg5ekpFekIrcWFIcW1iRnBIUWwvc3gyQjhlek5nVC84ODJIMklIMDBkWEVTRWZ5LysxZ0hnMnB4akduaFJCTjZlbC9nU2FEaXlTSU1LYmlsRHJmZnV2eGlDZmJwUE4wTlJSaVBKaGQyYXk5S3VML1J4UVJsMWdsOWNIYVdpb3VXV2JhMWJTQmIyWlBodjJyUE1Vc0ZvOThudGtHQ09iRFg2WTFTcGtxbW9UYnJzYkdGc1RHMkRMeG52cjRHZE4xQlNyMFV1L0tWM2FkajQ3V2tYVlBlTVlRdGkvYlFteFFCOHRSRmhydzgwcWFrVExVenJlTzk2V3psQkJNdFk9PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiIE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMC9tZXRhZGF0YSIgU1BOYW1lUXVhbGlmaWVyPSJodHRwOi8vbG9jYWxob3N0OjgwODAvaWRwcy8yMjg5Njg3OTIzNzIyODE3MDgvc2FtbC9tZXRhZGF0YSI+YWxpY2VAZXhhbXBsZS5jb208L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBBZGRyZXNzPSJbOjoxXTo1OTMzNCIgSW5SZXNwb25zZVRvPSJpZC1iMjJjOTBkYjg4YmYwMWQ4MmZmYjBhN2I2ZmUyNWFjOWZjYjJjNjc5IiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMjFUMTM6NTA6NTMuOTM4WiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjgwODAvaWRwcy8yMjg5Njg3OTIzNzIyODE3MDgvc2FtbC9hY3MiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAyMy0wOS0yMVQxMzo0OToxNC4yOThaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMjFUMTM6NTA6NDQuMjk4WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwOi8vbG9jYWxob3N0OjgwODAvaWRwcy8yMjg5Njg3OTIzNzIyODE3MDgvc2FtbC9tZXRhZGF0YTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMjMtMDktMjFUMTM6NDc6MzUuMTAzWiIgU2Vzc2lvbkluZGV4PSI0YzM5YjE5NTQyYzdjZTFjMzllOWMwNWJlMTdhNzJhNmQ4OGU1NWE3ZGFiYWRhZWQ3ODYxMDBiOWUzODBmYTA4Ij48c2FtbDpTdWJqZWN0TG9jYWxpdHkgQWRkcmVzcz0iWzo6MV06NTkzMzQiLz48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9InVpZCIgTmFtZT0idXJuOm9pZDowLjkuMjM0Mi4xOTIwMDMwMC4xMDAuMS4xIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+YWxpY2U8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJlZHVQZXJzb25QcmluY2lwYWxOYW1lIiBOYW1lPSJ1cm46b2lkOjEuMy42LjEuNC4xLjU5MjMuMS4xLjEuNiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFsaWNlQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0ic24iIE5hbWU9InVybjpvaWQ6Mi41LjQuNCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNtaXRoPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ2l2ZW5OYW1lIiBOYW1lPSJ1cm46b2lkOjIuNS40LjQyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+QWxpY2U8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJjbiIgTmFtZT0idXJuOm9pZDoyLjUuNC4zIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+QWxpY2UgU21pdGg8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZT0idXJuOm9pZDoxLjMuNi4xLjQuMS41OTIzLjEuMS4xLjEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5BZG1pbmlzdHJhdG9yczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5Vc2Vyczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==", + ), + requestID: "", + }, + want: want{ + err: caos_errs.ThrowInvalidArgument(nil, "SAML-d09hy0wkex", "Errors.Intent.ResponseInvalid"), + }, + }, + { + name: "request empty", + fields: fields{ + name: "saml", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: []ProviderOpts{ + WithLinkingAllowed(), + WithCreationAllowed(), + WithAutoCreation(), + WithAutoUpdate(), + WithBinding(saml.HTTPRedirectBinding), + WithSignedRequest(), + WithCustomRequestTracker(&requesttracker.RequestTracker{}), + }, + rootURL: "http://localhost:8080/idps/228968792372281708/", + }, + args: args{ + request: nil, + requestID: "id-b22c90db88bf01d82ffb0a7b6fe25ac9fcb2c679", + }, + want: want{ + err: caos_errs.ThrowInvalidArgument(nil, "SAML-d09hy0wkex", "Errors.Intent.ResponseInvalid"), + }, + }, + { + name: "response invalid", + fields: fields{ + name: "saml", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: []ProviderOpts{ + WithLinkingAllowed(), + WithCreationAllowed(), + WithAutoCreation(), + WithAutoUpdate(), + WithBinding(saml.HTTPRedirectBinding), + WithSignedRequest(), + WithCustomRequestTracker(&requesttracker.RequestTracker{}), + }, + rootURL: "http://localhost:8080/idps/228968792372281708/", + }, + args: args{ + request: httpPostFormRequest(t, + "http://localhost:8080/idps/228968792372281708/saml/acs", + "232881438356144492", + "no base64", + ), + requestID: "id-b22c90db88bf01d82ffb0a7b6fe25ac9fcb2c679", + }, + want: want{ + err: caos_errs.ThrowInvalidArgument(nil, "SAML-nuo0vphhh9", "Errors.Intent.ResponseInvalid"), + }, + }, + { + name: "post with user param", + fields: fields{ + name: "saml", + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + options: []ProviderOpts{ + WithLinkingAllowed(), + WithCreationAllowed(), + WithAutoCreation(), + WithAutoUpdate(), + WithBinding(saml.HTTPRedirectBinding), + WithSignedRequest(), + WithCustomRequestTracker(&requesttracker.RequestTracker{}), + }, + rootURL: "http://localhost:8080/idps/228968792372281708/", + }, + args: args{ + request: httpPostFormRequest(t, + "http://localhost:8080/idps/228968792372281708/saml/acs", + "232881438356144492", + "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9ImlkLTA4ZTA3MTFhYzYwZjE2Mzc2MTdhYjZhNDZkZDk0ZTZkMWQ3MDgzMWQiIEluUmVzcG9uc2VUbz0iaWQtYjIyYzkwZGI4OGJmMDFkODJmZmIwYTdiNmZlMjVhYzlmY2IyYzY3OSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMjMtMDktMjFUMTM6NDk6MjMuOTM4WiIgRGVzdGluYXRpb249Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9pZHBzLzIyODk2ODc5MjM3MjI4MTcwOC9zYW1sL2FjcyI+PHNhbWw6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwOi8vbG9jYWxob3N0OjgwMDAvbWV0YWRhdGE8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPjxkczpSZWZlcmVuY2UgVVJJPSIjaWQtMDhlMDcxMWFjNjBmMTYzNzYxN2FiNmE0NmRkOTRlNmQxZDcwODMxZCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+OWF2ektOOWhpazE4ZkFRdnZNZzJBZFoyYm9VPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5VMkFJOFNzMng5TDAwV3RkaVFlbldCSVFCcGJLNnU4ZU9XVGE2WExKR0lWMWg4d3VOdURqM2luVGpMVUhITkNUSHhTdTFXSE9OSW1CL3QwWlE1Z3EvYXlkcVVqaEVkNG50LysyaXBKelZjZHVHUG5nYjlMWjJ5R1Rsd1JiQkcyMzd4eCtlRWhUMUcrTUFGa3BtbnUrei9RN09vUjdQWHVZOWt6NTRCb0tVM1htK1UyWm9GVy9pVjhId01kYTJMajVLT0pjcnppSWVtNHF0dHlIZXBqcjI3NUhPM2hybzgvVW0xMm8wdk10OUhwaHJua0RNVzgzM3Q5c0k2aW5GRndiOUJkdm5ORkVxYkhCZ2RsemR5T0NqaWdreVNlTzZQNzhQQlhUTWlhM3RVaGxEL0dlZ2hmbTJ4NVI1Q2QrOXJ5RktjYlBKLzlUaFhwbTlIYUJ4R1RZNEE9PTwvZHM6U2lnbmF0dXJlVmFsdWU+PGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJREJ6Q0NBZStnQXdJQkFnSUpBUHIvTXJsYzhFR2hNQTBHQ1NxR1NJYjNEUUVCQlFVQU1Cb3hHREFXQmdOVkJBTU1EM2QzZHk1bGVHRnRjR3hsTG1OdmJUQWVGdzB4TlRFeU1qZ3hPVEU1TkRWYUZ3MHlOVEV5TWpVeE9URTVORFZhTUJveEdEQVdCZ05WQkFNTUQzZDNkeTVsZUdGdGNHeGxMbU52YlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTkRvV3pMb3M0TFd4VG44R3l1MmxFYmw0V2NlbFViZ0xONXpZbTRyb244QWhzK3J2Y3N1MnprZEQvczZqZEdKSThXcUpLaFlLMnU2MXlnblhnQVpxQzZnZ3RGUG5CcGl6Y0R6amdORDJnK2F1Y1NvVU9ESHQ2N2YwZlF1QW11cE4venA1TVp5c0o2SUhMSm5ZTE5wZkpZazk2bFJ6OU9Ebk8xTXBxdHI5UFd4bStwejduenE1RjB2UmVwa2dwY1J4djZ1ZlFCamxyRnl0Y2N5RVZkWHJ2RnRralhjbmhWVk5TUjRrSHVPT01TNkQ3cGViU0oxbXJDbXNoYkQ1U1gxalhQQktGUEFqb3pZWDZQeHFMeFV4MVk0ZmFGRWY0TUJCVmNJbnlCNG9VUk5CMnM1OWhFRWkyanE5aXpORTdFYkVLNkJZNXNFaG9DUGw5bTMyekU2bGprQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZCOVprbEMxT3JrMnpsNTZ6ZzA4ZWk3c3MvK2lNQjhHQTFVZEl3UVlNQmFBRkI5WmtsQzFPcmsyemw1NnpnMDhlaTdzcy8raU1Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFRkJRQURnZ0VCQUFWb1RTUTVwQWlydzhPUjlGWjFiUlN1VERoWTl1eHpsL09MN2xVbXN2MmNNTmVDQjNCUlpxbTNtRnQrY3dOOEdzSDZmM3V2Tk9OSWhnRnBUR041TEVjWFF6ODl6SkV6QitxYUhxbWJGcEhRbC9zeDJCOGV6TmdULzg4MkgySUgwMGRYRVNFZnkvKzFnSGcycHhqR25oUkJONmVsL2dTYURpeVNJTUtiaWxEcmZmdXZ4aUNmYnBQTjBOUlJpUEpoZDJheTlLdUwvUnhRUmwxZ2w5Y0hhV2lvdVdXYmExYlNCYjJaUGh2MnJQTVVzRm85OG50a0dDT2JEWDZZMVNwa3Ftb1RicnNiR0ZzVEcyREx4bnZyNEdkTjFCU3IwVXUvS1YzYWRqNDdXa1hWUGVNWVF0aS9iUW14UUI4dFJGaHJ3ODBxYWtUTFV6cmVPOTZXemxCQk10WT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9ImlkLTQ1ZTcyMTkyNjU4NmNkZGUzNDQ2NmI0MzQ5NDg4YjA5MDZhNTU3OGIiIElzc3VlSW5zdGFudD0iMjAyMy0wOS0yMVQxMzo0OToyMy45NDFaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI+aHR0cDovL2xvY2FsaG9zdDo4MDAwL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz48ZHM6UmVmZXJlbmNlIFVSST0iI2lkLTQ1ZTcyMTkyNjU4NmNkZGUzNDQ2NmI0MzQ5NDg4YjA5MDZhNTU3OGIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmVHeTMzcWpjZjYrT3R4YlVpVm5DUjFNdnlaaz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cDBEekU3Q0lTVTVNQjNvclpReGtUWHFsQXJRMjlFb0tXOUZuVWx5aUhYWGtmK2lXNGZ5SW9wYjcrTUk1VUk5TjVTQVdiVzV4U0R3NFJMMWNDaVNxdWJyU29pRi83MXdiM29naWlQOHhyYmJiajY3RHZMcUU3OVJDQXdySDlEU0FlZFlReVBOQ0tFQ0U0L3NvdnFEeUg5OTAwZEI4aTQ3VHdQRkhIclBlWE8wUGVoR0NWNTVEeTZJdC92Ull1VFRqU0tiVTczV014c1A5Mk9yVEdqamN1Smx1bFdSclBxNDk0aEpNMFJxV3gwN2RoWHJEbzlJY1JpK2RCOVhRN3UwbGJicEthQ0p6R2RiUG9JanJ1ZDhaWnh2djdiaDFtV1lEeGFodExvd2I1eGtDWlQrNFMybklrSWgwNEoxUURsZm1WSVdlRWljVW5odXpIcVBkU1RldklnPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURCekNDQWUrZ0F3SUJBZ0lKQVByL01ybGM4RUdoTUEwR0NTcUdTSWIzRFFFQkJRVUFNQm94R0RBV0JnTlZCQU1NRDNkM2R5NWxlR0Z0Y0d4bExtTnZiVEFlRncweE5URXlNamd4T1RFNU5EVmFGdzB5TlRFeU1qVXhPVEU1TkRWYU1Cb3hHREFXQmdOVkJBTU1EM2QzZHk1bGVHRnRjR3hsTG1OdmJUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5Eb1d6TG9zNExXeFRuOEd5dTJsRWJsNFdjZWxVYmdMTjV6WW00cm9uOEFocytydmNzdTJ6a2REL3M2amRHSkk4V3FKS2hZSzJ1NjF5Z25YZ0FacUM2Z2d0RlBuQnBpemNEempnTkQyZythdWNTb1VPREh0NjdmMGZRdUFtdXBOL3pwNU1aeXNKNklITEpuWUxOcGZKWWs5NmxSejlPRG5PMU1wcXRyOVBXeG0rcHo3bnpxNUYwdlJlcGtncGNSeHY2dWZRQmpsckZ5dGNjeUVWZFhydkZ0a2pYY25oVlZOU1I0a0h1T09NUzZEN3BlYlNKMW1yQ21zaGJENVNYMWpYUEJLRlBBam96WVg2UHhxTHhVeDFZNGZhRkVmNE1CQlZjSW55QjRvVVJOQjJzNTloRUVpMmpxOWl6TkU3RWJFSzZCWTVzRWhvQ1BsOW0zMnpFNmxqa0NBd0VBQWFOUU1FNHdIUVlEVlIwT0JCWUVGQjlaa2xDMU9yazJ6bDU2emcwOGVpN3NzLytpTUI4R0ExVWRJd1FZTUJhQUZCOVprbEMxT3JrMnpsNTZ6ZzA4ZWk3c3MvK2lNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ2dFQkFBVm9UU1E1cEFpcnc4T1I5RloxYlJTdVREaFk5dXh6bC9PTDdsVW1zdjJjTU5lQ0IzQlJacW0zbUZ0K2N3TjhHc0g2ZjN1dk5PTkloZ0ZwVEdONUxFY1hRejg5ekpFekIrcWFIcW1iRnBIUWwvc3gyQjhlek5nVC84ODJIMklIMDBkWEVTRWZ5LysxZ0hnMnB4akduaFJCTjZlbC9nU2FEaXlTSU1LYmlsRHJmZnV2eGlDZmJwUE4wTlJSaVBKaGQyYXk5S3VML1J4UVJsMWdsOWNIYVdpb3VXV2JhMWJTQmIyWlBodjJyUE1Vc0ZvOThudGtHQ09iRFg2WTFTcGtxbW9UYnJzYkdGc1RHMkRMeG52cjRHZE4xQlNyMFV1L0tWM2FkajQ3V2tYVlBlTVlRdGkvYlFteFFCOHRSRmhydzgwcWFrVExVenJlTzk2V3psQkJNdFk9PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiIE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMC9tZXRhZGF0YSIgU1BOYW1lUXVhbGlmaWVyPSJodHRwOi8vbG9jYWxob3N0OjgwODAvaWRwcy8yMjg5Njg3OTIzNzIyODE3MDgvc2FtbC9tZXRhZGF0YSI+YWxpY2VAZXhhbXBsZS5jb208L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBBZGRyZXNzPSJbOjoxXTo1OTMzNCIgSW5SZXNwb25zZVRvPSJpZC1iMjJjOTBkYjg4YmYwMWQ4MmZmYjBhN2I2ZmUyNWFjOWZjYjJjNjc5IiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMjFUMTM6NTA6NTMuOTM4WiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjgwODAvaWRwcy8yMjg5Njg3OTIzNzIyODE3MDgvc2FtbC9hY3MiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAyMy0wOS0yMVQxMzo0OToxNC4yOThaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDktMjFUMTM6NTA6NDQuMjk4WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwOi8vbG9jYWxob3N0OjgwODAvaWRwcy8yMjg5Njg3OTIzNzIyODE3MDgvc2FtbC9tZXRhZGF0YTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMjMtMDktMjFUMTM6NDc6MzUuMTAzWiIgU2Vzc2lvbkluZGV4PSI0YzM5YjE5NTQyYzdjZTFjMzllOWMwNWJlMTdhNzJhNmQ4OGU1NWE3ZGFiYWRhZWQ3ODYxMDBiOWUzODBmYTA4Ij48c2FtbDpTdWJqZWN0TG9jYWxpdHkgQWRkcmVzcz0iWzo6MV06NTkzMzQiLz48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9InVpZCIgTmFtZT0idXJuOm9pZDowLjkuMjM0Mi4xOTIwMDMwMC4xMDAuMS4xIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+YWxpY2U8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJlZHVQZXJzb25QcmluY2lwYWxOYW1lIiBOYW1lPSJ1cm46b2lkOjEuMy42LjEuNC4xLjU5MjMuMS4xLjEuNiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFsaWNlQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0ic24iIE5hbWU9InVybjpvaWQ6Mi41LjQuNCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNtaXRoPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ2l2ZW5OYW1lIiBOYW1lPSJ1cm46b2lkOjIuNS40LjQyIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+QWxpY2U8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJjbiIgTmFtZT0idXJuOm9pZDoyLjUuNC4zIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+QWxpY2UgU21pdGg8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZT0idXJuOm9pZDoxLjMuNi4xLjQuMS41OTIzLjEuMS4xLjEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5BZG1pbmlzdHJhdG9yczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5Vc2Vyczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==", + ), + requestID: "id-b22c90db88bf01d82ffb0a7b6fe25ac9fcb2c679", + }, + want: want{ + id: "alice@example.com", + attributes: map[string][]string{ + "urn:oid:0.9.2342.19200300.100.1.1": {"alice"}, + "urn:oid:1.3.6.1.4.1.5923.1.1.1.6": {"alice@example.com"}, + "urn:oid:2.5.4.4": {"Smith"}, + "urn:oid:2.5.4.42": {"Alice"}, + "urn:oid:2.5.4.3": {"Alice Smith"}, + "urn:oid:1.3.6.1.4.1.5923.1.1.1.1": {"Administrators", "Users"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, tt.fields.options...) + require.NoError(t, err) + + sp, err := provider.GetSP() + require.NoError(t, err) + + session := &Session{ + ServiceProvider: sp, + state: tt.args.intentID, + RequestID: tt.args.requestID, + Request: tt.args.request, + } + // set to time of response for validation + saml.TimeNow = func() time.Time { + time, _ := time.Parse(time.RFC3339, "2023-09-21T13:47:40.0Z") + return time + } + user, err := session.FetchUser(context.Background()) + if tt.want.err != nil && !errors.Is(err, tt.want.err) { + a.Fail("invalid error", "expected %v, got %v", tt.want.err, err) + } + if tt.want.err == nil { + a.NoError(err) + a.Equal(tt.want.id, user.GetID()) + } + }) + } +} + +func httpPostFormRequest(t *testing.T, callbackURL, relayState, response string) *http.Request { + body := url.Values{ + "SAMLResponse": {response}, + "RelayState": {relayState}, + } + + req, err := http.NewRequest("POST", callbackURL, strings.NewReader(body.Encode())) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + assert.NoError(t, req.ParseForm()) + return req +} diff --git a/internal/idp/session.go b/internal/idp/session.go index 75f2e7a1e6..6d6519a54c 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -6,7 +6,7 @@ import ( // Session is the minimal implementation for a session of a 3rd party authentication [Provider] type Session interface { - GetAuthURL() string + GetAuth(ctx context.Context) (content string, redirect bool) FetchUser(ctx context.Context) (User, error) } @@ -18,3 +18,11 @@ type Session interface { type SessionSupportsMigration interface { RetrievePreviousID() (previousID string, err error) } + +func Redirect(redirectURL string) (string, bool) { + return redirectURL, true +} + +func Form(html string) (string, bool) { + return html, false +} diff --git a/internal/integration/client.go b/internal/integration/client.go index 4e82ea5442..71bc9805c4 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -6,6 +6,7 @@ import ( "testing" "time" + crewjam_saml "github.com/crewjam/saml" "github.com/stretchr/testify/require" "github.com/zitadel/logging" "github.com/zitadel/oidc/v2/pkg/oidc" @@ -17,6 +18,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/idp/providers/ldap" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" @@ -196,6 +198,54 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T) string { return id } +func (s *Tester) AddSAMLProvider(t *testing.T) string { + ctx := authz.WithInstance(context.Background(), s.Instance) + id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ + Name: "saml-idp", + Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + IDPOptions: idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + require.NoError(t, err) + return id +} + +func (s *Tester) AddSAMLRedirectProvider(t *testing.T) string { + ctx := authz.WithInstance(context.Background(), s.Instance) + id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ + Name: "saml-idp-redirect", + Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), + IDPOptions: idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + require.NoError(t, err) + return id +} + +func (s *Tester) AddSAMLPostProvider(t *testing.T) string { + ctx := authz.WithInstance(context.Background(), s.Instance) + id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ + Name: "saml-idp-post", + Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), + IDPOptions: idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + require.NoError(t, err) + return id +} + func (s *Tester) CreateIntent(t *testing.T, idpID string) string { ctx := authz.WithInstance(context.Background(), s.Instance) writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Organisation.ID) @@ -257,6 +307,23 @@ func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, idpID, userID, idpUser return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence } +func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { + ctx := authz.WithInstance(context.Background(), s.Instance) + intentID := s.CreateIntent(t, idpID) + writeModel, err := s.Server.Commands.GetIntentWriteModel(ctx, intentID, s.Organisation.ID) + require.NoError(t, err) + + idpUser := &saml.UserMapper{ + ID: idpUserID, + Attributes: map[string][]string{"attribute1": {"value1"}}, + } + assertion := &crewjam_saml.Assertion{ID: "id"} + + token, err := s.Server.Commands.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, userID, assertion) + require.NoError(t, err) + return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence +} + func (s *Tester) CreateVerfiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) { createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ Checks: &session.Checks{ diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index e34850d5e8..ad1a44de32 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -93,7 +93,12 @@ func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string codeChallenge := oidc.NewSHACodeChallenge(codeVerifier) authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge)) - loc, err := CheckRedirect(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient}) + req, err := GetRequest(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient}) + if err != nil { + return "", err + } + + loc, err := CheckRedirect(req) if err != nil { return "", err } @@ -120,7 +125,12 @@ func (s *Tester) CreateOIDCAuthRequestImplicit(clientID, loginClient, redirectUR parsed.RawQuery = queries.Encode() authURL = parsed.String() - loc, err := CheckRedirect(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient}) + req, err := GetRequest(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient}) + if err != nil { + return "", err + } + + loc, err := CheckRedirect(req) if err != nil { return "", err } @@ -161,7 +171,7 @@ func (s *Tester) CreateResourceServer(keyFileData []byte) (rs.ResourceServer, er return rs.NewResourceServerJWTProfile(s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key)) } -func CheckRedirect(url string, headers map[string]string) (*url.URL, error) { +func GetRequest(url string, headers map[string]string) (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err @@ -169,7 +179,10 @@ func CheckRedirect(url string, headers map[string]string) (*url.URL, error) { for key, value := range headers { req.Header.Set(key, value) } + return req, nil +} +func CheckRedirect(req *http.Request) (*url.URL, error) { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 148fad49f3..b12824dbb8 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -44,6 +44,7 @@ type IDPTemplate struct { *GoogleIDPTemplate *LDAPIDPTemplate *AppleIDPTemplate + *SAMLIDPTemplate } type IDPTemplates struct { @@ -150,6 +151,15 @@ type AppleIDPTemplate struct { Scopes database.StringArray } +type SAMLIDPTemplate struct { + IDPID string + Metadata []byte + Key *crypto.CryptoValue + Certificate []byte + Binding string + WithSignedRequest bool +} + var ( idpTemplateTable = table{ name: projection.IDPTemplateTable, @@ -650,6 +660,41 @@ var ( } ) +var ( + samlIdpTemplateTable = table{ + name: projection.IDPTemplateSAMLTable, + instanceIDCol: projection.IDPTemplateInstanceIDCol, + } + SAMLIDCol = Column{ + name: projection.SAMLIDCol, + table: samlIdpTemplateTable, + } + SAMLInstanceCol = Column{ + name: projection.SAMLInstanceIDCol, + table: samlIdpTemplateTable, + } + SAMLMetadataCol = Column{ + name: projection.SAMLMetadataCol, + table: samlIdpTemplateTable, + } + SAMLKeyCol = Column{ + name: projection.SAMLKeyCol, + table: samlIdpTemplateTable, + } + SAMLCertificateCol = Column{ + name: projection.SAMLCertificateCol, + table: samlIdpTemplateTable, + } + SAMLBindingCol = Column{ + name: projection.SAMLBindingCol, + table: samlIdpTemplateTable, + } + SAMLWithSignedRequestCol = Column{ + name: projection.SAMLWithSignedRequestCol, + table: samlIdpTemplateTable, + } +) + // IDPTemplateByID searches for the requested id func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) { ctx, span := tracing.NewSpan(ctx) @@ -820,6 +865,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se GoogleClientIDCol.identifier(), GoogleClientSecretCol.identifier(), GoogleScopesCol.identifier(), + // saml + SAMLIDCol.identifier(), + SAMLMetadataCol.identifier(), + SAMLKeyCol.identifier(), + SAMLCertificateCol.identifier(), + SAMLBindingCol.identifier(), + SAMLWithSignedRequestCol.identifier(), // ldap LDAPIDCol.identifier(), LDAPServersCol.identifier(), @@ -861,6 +913,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabSelfHostedIDCol, IDPTemplateIDCol)). LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). + LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), @@ -927,6 +980,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se googleClientSecret := new(crypto.CryptoValue) googleScopes := database.StringArray{} + samlID := sql.NullString{} + var samlMetadata []byte + samlKey := new(crypto.CryptoValue) + var samlCertificate []byte + samlBinding := sql.NullString{} + samlWithSignedRequest := sql.NullBool{} + ldapID := sql.NullString{} ldapServers := database.StringArray{} ldapStartTls := sql.NullBool{} @@ -1030,6 +1090,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se &googleClientID, &googleClientSecret, &googleScopes, + // saml + &samlID, + &samlMetadata, + &samlKey, + &samlCertificate, + &samlBinding, + &samlWithSignedRequest, // ldap &ldapID, &ldapServers, @@ -1156,6 +1223,16 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se Scopes: googleScopes, } } + if samlID.Valid { + idpTemplate.SAMLIDPTemplate = &SAMLIDPTemplate{ + IDPID: samlID.String, + Metadata: samlMetadata, + Key: samlKey, + Certificate: samlCertificate, + Binding: samlBinding.String, + WithSignedRequest: samlWithSignedRequest.Bool, + } + } if ldapID.Valid { idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{ IDPID: ldapID.String, @@ -1273,6 +1350,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec GoogleClientIDCol.identifier(), GoogleClientSecretCol.identifier(), GoogleScopesCol.identifier(), + // saml + SAMLIDCol.identifier(), + SAMLMetadataCol.identifier(), + SAMLKeyCol.identifier(), + SAMLCertificateCol.identifier(), + SAMLBindingCol.identifier(), + SAMLWithSignedRequestCol.identifier(), // ldap LDAPIDCol.identifier(), LDAPServersCol.identifier(), @@ -1316,6 +1400,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabSelfHostedIDCol, IDPTemplateIDCol)). LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). + LeftJoin(join(SAMLIDCol, IDPTemplateIDCol)). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)). LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), @@ -1385,6 +1470,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec googleClientSecret := new(crypto.CryptoValue) googleScopes := database.StringArray{} + samlID := sql.NullString{} + var samlMetadata []byte + samlKey := new(crypto.CryptoValue) + var samlCertificate []byte + samlBinding := sql.NullString{} + samlWithSignedRequest := sql.NullBool{} + ldapID := sql.NullString{} ldapServers := database.StringArray{} ldapStartTls := sql.NullBool{} @@ -1488,6 +1580,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec &googleClientID, &googleClientSecret, &googleScopes, + // saml + &samlID, + &samlMetadata, + &samlKey, + &samlCertificate, + &samlBinding, + &samlWithSignedRequest, // ldap &ldapID, &ldapServers, @@ -1613,6 +1712,16 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec Scopes: googleScopes, } } + if samlID.Valid { + idpTemplate.SAMLIDPTemplate = &SAMLIDPTemplate{ + IDPID: samlID.String, + Metadata: samlMetadata, + Key: samlKey, + Certificate: samlCertificate, + Binding: samlBinding.String, + WithSignedRequest: samlWithSignedRequest.Bool, + } + } if ldapID.Valid { idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{ IDPID: ldapID.String, diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index e5c27565d4..b6eb54aad7 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -87,6 +87,13 @@ var ( ` projections.idp_templates5_google.client_id,` + ` projections.idp_templates5_google.client_secret,` + ` projections.idp_templates5_google.scopes,` + + // saml + ` projections.idp_templates5_saml.idp_id,` + + ` projections.idp_templates5_saml.metadata,` + + ` projections.idp_templates5_saml.key,` + + ` projections.idp_templates5_saml.certificate,` + + ` projections.idp_templates5_saml.binding,` + + ` projections.idp_templates5_saml.with_signed_request,` + // ldap ` projections.idp_templates5_ldap2.idp_id,` + ` projections.idp_templates5_ldap2.servers,` + @@ -128,6 +135,7 @@ var ( ` LEFT JOIN projections.idp_templates5_gitlab ON projections.idp_templates5.id = projections.idp_templates5_gitlab.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab.instance_id` + ` LEFT JOIN projections.idp_templates5_gitlab_self_hosted ON projections.idp_templates5.id = projections.idp_templates5_gitlab_self_hosted.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab_self_hosted.instance_id` + ` LEFT JOIN projections.idp_templates5_google ON projections.idp_templates5.id = projections.idp_templates5_google.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_google.instance_id` + + ` LEFT JOIN projections.idp_templates5_saml ON projections.idp_templates5.id = projections.idp_templates5_saml.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_saml.instance_id` + ` LEFT JOIN projections.idp_templates5_ldap2 ON projections.idp_templates5.id = projections.idp_templates5_ldap2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_ldap2.instance_id` + ` LEFT JOIN projections.idp_templates5_apple ON projections.idp_templates5.id = projections.idp_templates5_apple.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_apple.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` @@ -203,6 +211,13 @@ var ( "client_id", "client_secret", "scopes", + // saml config + "idp_id", + "metadata", + "key", + "certificate", + "binding", + "with_signed_request", // ldap config "idp_id", "servers", @@ -306,6 +321,13 @@ var ( ` projections.idp_templates5_google.client_id,` + ` projections.idp_templates5_google.client_secret,` + ` projections.idp_templates5_google.scopes,` + + // saml + ` projections.idp_templates5_saml.idp_id,` + + ` projections.idp_templates5_saml.metadata,` + + ` projections.idp_templates5_saml.key,` + + ` projections.idp_templates5_saml.certificate,` + + ` projections.idp_templates5_saml.binding,` + + ` projections.idp_templates5_saml.with_signed_request,` + // ldap ` projections.idp_templates5_ldap2.idp_id,` + ` projections.idp_templates5_ldap2.servers,` + @@ -348,6 +370,7 @@ var ( ` LEFT JOIN projections.idp_templates5_gitlab ON projections.idp_templates5.id = projections.idp_templates5_gitlab.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab.instance_id` + ` LEFT JOIN projections.idp_templates5_gitlab_self_hosted ON projections.idp_templates5.id = projections.idp_templates5_gitlab_self_hosted.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab_self_hosted.instance_id` + ` LEFT JOIN projections.idp_templates5_google ON projections.idp_templates5.id = projections.idp_templates5_google.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_google.instance_id` + + ` LEFT JOIN projections.idp_templates5_saml ON projections.idp_templates5.id = projections.idp_templates5_saml.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_saml.instance_id` + ` LEFT JOIN projections.idp_templates5_ldap2 ON projections.idp_templates5.id = projections.idp_templates5_ldap2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_ldap2.instance_id` + ` LEFT JOIN projections.idp_templates5_apple ON projections.idp_templates5.id = projections.idp_templates5_apple.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_apple.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` @@ -423,6 +446,13 @@ var ( "client_id", "client_secret", "scopes", + // saml config + "idp_id", + "metadata", + "key", + "certificate", + "binding", + "with_signed_request", // ldap config "idp_id", "servers", @@ -566,6 +596,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -705,6 +742,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -842,6 +886,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -978,6 +1029,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -1113,6 +1171,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -1248,6 +1313,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -1384,6 +1456,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "client_id", nil, database.StringArray{"profile"}, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -1440,6 +1519,150 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { }, }, }, + { + name: "prepareIDPTemplateByIDQuery saml idp", + prepare: prepareIDPTemplateByIDQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(idpTemplateQuery), + idpTemplateCols, + []driver.Value{ + "idp-id", + "ro", + testNow, + testNow, + uint64(20211109), + domain.IDPConfigStateActive, + "idp-name", + domain.IDPTypeSAML, + domain.IdentityProviderTypeOrg, + true, + true, + true, + true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // oidc + nil, + nil, + nil, + nil, + nil, + nil, + // jwt + nil, + nil, + nil, + nil, + nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, + // github + nil, + nil, + nil, + nil, + // github enterprise + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // gitlab + nil, + nil, + nil, + nil, + // gitlab self hosted + nil, + nil, + nil, + nil, + nil, + // google + nil, + nil, + nil, + nil, + // saml + "idp-id", + []byte("metadata"), + nil, + nil, + "binding", + false, + // ldap config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // apple + nil, + nil, + nil, + nil, + nil, + nil, + }, + ), + }, + object: &IDPTemplate{ + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + ResourceOwner: "ro", + ID: "idp-id", + State: domain.IDPStateActive, + Name: "idp-name", + Type: domain.IDPTypeSAML, + OwnerType: domain.IdentityProviderTypeOrg, + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + SAMLIDPTemplate: &SAMLIDPTemplate{ + IDPID: "idp-id", + Metadata: []byte("metadata"), + Key: nil, + Certificate: nil, + Binding: "binding", + WithSignedRequest: false, + }, + }, + }, { name: "prepareIDPTemplateByIDQuery ldap idp", prepare: prepareIDPTemplateByIDQuery, @@ -1519,6 +1742,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config "idp-id", database.StringArray{"server"}, @@ -1674,6 +1904,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -1811,6 +2048,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -1976,6 +2220,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config "idp-id", database.StringArray{"server"}, @@ -2140,6 +2391,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -2278,6 +2536,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config "idp-id-ldap", database.StringArray{"server"}, @@ -2310,6 +2575,117 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, }, + { + "idp-id-saml", + "ro", + testNow, + testNow, + uint64(20211109), + domain.IDPConfigStateActive, + "idp-name", + domain.IDPTypeSAML, + domain.IdentityProviderTypeOrg, + true, + true, + true, + true, + // oauth + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // oidc + nil, + nil, + nil, + nil, + nil, + nil, + // jwt + nil, + nil, + nil, + nil, + nil, + // azure + nil, + nil, + nil, + nil, + nil, + nil, + // github + nil, + nil, + nil, + nil, + // github enterprise + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // gitlab + nil, + nil, + nil, + nil, + // gitlab self hosted + nil, + nil, + nil, + nil, + nil, + // google + nil, + nil, + nil, + nil, + // saml + "idp-id-saml", + []byte("metadata"), + nil, + nil, + "binding", + false, + // ldap config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // apple + nil, + nil, + nil, + nil, + nil, + nil, + }, { "idp-id-google", "ro", @@ -2382,6 +2758,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { "client_id", nil, database.StringArray{"profile"}, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -2486,6 +2869,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -2590,6 +2980,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -2694,6 +3091,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + // saml + nil, + nil, + nil, + nil, + nil, + nil, // ldap config nil, nil, @@ -2731,7 +3135,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { }, object: &IDPTemplates{ SearchResponse: SearchResponse{ - Count: 5, + Count: 6, }, Templates: []*IDPTemplate{ { @@ -2775,6 +3179,29 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { }, }, }, + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + ResourceOwner: "ro", + ID: "idp-id-saml", + State: domain.IDPStateActive, + Name: "idp-name", + Type: domain.IDPTypeSAML, + OwnerType: domain.IdentityProviderTypeOrg, + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + SAMLIDPTemplate: &SAMLIDPTemplate{ + IDPID: "idp-id-saml", + Metadata: []byte("metadata"), + Key: nil, + Certificate: nil, + Binding: "binding", + WithSignedRequest: false, + }, + }, { CreationDate: testNow, ChangeDate: testNow, diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index 9dc9ff6073..715b853c57 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -29,6 +29,7 @@ const ( IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix IDPTemplateAppleTable = IDPTemplateTable + "_" + IDPTemplateAppleSuffix + IDPTemplateSAMLTable = IDPTemplateTable + "_" + IDPTemplateSAMLSuffix IDPTemplateOAuthSuffix = "oauth2" IDPTemplateOIDCSuffix = "oidc" @@ -41,6 +42,7 @@ const ( IDPTemplateGoogleSuffix = "google" IDPTemplateLDAPSuffix = "ldap2" IDPTemplateAppleSuffix = "apple" + IDPTemplateSAMLSuffix = "saml" IDPTemplateIDCol = "id" IDPTemplateCreationDateCol = "creation_date" @@ -157,6 +159,14 @@ const ( AppleKeyIDCol = "key_id" ApplePrivateKeyCol = "private_key" AppleScopesCol = "scopes" + + SAMLIDCol = "idp_id" + SAMLInstanceIDCol = "instance_id" + SAMLMetadataCol = "metadata" + SAMLKeyCol = "key" + SAMLCertificateCol = "certificate" + SAMLBindingCol = "binding" + SAMLWithSignedRequestCol = "with_signed_request" ) type idpTemplateProjection struct { @@ -344,6 +354,19 @@ func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerC IDPTemplateAppleSuffix, crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()), ), + crdb.NewSuffixedTable([]*crdb.Column{ + crdb.NewColumn(SAMLIDCol, crdb.ColumnTypeText), + crdb.NewColumn(SAMLInstanceIDCol, crdb.ColumnTypeText), + crdb.NewColumn(SAMLMetadataCol, crdb.ColumnTypeBytes), + crdb.NewColumn(SAMLKeyCol, crdb.ColumnTypeJSONB), + crdb.NewColumn(SAMLCertificateCol, crdb.ColumnTypeBytes), + crdb.NewColumn(SAMLBindingCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(SAMLWithSignedRequestCol, crdb.ColumnTypeBool, crdb.Nullable()), + }, + crdb.NewPrimaryKey(SAMLInstanceIDCol, SAMLIDCol), + IDPTemplateSAMLSuffix, + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()), + ), ) p.StatementHandler = crdb.NewStatementHandler(ctx, config) return p @@ -474,6 +497,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { Event: instance.AppleIDPChangedEventType, Reduce: p.reduceAppleIDPChanged, }, + { + Event: instance.SAMLIDPAddedEventType, + Reduce: p.reduceSAMLIDPAdded, + }, + { + Event: instance.SAMLIDPChangedEventType, + Reduce: p.reduceSAMLIDPChanged, + }, { Event: instance.IDPConfigRemovedEventType, Reduce: p.reduceIDPConfigRemoved, @@ -611,6 +642,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { Event: org.AppleIDPChangedEventType, Reduce: p.reduceAppleIDPChanged, }, + { + Event: org.SAMLIDPAddedEventType, + Reduce: p.reduceSAMLIDPAdded, + }, + { + Event: org.SAMLIDPChangedEventType, + Reduce: p.reduceSAMLIDPChanged, + }, { Event: org.IDPConfigRemovedEventType, Reduce: p.reduceIDPConfigRemoved, @@ -1898,6 +1937,97 @@ func (p *idpTemplateProjection) reduceLDAPIDPChanged(event eventstore.Event) (*h ), nil } +func (p *idpTemplateProjection) reduceSAMLIDPAdded(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.SAMLIDPAddedEvent + var idpOwnerType domain.IdentityProviderType + switch e := event.(type) { + case *org.SAMLIDPAddedEvent: + idpEvent = e.SAMLIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeOrg + case *instance.SAMLIDPAddedEvent: + idpEvent = e.SAMLIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeSystem + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-9s02m1", "reduce.wrong.event.type %v", []eventstore.EventType{org.SAMLIDPAddedEventType, instance.SAMLIDPAddedEventType}) + } + + return crdb.NewMultiStatement( + &idpEvent, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(IDPTemplateIDCol, idpEvent.ID), + handler.NewCol(IDPTemplateCreationDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateResourceOwnerCol, idpEvent.Aggregate().ResourceOwner), + handler.NewCol(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive), + handler.NewCol(IDPTemplateNameCol, idpEvent.Name), + handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeSAML), + handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed), + handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed), + handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation), + handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate), + }, + ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(SAMLIDCol, idpEvent.ID), + handler.NewCol(SAMLInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(SAMLMetadataCol, idpEvent.Metadata), + handler.NewCol(SAMLKeyCol, idpEvent.Key), + handler.NewCol(SAMLCertificateCol, idpEvent.Certificate), + handler.NewCol(SAMLBindingCol, idpEvent.Binding), + handler.NewCol(SAMLWithSignedRequestCol, idpEvent.WithSignedRequest), + }, + crdb.WithTableSuffix(IDPTemplateSAMLSuffix), + ), + ), nil +} + +func (p *idpTemplateProjection) reduceSAMLIDPChanged(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.SAMLIDPChangedEvent + switch e := event.(type) { + case *org.SAMLIDPChangedEvent: + idpEvent = e.SAMLIDPChangedEvent + case *instance.SAMLIDPChangedEvent: + idpEvent = e.SAMLIDPChangedEvent + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-o7c0fii4ad", "reduce.wrong.event.type %v", []eventstore.EventType{org.SAMLIDPChangedEventType, instance.SAMLIDPChangedEventType}) + } + + ops := make([]func(eventstore.Event) crdb.Exec, 0, 2) + ops = append(ops, + crdb.AddUpdateStatement( + reduceIDPChangedTemplateColumns(idpEvent.Name, idpEvent.CreationDate(), idpEvent.Sequence(), idpEvent.OptionChanges), + []handler.Condition{ + handler.NewCond(IDPTemplateIDCol, idpEvent.ID), + handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + ), + ) + + SAMLCols := reduceSAMLIDPChangedColumns(idpEvent) + if len(SAMLCols) > 0 { + ops = append(ops, + crdb.AddUpdateStatement( + SAMLCols, + []handler.Condition{ + handler.NewCond(SAMLIDCol, idpEvent.ID), + handler.NewCond(SAMLInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(IDPTemplateSAMLSuffix), + ), + ) + } + + return crdb.NewMultiStatement( + &idpEvent, + ops..., + ), nil +} + func (p *idpTemplateProjection) reduceAppleIDPAdded(event eventstore.Event) (*handler.Statement, error) { var idpEvent idp.AppleIDPAddedEvent var idpOwnerType domain.IdentityProviderType @@ -2067,7 +2197,7 @@ func reduceIDPChangedTemplateColumns(name *string, creationDate time.Time, seque } func reduceOAuthIDPChangedColumns(idpEvent idp.OAuthIDPChangedEvent) []handler.Column { - oauthCols := make([]handler.Column, 0, 6) + oauthCols := make([]handler.Column, 0, 7) if idpEvent.ClientID != nil { oauthCols = append(oauthCols, handler.NewCol(OAuthClientIDCol, *idpEvent.ClientID)) } @@ -2093,7 +2223,7 @@ func reduceOAuthIDPChangedColumns(idpEvent idp.OAuthIDPChangedEvent) []handler.C } func reduceOIDCIDPChangedColumns(idpEvent idp.OIDCIDPChangedEvent) []handler.Column { - oidcCols := make([]handler.Column, 0, 4) + oidcCols := make([]handler.Column, 0, 5) if idpEvent.ClientID != nil { oidcCols = append(oidcCols, handler.NewCol(OIDCClientIDCol, *idpEvent.ClientID)) } @@ -2232,7 +2362,7 @@ func reduceGoogleIDPChangedColumns(idpEvent idp.GoogleIDPChangedEvent) []handler } func reduceLDAPIDPChangedColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column { - ldapCols := make([]handler.Column, 0, 4) + ldapCols := make([]handler.Column, 0, 22) if idpEvent.Servers != nil { ldapCols = append(ldapCols, handler.NewCol(LDAPServersCol, database.StringArray(idpEvent.Servers))) } @@ -2321,3 +2451,23 @@ func reduceAppleIDPChangedColumns(idpEvent idp.AppleIDPChangedEvent) []handler.C } return appleCols } + +func reduceSAMLIDPChangedColumns(idpEvent idp.SAMLIDPChangedEvent) []handler.Column { + SAMLCols := make([]handler.Column, 0, 5) + if idpEvent.Metadata != nil { + SAMLCols = append(SAMLCols, handler.NewCol(SAMLMetadataCol, idpEvent.Metadata)) + } + if idpEvent.Key != nil { + SAMLCols = append(SAMLCols, handler.NewCol(SAMLKeyCol, idpEvent.Key)) + } + if idpEvent.Certificate != nil { + SAMLCols = append(SAMLCols, handler.NewCol(SAMLCertificateCol, idpEvent.Certificate)) + } + if idpEvent.Binding != nil { + SAMLCols = append(SAMLCols, handler.NewCol(SAMLBindingCol, *idpEvent.Binding)) + } + if idpEvent.WithSignedRequest != nil { + SAMLCols = append(SAMLCols, handler.NewCol(SAMLWithSignedRequestCol, *idpEvent.WithSignedRequest)) + } + return SAMLCols +} diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index fd4eedf880..9e99c6cdbc 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -1,6 +1,7 @@ package projection import ( + "encoding/json" "testing" "time" @@ -2696,6 +2697,297 @@ func TestIDPTemplateProjection_reducesApple(t *testing.T) { } } +func TestIDPTemplateProjection_reducesSAML(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "instance reduceSAMLIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.SAMLIDPAddedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "custom-zitadel-instance", + "metadata": `+stringToJSONByte("metadata")+`, + "key": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "certificate": `+stringToJSONByte("certificate")+`, + "binding": "binding", + "withSignedRequest": true, + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.SAMLIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceSAMLIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateInsertStmt, + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "custom-zitadel-instance", + domain.IdentityProviderTypeSystem, + domain.IDPTypeSAML, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates5_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + []byte("metadata"), + anyArg{}, + anyArg{}, + "binding", + true, + }, + }, + }, + }, + }, + }, + { + name: "org reduceSAMLIDPAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.SAMLIDPAddedEventType), + org.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "custom-zitadel-instance", + "metadata": `+stringToJSONByte("metadata")+`, + "key": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "certificate": `+stringToJSONByte("certificate")+`, + "binding": "binding", + "withSignedRequest": true, + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), org.SAMLIDPAddedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceSAMLIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateInsertStmt, + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "custom-zitadel-instance", + domain.IdentityProviderTypeOrg, + domain.IDPTypeSAML, + true, + true, + true, + true, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates5_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + []byte("metadata"), + anyArg{}, + anyArg{}, + "binding", + true, + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSAMLIDPChanged minimal", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.SAMLIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "custom-zitadel-instance", + "binding": "binding" +}`), + ), instance.SAMLIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceSAMLIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates5 SET (name, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + "custom-zitadel-instance", + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates5_saml SET binding = $1 WHERE (idp_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "binding", + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSAMLIDPChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.SAMLIDPChangedEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "custom-zitadel-instance", + "metadata": `+stringToJSONByte("metadata")+`, + "key": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "certificate": `+stringToJSONByte("certificate")+`, + "binding": "binding", + "withSignedRequest": true, + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.SAMLIDPChangedEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceSAMLIDPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateUpdateStmt, + expectedArgs: []interface{}{ + "custom-zitadel-instance", + true, + true, + true, + true, + anyArg{}, + uint64(15), + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.idp_templates5_saml SET (metadata, key, certificate, binding, with_signed_request) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)", + expectedArgs: []interface{}{ + []byte("metadata"), + anyArg{}, + anyArg{}, + "binding", + true, + "idp-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "org.reduceOwnerRemoved", + reduce: (&idpProjection{}).reduceOwnerRemoved, + args: args{ + event: getEvent(testEvent( + repository.EventType(org.OrgRemovedEventType), + org.AggregateType, + nil, + ), org.OrgRemovedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.idp_templates5 WHERE (instance_id = $1) AND (resource_owner = $2)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if !errors.IsErrorInvalidArgument(err) { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, IDPTemplateTable, tt.want) + }) + } +} + func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { type args struct { event func(t *testing.T) eventstore.Event @@ -4058,3 +4350,8 @@ func TestIDPTemplateProjection_reducesJWT(t *testing.T) { }) } } + +func stringToJSONByte(data string) string { + jsondata, _ := json.Marshal([]byte(data)) + return string(jsondata) +} diff --git a/internal/repository/idp/saml.go b/internal/repository/idp/saml.go new file mode 100644 index 0000000000..2030bcd6f4 --- /dev/null +++ b/internal/repository/idp/saml.go @@ -0,0 +1,164 @@ +package idp + +import ( + "encoding/json" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" +) + +type SAMLIDPAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name string `json:"name,omitempty"` + Metadata []byte `json:"metadata,omitempty"` + Key *crypto.CryptoValue `json:"key,omitempty"` + Certificate []byte `json:"certificate,omitempty"` + Binding string `json:"binding,omitempty"` + WithSignedRequest bool `json:"withSignedRequest,omitempty"` + Options +} + +func NewSAMLIDPAddedEvent( + base *eventstore.BaseEvent, + id, + name string, + metadata []byte, + key *crypto.CryptoValue, + certificate []byte, + binding string, + withSignedRequest bool, + options Options, +) *SAMLIDPAddedEvent { + return &SAMLIDPAddedEvent{ + BaseEvent: *base, + ID: id, + Name: name, + Metadata: metadata, + Key: key, + Certificate: certificate, + Binding: binding, + WithSignedRequest: withSignedRequest, + Options: options, + } +} + +func (e *SAMLIDPAddedEvent) Data() interface{} { + return e +} + +func (e *SAMLIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func SAMLIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &SAMLIDPAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-v9uajo3k71", "unable to unmarshal event") + } + + return e, nil +} + +type SAMLIDPChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` + Name *string `json:"name,omitempty"` + Metadata []byte `json:"metadata,omitempty"` + Key *crypto.CryptoValue `json:"key,omitempty"` + Certificate []byte `json:"certificate,omitempty"` + Binding *string `json:"binding,omitempty"` + WithSignedRequest *bool `json:"withSignedRequest,omitempty"` + OptionChanges +} + +func NewSAMLIDPChangedEvent( + base *eventstore.BaseEvent, + id string, + changes []SAMLIDPChanges, +) (*SAMLIDPChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "IDP-cz6mnf860t", "Errors.NoChangesFound") + } + changedEvent := &SAMLIDPChangedEvent{ + BaseEvent: *base, + ID: id, + } + for _, change := range changes { + change(changedEvent) + } + return changedEvent, nil +} + +type SAMLIDPChanges func(*SAMLIDPChangedEvent) + +func ChangeSAMLName(name string) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.Name = &name + } +} + +func ChangeSAMLMetadata(metadata []byte) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.Metadata = metadata + } +} + +func ChangeSAMLKey(key *crypto.CryptoValue) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.Key = key + } +} + +func ChangeSAMLCertificate(certificate []byte) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.Certificate = certificate + } +} + +func ChangeSAMLBinding(binding string) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.Binding = &binding + } +} + +func ChangeSAMLWithSignedRequest(withSignedRequest bool) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.WithSignedRequest = &withSignedRequest + } +} + +func ChangeSAMLOptions(options OptionChanges) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.OptionChanges = options + } +} + +func (e *SAMLIDPChangedEvent) Data() interface{} { + return e +} + +func (e *SAMLIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func SAMLIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &SAMLIDPChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-w1t1824tw5", "unable to unmarshal event") + } + + return e, nil +} diff --git a/internal/repository/idpintent/eventstore.go b/internal/repository/idpintent/eventstore.go index 12129673a7..1c7417ef0c 100644 --- a/internal/repository/idpintent/eventstore.go +++ b/internal/repository/idpintent/eventstore.go @@ -7,6 +7,8 @@ import ( func RegisterEventMappers(es *eventstore.Eventstore) { es.RegisterFilterEventMapper(AggregateType, StartedEventType, StartedEventMapper). RegisterFilterEventMapper(AggregateType, SucceededEventType, SucceededEventMapper). + RegisterFilterEventMapper(AggregateType, SAMLSucceededEventType, SAMLSucceededEventMapper). + RegisterFilterEventMapper(AggregateType, SAMLRequestEventType, SAMLRequestEventMapper). RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper). RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper) } diff --git a/internal/repository/idpintent/intent.go b/internal/repository/idpintent/intent.go index a4ff650b40..39779df4dd 100644 --- a/internal/repository/idpintent/intent.go +++ b/internal/repository/idpintent/intent.go @@ -14,6 +14,8 @@ import ( const ( StartedEventType = instanceEventTypePrefix + "started" SucceededEventType = instanceEventTypePrefix + "succeeded" + SAMLSucceededEventType = instanceEventTypePrefix + "saml.succeeded" + SAMLRequestEventType = instanceEventTypePrefix + "saml.requested" LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded" FailedEventType = instanceEventTypePrefix + "failed" ) @@ -124,6 +126,103 @@ func SucceededEventMapper(event *repository.Event) (eventstore.Event, error) { return e, nil } +type SAMLSucceededEvent struct { + eventstore.BaseEvent `json:"-"` + + IDPUser []byte `json:"idpUser"` + IDPUserID string `json:"idpUserId,omitempty"` + IDPUserName string `json:"idpUserName,omitempty"` + UserID string `json:"userId,omitempty"` + + Assertion *crypto.CryptoValue `json:"assertion,omitempty"` +} + +func NewSAMLSucceededEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpUser []byte, + idpUserID, + idpUserName, + userID string, + assertion *crypto.CryptoValue, +) *SAMLSucceededEvent { + return &SAMLSucceededEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SAMLSucceededEventType, + ), + IDPUser: idpUser, + IDPUserID: idpUserID, + IDPUserName: idpUserName, + UserID: userID, + Assertion: assertion, + } +} + +func (e *SAMLSucceededEvent) Data() interface{} { + return e +} + +func (e *SAMLSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func SAMLSucceededEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &SAMLSucceededEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-l4tw23y6lq", "unable to unmarshal event") + } + + return e, nil +} + +type SAMLRequestEvent struct { + eventstore.BaseEvent `json:"-"` + + RequestID string `json:"requestId"` +} + +func NewSAMLRequestEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + requestID string, +) *SAMLRequestEvent { + return &SAMLRequestEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SAMLRequestEventType, + ), + RequestID: requestID, + } +} + +func (e *SAMLRequestEvent) Data() interface{} { + return e +} + +func (e *SAMLRequestEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func SAMLRequestEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &SAMLRequestEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-l85678vwlf", "unable to unmarshal event") + } + + return e, nil +} + type LDAPSucceededEvent struct { eventstore.BaseEvent `json:"-"` diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 709abde5e6..c9d750e3b1 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -94,6 +94,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, AppleIDPAddedEventType, AppleIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, AppleIDPChangedEventType, AppleIDPChangedEventMapper). + RegisterFilterEventMapper(AggregateType, SAMLIDPAddedEventType, SAMLIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, SAMLIDPChangedEventType, SAMLIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper). RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper). RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper). diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index 69b1886277..7e82c89b05 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -35,6 +35,8 @@ const ( LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.v2.changed" AppleIDPAddedEventType eventstore.EventType = "instance.idp.apple.added" AppleIDPChangedEventType eventstore.EventType = "instance.idp.apple.changed" + SAMLIDPAddedEventType eventstore.EventType = "instance.idp.saml.added" + SAMLIDPChangedEventType eventstore.EventType = "instance.idp.saml.changed" IDPRemovedEventType eventstore.EventType = "instance.idp.removed" ) @@ -897,7 +899,6 @@ func NewLDAPIDPChangedEvent( id string, changes []idp.LDAPIDPChanges, ) (*LDAPIDPChangedEvent, error) { - changedEvent, err := idp.NewLDAPIDPChangedEvent( eventstore.NewBaseEventForPush( ctx, @@ -1002,6 +1003,85 @@ func AppleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, erro return &AppleIDPChangedEvent{AppleIDPChangedEvent: *e.(*idp.AppleIDPChangedEvent)}, nil } +type SAMLIDPAddedEvent struct { + idp.SAMLIDPAddedEvent +} + +func NewSAMLIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name string, + metadata []byte, + key *crypto.CryptoValue, + certificate []byte, + binding string, + withSignedRequest bool, + options idp.Options, +) *SAMLIDPAddedEvent { + return &SAMLIDPAddedEvent{ + SAMLIDPAddedEvent: *idp.NewSAMLIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + SAMLIDPAddedEventType, + ), + id, + name, + metadata, + key, + certificate, + binding, + withSignedRequest, + options, + ), + } +} + +func SAMLIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.SAMLIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &SAMLIDPAddedEvent{SAMLIDPAddedEvent: *e.(*idp.SAMLIDPAddedEvent)}, nil +} + +type SAMLIDPChangedEvent struct { + idp.SAMLIDPChangedEvent +} + +func NewSAMLIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.SAMLIDPChanges, +) (*SAMLIDPChangedEvent, error) { + + changedEvent, err := idp.NewSAMLIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + SAMLIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &SAMLIDPChangedEvent{SAMLIDPChangedEvent: *changedEvent}, nil +} + +func SAMLIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.SAMLIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &SAMLIDPChangedEvent{SAMLIDPChangedEvent: *e.(*idp.SAMLIDPChangedEvent)}, nil +} + type IDPRemovedEvent struct { idp.RemovedEvent } diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 31ad0c655d..79d409e1c7 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -103,6 +103,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, AppleIDPAddedEventType, AppleIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, AppleIDPChangedEventType, AppleIDPChangedEventMapper). + RegisterFilterEventMapper(AggregateType, SAMLIDPAddedEventType, SAMLIDPAddedEventMapper). + RegisterFilterEventMapper(AggregateType, SAMLIDPChangedEventType, SAMLIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper). RegisterFilterEventMapper(AggregateType, TriggerActionsSetEventType, TriggerActionsSetEventMapper). RegisterFilterEventMapper(AggregateType, TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper). diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index d6e85c13f8..ef590df3fb 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -35,6 +35,8 @@ const ( LDAPIDPChangedEventType eventstore.EventType = "org.idp.ldap.changed" AppleIDPAddedEventType eventstore.EventType = "org.idp.apple.added" AppleIDPChangedEventType eventstore.EventType = "org.idp.apple.changed" + SAMLIDPAddedEventType eventstore.EventType = "org.idp.saml.added" + SAMLIDPChangedEventType eventstore.EventType = "org.idp.saml.changed" IDPRemovedEventType eventstore.EventType = "org.idp.removed" ) @@ -1002,6 +1004,85 @@ func AppleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, erro return &AppleIDPChangedEvent{AppleIDPChangedEvent: *e.(*idp.AppleIDPChangedEvent)}, nil } +type SAMLIDPAddedEvent struct { + idp.SAMLIDPAddedEvent +} + +func NewSAMLIDPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name string, + metadata []byte, + key *crypto.CryptoValue, + certificate []byte, + binding string, + withSignedRequest bool, + options idp.Options, +) *SAMLIDPAddedEvent { + + return &SAMLIDPAddedEvent{ + SAMLIDPAddedEvent: *idp.NewSAMLIDPAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + SAMLIDPAddedEventType, + ), + id, + name, + metadata, + key, + certificate, + binding, + withSignedRequest, + options, + ), + } +} + +func SAMLIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.SAMLIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &SAMLIDPAddedEvent{SAMLIDPAddedEvent: *e.(*idp.SAMLIDPAddedEvent)}, nil +} + +type SAMLIDPChangedEvent struct { + idp.SAMLIDPChangedEvent +} + +func NewSAMLIDPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []idp.SAMLIDPChanges, +) (*SAMLIDPChangedEvent, error) { + changedEvent, err := idp.NewSAMLIDPChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + SAMLIDPChangedEventType, + ), + id, + changes, + ) + if err != nil { + return nil, err + } + return &SAMLIDPChangedEvent{SAMLIDPChangedEvent: *changedEvent}, nil +} + +func SAMLIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.SAMLIDPChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &SAMLIDPChangedEvent{SAMLIDPChangedEvent: *e.(*idp.SAMLIDPChangedEvent)}, nil +} + type IDPRemovedEvent struct { idp.RemovedEvent } diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index d1540b262f..3588c25312 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -505,6 +505,8 @@ Errors: NoChallenge: Сесия без WebAuthN предизвикателство Intent: IDPMissing: IDP липсва в заявката + IDPInvalid: IDP невалиден за заявката + ResponseInvalid: Отговорът на IDP е невалиден SuccessURLMissing: В заявката липсва URL адрес за успех FailureURLMissing: В заявката липсва URL адрес за грешка StateMissing: В заявката липсва параметър състояние diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index ca7d9d45a0..731d63962b 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -487,6 +487,8 @@ Errors: NoChallenge: Sitzung ohne WebAuthN-Challenge Intent: IDPMissing: IDP ID fehlt im Request + IDPInvalid: IDP ungültig für die Anfrage + ResponseInvalid: IDP-Antwort ist ungültig SuccessURLMissing: Success URL fehlt im Request FailureURLMissing: Failure URL fehlt im Request StateMissing: State parameter fehlt im Request diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 82df7a5932..a05c418c3d 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -487,6 +487,8 @@ Errors: NoChallenge: Session without WebAuthN challenge Intent: IDPMissing: IDP ID is missing in the request + IDPInvalid: IDP invalid for the request + ResponseInvalid: IDP response is invalid SuccessURLMissing: Success URL is missing in the request FailureURLMissing: Failure URL is missing in the request StateMissing: State parameter is missing in the request diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index f45ecdcff8..ceb0312735 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -487,6 +487,8 @@ Errors: NoChallenge: Sesión sin desafío WebAuthN Intent: IDPMissing: Falta IDP en la solicitud + IDPInvalid: IDP no válido para la solicitud + ResponseInvalid: La respuesta del IDP no es válida SuccessURLMissing: Falta la URL de éxito en la solicitud FailureURLMissing: Falta la URL de error en la solicitud StateMissing: Falta un parámetro de estado en la solicitud diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 150cb17c0c..d3b5bf7eea 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -487,6 +487,8 @@ Errors: NoChallenge: Session sans challenge WebAuthN Intent: IDPMissing: IDP manquant dans la requête + IDPInvalid: IDP non valide pour la demande + ResponseInvalid: La réponse de l'IDP n'est pas valide SuccessURLMissing: Success URL absent de la requête FailureURLMissing: Failure URL absent de la requête StateMissing: Paramètre d'état manquant dans la requête diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index bd38e9a472..e8f09e0e95 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -487,6 +487,8 @@ Errors: NoChallenge: Sessione senza sfida WebAuthN Intent: IDPMissing: IDP mancante nella richiesta + IDPInvalid: IDP non valido per la richiesta + ResponseInvalid: La risposta dell'IDP non è valida SuccessURLMissing: URL di successo mancante nella richiesta FailureURLMissing: URL di errore mancante nella richiesta StateMissing: parametro di stato mancante nella richiesta diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index cfd1625fc3..d302596104 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -476,6 +476,8 @@ Errors: NoChallenge: WebAuthN チャレンジを使用しないセッション Intent: IDPMissing: リクエストにIDP IDが含まれていません + IDPInvalid: リクエストのIDPが無効 + ResponseInvalid: IDPの回答は無効 SuccessURLMissing: リクエストに成功時の URL がありません FailureURLMissing: リクエストに失敗の URL がありません StateMissing: リクエストに State パラメータがありません diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 026da0aeb2..a11d1fb9ff 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -486,7 +486,9 @@ Errors: WebAuthN: NoChallenge: Сесија без предизвик WebAuthN Intent: - IDPMissing: ID на IDP недостасува во барањето + IDPMissing: ID на IDP недостасува во барањето6bg + IDPInvalid: ВРЛ неважечки за барањето + ResponseInvalid: Одговорот на ВРЛ е неважечки SuccessURLMissing: URL за успех недостасува во барањето FailureURLMissing: URL за неуспех недостасува во барањето StateMissing: Параметарот State недостасува во барањето diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index bd3fb70e91..6808de2eb6 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -487,6 +487,8 @@ Errors: NoChallenge: Sesja bez wyzwania WebAuthN Intent: IDPMissing: Brak identyfikatora IDP w żądaniu + IDPInvalid: IDP nieprawidłowe dla żądania + ResponseInvalid: Odpowiedź IDP jest nieprawidłowa SuccessURLMissing: Brak adresu URL powodzenia w żądaniu FailureURLMissing: Brak adresu URL niepowodzenia w żądaniu StateMissing: Brak parametru stanu w żądaniu diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index eab7ff98e5..9888a2a494 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -485,6 +485,8 @@ Errors: NoChallenge: Sessão sem desafio WebAuthN Intent: IDPMissing: O ID do IDP está faltando na solicitação + IDPInvalid: IDP inválido para o pedido + ResponseInvalid: A resposta da PDI é inválida SuccessURLMissing: A URL de sucesso está faltando na solicitação FailureURLMissing: A URL de falha está faltando na solicitação StateMissing: O parâmetro de estado está faltando na solicitação diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index fe381c029d..83b7b18398 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -487,6 +487,8 @@ Errors: NoChallenge: 没有 WebAuthN 质询的会话 Intent: IDPMissing: 请求中缺少IDP ID + IDPInvalid: 请求的 IDP 无效 + ResponseInvalid: IDP 响应无效 SuccessURLMissing: 请求中缺少成功URL FailureURLMissing: 请求中缺少失败的URL StateMissing: 请求中缺少状态参数 diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index e20e52e427..620936f12b 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -26,7 +26,7 @@ import "validate/validate.proto"; package zitadel.admin.v1; -option go_package ="github.com/zitadel/zitadel/pkg/grpc/admin"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/admin"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { @@ -85,7 +85,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { name: "Message Texts" }, { - name: "Notification Providers" + name: "Notification Providers" }, { name: "Notification Settings" @@ -167,13 +167,13 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } security: { security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } } - } + } responses: { key: "403"; value: { @@ -1684,6 +1684,60 @@ service AdminService { }; } + // Add a new SAML identity provider on the instance + rpc AddSAMLProvider(AddSAMLProviderRequest) returns (AddSAMLProviderResponse) { + option (google.api.http) = { + post: "/idps/saml" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Add SAML Identity Provider"; + description: ""; + }; + } + + // Change an existing SAML identity provider on the instance + rpc UpdateSAMLProvider(UpdateSAMLProviderRequest) returns (UpdateSAMLProviderResponse) { + option (google.api.http) = { + put: "/idps/saml/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Update SAML Identity Provider"; + description: ""; + }; + } + + // Regenerate certificate for an existing SAML identity provider in the organization + rpc RegenerateSAMLProviderCertificate(RegenerateSAMLProviderCertificateRequest) returns (RegenerateSAMLProviderCertificateResponse) { + option (google.api.http) = { + post: "/idps/saml/{id}/_generate_certificate" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Regenerate SAML Identity Provider Certificate"; + description: ""; + }; + } + // Remove an identity provider // Will remove all linked providers of this configuration on the users rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) { @@ -1692,7 +1746,7 @@ service AdminService { }; option (zitadel.v1.auth_option) = { - permission: "org.idp.write" + permission: "iam.idp.write" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -1702,7 +1756,7 @@ service AdminService { }; } - rpc GetOrgIAMPolicy(GetOrgIAMPolicyRequest) returns (GetOrgIAMPolicyResponse) { + rpc GetOrgIAMPolicy(GetOrgIAMPolicyRequest) returns (GetOrgIAMPolicyResponse) { option (google.api.http) = { get: "/policies/orgiam"; }; @@ -2004,7 +2058,7 @@ service AdminService { }; } - rpc UpdateLabelPolicy(UpdateLabelPolicyRequest) returns (UpdateLabelPolicyResponse) { + rpc UpdateLabelPolicy(UpdateLabelPolicyRequest) returns (UpdateLabelPolicyResponse) { option (google.api.http) = { put: "/policies/label"; body: "*"; @@ -4106,10 +4160,10 @@ message GetOIDCSettingsResponse { } message AddOIDCSettingsRequest { - google.protobuf.Duration access_token_lifetime = 1; - google.protobuf.Duration id_token_lifetime = 2; - google.protobuf.Duration refresh_token_idle_expiration = 3; - google.protobuf.Duration refresh_token_expiration = 4; + google.protobuf.Duration access_token_lifetime = 1; + google.protobuf.Duration id_token_lifetime = 2; + google.protobuf.Duration refresh_token_idle_expiration = 3; + google.protobuf.Duration refresh_token_expiration = 4; } message AddOIDCSettingsResponse { @@ -4117,10 +4171,10 @@ message AddOIDCSettingsResponse { } message UpdateOIDCSettingsRequest { - google.protobuf.Duration access_token_lifetime = 1; - google.protobuf.Duration id_token_lifetime = 2; - google.protobuf.Duration refresh_token_idle_expiration = 3; - google.protobuf.Duration refresh_token_expiration = 4; + google.protobuf.Duration access_token_lifetime = 1; + google.protobuf.Duration id_token_lifetime = 2; + google.protobuf.Duration refresh_token_idle_expiration = 3; + google.protobuf.Duration refresh_token_expiration = 4; } message UpdateOIDCSettingsResponse { @@ -4136,9 +4190,9 @@ message GetSecurityPolicyResponse{ message SetSecurityPolicyRequest{ // states if iframe embedding is enabled or disabled - bool enable_iframe_embedding = 1; - // origins allowed loading ZITADEL in an iframe if enable_iframe_embedding is true - repeated string allowed_origins = 2; + bool enable_iframe_embedding = 1; + // origins allowed loading ZITADEL in an iframe if enable_iframe_embedding is true + repeated string allowed_origins = 2; } message SetSecurityPolicyResponse{ @@ -4149,11 +4203,11 @@ message SetSecurityPolicyResponse{ // at least one argument has to be provided message IsOrgUniqueRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - description: "All unique fields of an organization"; - required: ["name", "domain"] - }; - }; + json_schema: { + description: "All unique fields of an organization"; + required: ["name", "domain"] + }; + }; string name = 1 [ (validate.rules).string = {max_len: 200}, @@ -4192,11 +4246,11 @@ message GetOrgByIDResponse { message ListOrgsRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - description: "Search query for lists"; - required: ["query"] - }; - }; + json_schema: { + description: "Search query for lists"; + required: ["query"] + }; + }; //list limitations and ordering zitadel.v1.ListQuery query = 1; @@ -4214,18 +4268,18 @@ message ListOrgsResponse { message SetUpOrgRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - description: "Request to set up an organization. User is required"; - required: ["org", "user"] - }; - }; + json_schema: { + description: "Request to set up an organization. User is required"; + required: ["org", "user"] + }; + }; message Org { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { required: ["name"] }; - }; + }; string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -4249,7 +4303,7 @@ message SetUpOrgRequest { json_schema: { required: ["user_name", "profile", "email", "password"]; }; - }; + }; message Profile { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { @@ -4583,11 +4637,11 @@ message AddJWTIDPResponse { message UpdateIDPRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - description: "Updates fields of an IDP"; - required: ["idp_id", "name"] - }; - }; + json_schema: { + description: "Updates fields of an IDP"; + required: ["idp_id", "name"] + }; + }; string idp_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string name = 2 [ @@ -4613,10 +4667,10 @@ message UpdateIDPResponse { message DeactivateIDPRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["idp_id"] - }; - }; + json_schema: { + required: ["idp_id"] + }; + }; string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -4633,10 +4687,10 @@ message DeactivateIDPResponse { message ReactivateIDPRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["idp_id"] - }; - }; + json_schema: { + required: ["idp_id"] + }; + }; string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -4653,10 +4707,10 @@ message ReactivateIDPResponse { message RemoveIDPRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["idp_id"] - }; - }; + json_schema: { + required: ["idp_id"] + }; + }; string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -4674,10 +4728,10 @@ message RemoveIDPResponse { message UpdateIDPOIDCConfigRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["idp_id", "issuer", "client_id"] - }; - }; + json_schema: { + required: ["idp_id", "issuer", "client_id"] + }; + }; string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -5741,6 +5795,86 @@ message UpdateAppleProviderResponse { zitadel.v1.ObjectDetails details = 1; } +message AddSAMLProviderRequest { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 2 [ + (validate.rules).bytes.max_len = 500000, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Metadata of the SAML identity provider"; + } + ]; + string metadata_url = 3 [ + (validate.rules).string.max_len = 200, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://test.com/saml/metadata\"" + description: "Url to the metadata of the SAML identity provider"; + } + ]; + } + zitadel.idp.v1.SAMLBinding binding = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Binding which defines the type of communication with the identity provider"; + } + ]; + bool with_signed_request = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Boolean which defines if the authentication requests are signed"; + } + ]; + zitadel.idp.v1.Options provider_options = 6; +} + +message AddSAMLProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateSAMLProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 3 [ + (validate.rules).bytes.max_len = 500000, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Metadata of the SAML identity provider"; + } + ]; + string metadata_url = 4 [ + (validate.rules).string.max_len = 200, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://test.com/saml/metadata\"" + description: "Url to the metadata of the SAML identity provider"; + } + ]; + } + zitadel.idp.v1.SAMLBinding binding = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Binding which defines the type of communication with the identity provider"; + } + ]; + bool with_signed_request = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Boolean which defines if the authentication requests are signed"; + } + ]; + zitadel.idp.v1.Options provider_options = 7; +} + +message UpdateSAMLProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message RegenerateSAMLProviderCertificateRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message RegenerateSAMLProviderCertificateResponse { + zitadel.v1.ObjectDetails details = 1; +} + message DeleteProviderRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -5876,10 +6010,10 @@ message UpdateDomainPolicyResponse { message GetCustomDomainPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["org_id"] - }; - }; + json_schema: { + required: ["org_id"] + }; + }; string org_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -5898,10 +6032,10 @@ message GetCustomDomainPolicyResponse { message AddCustomDomainPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["org_id"] - }; - }; + json_schema: { + required: ["org_id"] + }; + }; string org_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -5934,10 +6068,10 @@ message AddCustomDomainPolicyResponse { message UpdateCustomDomainPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["org_id"] - }; - }; + json_schema: { + required: ["org_id"] + }; + }; string org_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -5970,10 +6104,10 @@ message UpdateCustomDomainPolicyResponse { message ResetCustomDomainPolicyToDefaultRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["org_id"] - }; - }; + json_schema: { + required: ["org_id"] + }; + }; string org_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -6050,7 +6184,7 @@ message UpdateLabelPolicyRequest { } ]; string background_color_dark = 8 [ - (validate.rules).string = { max_len: 50}, + (validate.rules).string = {max_len: 50}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "hex value for background color dark theme"; example: "\"#111827\""; @@ -6058,7 +6192,7 @@ message UpdateLabelPolicyRequest { } ]; string warn_color_dark = 9 [ - (validate.rules).string = { max_len: 50}, + (validate.rules).string = {max_len: 50}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "hex value for warning color dark theme"; example: "\"#FF3B5B\""; @@ -6066,7 +6200,7 @@ message UpdateLabelPolicyRequest { } ]; string font_color_dark = 10 [ - (validate.rules).string = { max_len: 50}, + (validate.rules).string = {max_len: 50}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "hex value for font color dark theme"; example: "\"#FFFFFF\""; @@ -6214,10 +6348,10 @@ message ListLoginPolicyIDPsResponse { message AddIDPToLoginPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["org_id"] - }; - }; + json_schema: { + required: ["org_id"] + }; + }; string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -6237,10 +6371,10 @@ message AddIDPToLoginPolicyResponse { message RemoveIDPFromLoginPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["idp_id"] - }; - }; + json_schema: { + required: ["idp_id"] + }; + }; string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -6266,10 +6400,10 @@ message ListLoginPolicySecondFactorsResponse { message AddSecondFactorToLoginPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["type"] - }; - }; + json_schema: { + required: ["type"] + }; + }; zitadel.policy.v1.SecondFactorType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; } @@ -6280,10 +6414,10 @@ message AddSecondFactorToLoginPolicyResponse { message RemoveSecondFactorFromLoginPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["type"] - }; - }; + json_schema: { + required: ["type"] + }; + }; zitadel.policy.v1.SecondFactorType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; } @@ -6302,10 +6436,10 @@ message ListLoginPolicyMultiFactorsResponse { message AddMultiFactorToLoginPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["type"] - }; - }; + json_schema: { + required: ["type"] + }; + }; zitadel.policy.v1.MultiFactorType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; } @@ -6316,10 +6450,10 @@ message AddMultiFactorToLoginPolicyResponse { message RemoveMultiFactorFromLoginPolicyRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["type"] - }; - }; + json_schema: { + required: ["type"] + }; + }; zitadel.policy.v1.MultiFactorType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; } @@ -6469,11 +6603,11 @@ message GetNotificationPolicyResponse { } message UpdateNotificationPolicyRequest { - bool password_change = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "If set to true the users will get a notification whenever their password has been changed."; - } - ]; + bool password_change = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "If set to true the users will get a notification whenever their password has been changed."; + } + ]; } message UpdateNotificationPolicyResponse { @@ -7261,10 +7395,10 @@ message ResetCustomLoginTextsToDefaultResponse { message AddIAMMemberRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["user_id"] - }; - }; + json_schema: { + required: ["user_id"] + }; + }; string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -7288,10 +7422,10 @@ message AddIAMMemberResponse { message UpdateIAMMemberRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["user_id"] - }; - }; + json_schema: { + required: ["user_id"] + }; + }; string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -7315,10 +7449,10 @@ message UpdateIAMMemberResponse { message RemoveIAMMemberRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["user_id"] - }; - }; + json_schema: { + required: ["user_id"] + }; + }; string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -7376,10 +7510,10 @@ message ListFailedEventsResponse { message RemoveFailedEventRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["database", "view_name", "failed_sequence"] - }; - }; + json_schema: { + required: ["database", "view_name", "failed_sequence"] + }; + }; string database = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -7476,7 +7610,7 @@ message ImportDataRequest { message S3Input{ string path = 1; string endpoint = 2; - string access_key_id =3; + string access_key_id = 3; string secret_access_key = 4; bool ssl = 5; string bucket = 6; @@ -7635,7 +7769,7 @@ message ExportDataRequest { message S3Output{ string path = 1; string endpoint = 2; - string access_key_id =3; + string access_key_id = 3; string secret_access_key = 4; bool ssl = 5; string bucket = 6; diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index ddf416dd54..6171471075 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -117,7 +117,6 @@ enum IDPStylingType { enum IDPType { IDP_TYPE_UNSPECIFIED = 0; IDP_TYPE_OIDC = 1; - //PLANNED: IDP_TYPE_SAML IDP_TYPE_JWT = 3; } @@ -267,6 +266,14 @@ enum ProviderType { PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9; PROVIDER_TYPE_GOOGLE = 10; PROVIDER_TYPE_APPLE = 11; + PROVIDER_TYPE_SAML = 12; +} + +enum SAMLBinding { + SAML_BINDING_UNSPECIFIED = 0; + SAML_BINDING_POST = 1; + SAML_BINDING_REDIRECT = 2; + SAML_BINDING_ARTIFACT = 3; } message ProviderConfig { @@ -283,6 +290,7 @@ message ProviderConfig { GitLabSelfHostedConfig gitlab_self_hosted = 10; AzureADConfig azure_ad = 11; AppleConfig apple = 12; + SAMLConfig saml = 13; } } @@ -443,6 +451,24 @@ message LDAPConfig { LDAPAttributes attributes = 9; } +message SAMLConfig { + bytes metadata_xml = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Metadata of the SAML identity provider"; + } + ]; + zitadel.idp.v1.SAMLBinding binding = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Binding which defines the type of communication with the identity provider"; + } + ]; + bool with_signed_request = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Boolean which defines if the authentication requests are signed"; + } + ]; +} + message AzureADConfig { string client_id = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 26b9bb0f68..6770f77427 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -7094,6 +7094,60 @@ service ManagementService { }; } + // Add a new SAML identity provider in the organization + rpc AddSAMLProvider(AddSAMLProviderRequest) returns (AddSAMLProviderResponse) { + option (google.api.http) = { + post: "/idps/saml" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Add SAML Identity Provider"; + description: ""; + }; + } + + // Change an existing SAML identity provider in the organization + rpc UpdateSAMLProvider(UpdateSAMLProviderRequest) returns (UpdateSAMLProviderResponse) { + option (google.api.http) = { + put: "/idps/saml/{id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Update SAML Identity Provider"; + description: ""; + }; + } + + // Regenerate certificate for an existing SAML identity provider in the organization + rpc RegenerateSAMLProviderCertificate(RegenerateSAMLProviderCertificateRequest) returns (RegenerateSAMLProviderCertificateResponse) { + option (google.api.http) = { + post: "/idps/saml/{id}/_generate_certificate" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Regenerate SAML Identity Provider Certificate"; + description: ""; + }; + } + // Remove an identity provider // Will remove all linked providers of this configuration on the users rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) { @@ -12485,6 +12539,86 @@ message UpdateLDAPProviderResponse { zitadel.v1.ObjectDetails details = 1; } +message AddSAMLProviderRequest { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 2 [ + (validate.rules).bytes.max_len = 500000, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Metadata of the SAML identity provider"; + } + ]; + string metadata_url = 3 [ + (validate.rules).string.max_len = 200, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://test.com/saml/metadata\"" + description: "Url to the metadata of the SAML identity provider"; + } + ]; + } + zitadel.idp.v1.SAMLBinding binding = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Binding which defines the type of communication with the identity provider"; + } + ]; + bool with_signed_request = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Boolean which defines if the authentication requests are signed"; + } + ]; + zitadel.idp.v1.Options provider_options = 6; +} + +message AddSAMLProviderResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateSAMLProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 3 [ + (validate.rules).bytes.max_len = 500000, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Metadata of the SAML identity provider"; + } + ]; + string metadata_url = 4 [ + (validate.rules).string.max_len = 200, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://test.com/saml/metadata\"" + description: "Url to the metadata of the SAML identity provider"; + } + ]; + } + zitadel.idp.v1.SAMLBinding binding = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Binding which defines the type of communication with the identity provider"; + } + ]; + bool with_signed_request = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Boolean which defines if the authentication requests are signed"; + } + ]; + zitadel.idp.v1.Options provider_options = 7; +} + +message UpdateSAMLProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message RegenerateSAMLProviderCertificateRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message RegenerateSAMLProviderCertificateResponse { + zitadel.v1.ObjectDetails details = 1; +} + message AddAppleProviderRequest { // Apple will be used as default, if no name is provided string name = 1 [ diff --git a/proto/zitadel/user/v2beta/idp.proto b/proto/zitadel/user/v2beta/idp.proto index 9bf11c7c30..dc6e07a5e2 100644 --- a/proto/zitadel/user/v2beta/idp.proto +++ b/proto/zitadel/user/v2beta/idp.proto @@ -89,6 +89,11 @@ message IDPInformation{ description: "LDAP entity attributes returned by the identity provider" } ]; + IDPSAMLAccessInformation saml = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "SAMLResponse returned by the identity provider" + } + ]; } string idp_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -124,6 +129,10 @@ message IDPLDAPAccessInformation{ google.protobuf.Struct attributes = 1; } +message IDPSAMLAccessInformation{ + bytes assertion = 1; +} + message IDPLink { string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 94dcc9b3fd..01abeceed4 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -1132,6 +1132,11 @@ message StartIdentityProviderIntentResponse{ description: "IDP Intent information" } ]; + bytes post_form = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "POST call information" + } + ]; } }