feat: add SAML as identity provider (#6454)

* feat: first implementation for saml sp

* fix: add command side instance and org for saml provider

* fix: add query side instance and org for saml provider

* fix: request handling in event and retrieval of finished intent

* fix: add review changes and integration tests

* fix: add integration tests for saml idp

* fix: correct unit tests with review changes

* fix: add saml session unit test

* fix: add saml session unit test

* fix: add saml session unit test

* fix: changes from review

* fix: changes from review

* fix: proto build error

* fix: proto build error

* fix: proto build error

* fix: proto require metadata oneof

* fix: login with saml provider

* fix: integration test for saml assertion

* lint client.go

* fix json tag

* fix: linting

* fix import

* fix: linting

* fix saml idp query

* fix: linting

* lint: try all issues

* revert linting config

* fix: add regenerate endpoints

* fix: translations

* fix mk.yaml

* ignore acs path for user agent cookie

* fix: add AuthFromProvider test for saml

* fix: integration test for saml retrieve information

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz 2023-09-29 11:26:14 +02:00 committed by GitHub
parent 2e99d0fe1b
commit 15fd3045e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 6301 additions and 245 deletions

View File

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

7
go.mod
View File

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

16
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
},
},
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) {

View File

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

View File

@ -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("<?xml version=\"1.0\"?>"), 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-08-27T12:40:58.803Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
&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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-08-27T12:40:58.803Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
&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("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
},
),
),
),
),
},
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("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
},
),
),
),
),
},
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,6 +55,7 @@ type AuthRequest struct {
LockoutPolicy *LockoutPolicy
DefaultTranslations []*CustomText
OrgTranslations []*CustomText
SAMLRequestID string
}
type ExternalUser struct {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-08-27T12:40:58.803Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-08-27T12:40:58.803Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-08-27T12:40:58.803Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-08-27T12:40:58.803Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-08-27T12:40:58.803Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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)
}
})
}
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
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{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:"-"`

View File

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

View File

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

View File

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

View File

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

View File

@ -505,6 +505,8 @@ Errors:
NoChallenge: Сесия без WebAuthN предизвикателство
Intent:
IDPMissing: IDP липсва в заявката
IDPInvalid: IDP невалиден за заявката
ResponseInvalid: Отговорът на IDP е невалиден
SuccessURLMissing: В заявката липсва URL адрес за успех
FailureURLMissing: В заявката липсва URL адрес за грешка
StateMissing: В заявката липсва параметър състояние

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -476,6 +476,8 @@ Errors:
NoChallenge: WebAuthN チャレンジを使用しないセッション
Intent:
IDPMissing: リクエストにIDP IDが含まれていません
IDPInvalid: リクエストのIDPが無効
ResponseInvalid: IDPの回答は無効
SuccessURLMissing: リクエストに成功時の URL がありません
FailureURLMissing: リクエストに失敗の URL がありません
StateMissing: リクエストに State パラメータがありません

View File

@ -486,7 +486,9 @@ Errors:
WebAuthN:
NoChallenge: Сесија без предизвик WebAuthN
Intent:
IDPMissing: ID на IDP недостасува во барањето
IDPMissing: ID на IDP недостасува во барањето6bg
IDPInvalid: ВРЛ неважечки за барањето
ResponseInvalid: Одговорот на ВРЛ е неважечки
SuccessURLMissing: URL за успех недостасува во барањето
FailureURLMissing: URL за неуспех недостасува во барањето
StateMissing: Параметарот State недостасува во барањето

View File

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

View File

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

View File

@ -487,6 +487,8 @@ Errors:
NoChallenge: 没有 WebAuthN 质询的会话
Intent:
IDPMissing: 请求中缺少IDP ID
IDPInvalid: 请求的 IDP 无效
ResponseInvalid: IDP 响应无效
SuccessURLMissing: 请求中缺少成功URL
FailureURLMissing: 请求中缺少失败的URL
StateMissing: 请求中缺少状态参数

View File

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

View File

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

View File

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

View File

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

View File

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