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