mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
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:
parent
2e99d0fe1b
commit
15fd3045e0
@ -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
7
go.mod
@ -15,6 +15,7 @@ require (
|
||||
github.com/boombuler/barcode v1.0.1
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.5
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
|
||||
github.com/crewjam/saml v0.4.13
|
||||
github.com/descope/virtualwebauthn v1.0.2
|
||||
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562
|
||||
@ -87,11 +88,13 @@ require (
|
||||
|
||||
require (
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect
|
||||
github.com/crewjam/httperr v0.2.0 // indirect
|
||||
github.com/dmarkham/enumer v1.5.8 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.4 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
|
||||
github.com/golang/glog v1.1.1 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
@ -99,12 +102,14 @@ require (
|
||||
github.com/google/s2a-go v0.1.5 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
|
||||
github.com/pascaldekloe/name v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/smartystreets/assertions v1.0.0 // indirect
|
||||
github.com/zenazn/goji v1.0.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
@ -120,7 +125,7 @@ require (
|
||||
cloud.google.com/go/trace v1.10.1 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/amdonov/xmlsig v0.1.0 // indirect
|
||||
github.com/beevik/etree v1.2.0 // indirect
|
||||
github.com/beevik/etree v1.2.0
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
|
16
go.sum
16
go.sum
@ -165,9 +165,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
|
||||
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
|
||||
github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
|
||||
github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/descope/virtualwebauthn v1.0.2 h1:cAvfS9wHh6On9HAE4Gjn3fJkf8MPQW2LzN8BPKEPs0M=
|
||||
@ -324,6 +329,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
@ -625,6 +632,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@ -769,6 +778,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
@ -779,6 +789,7 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||
github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys=
|
||||
github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@ -868,6 +879,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
|
||||
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/zitadel/logging v0.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA=
|
||||
github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc=
|
||||
github.com/zitadel/oidc/v2 v2.11.0 h1:Am4/yQr4iiM5bznRgF3FOp+wLdKx2gzSU73uyI9vvBE=
|
||||
@ -947,6 +960,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
@ -1389,6 +1403,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -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 {
|
||||
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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")
|
||||
|
488
internal/api/idp/idp_integration_test.go
Normal file
488
internal/api/idp/idp_integration_test.go
Normal 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
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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) }()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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}).
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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}).
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -55,6 +55,7 @@ type AuthRequest struct {
|
||||
LockoutPolicy *LockoutPolicy
|
||||
DefaultTranslations []*CustomText
|
||||
OrgTranslations []*CustomText
|
||||
SAMLRequestID string
|
||||
}
|
||||
|
||||
type ExternalUser struct {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
90
internal/idp/providers/saml/mapper.go
Normal file
90
internal/idp/providers/saml/mapper.go
Normal 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 ""
|
||||
}
|
@ -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)
|
||||
}
|
175
internal/idp/providers/saml/saml.go
Normal file
175
internal/idp/providers/saml/saml.go
Normal 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
|
||||
}
|
161
internal/idp/providers/saml/saml_test.go
Normal file
161
internal/idp/providers/saml/saml_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
93
internal/idp/providers/saml/session.go
Normal file
93
internal/idp/providers/saml/session.go
Normal 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{}),
|
||||
}
|
||||
}
|
214
internal/idp/providers/saml/session_test.go
Normal file
214
internal/idp/providers/saml/session_test.go
Normal file
File diff suppressed because one or more lines are too long
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
164
internal/repository/idp/saml.go
Normal file
164
internal/repository/idp/saml.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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:"-"`
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -505,6 +505,8 @@ Errors:
|
||||
NoChallenge: Сесия без WebAuthN предизвикателство
|
||||
Intent:
|
||||
IDPMissing: IDP липсва в заявката
|
||||
IDPInvalid: IDP невалиден за заявката
|
||||
ResponseInvalid: Отговорът на IDP е невалиден
|
||||
SuccessURLMissing: В заявката липсва URL адрес за успех
|
||||
FailureURLMissing: В заявката липсва URL адрес за грешка
|
||||
StateMissing: В заявката липсва параметър състояние
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -476,6 +476,8 @@ Errors:
|
||||
NoChallenge: WebAuthN チャレンジを使用しないセッション
|
||||
Intent:
|
||||
IDPMissing: リクエストにIDP IDが含まれていません
|
||||
IDPInvalid: リクエストのIDPが無効
|
||||
ResponseInvalid: IDPの回答は無効
|
||||
SuccessURLMissing: リクエストに成功時の URL がありません
|
||||
FailureURLMissing: リクエストに失敗の URL がありません
|
||||
StateMissing: リクエストに State パラメータがありません
|
||||
|
@ -486,7 +486,9 @@ Errors:
|
||||
WebAuthN:
|
||||
NoChallenge: Сесија без предизвик WebAuthN
|
||||
Intent:
|
||||
IDPMissing: ID на IDP недостасува во барањето
|
||||
IDPMissing: ID на IDP недостасува во барањето6bg
|
||||
IDPInvalid: ВРЛ неважечки за барањето
|
||||
ResponseInvalid: Одговорот на ВРЛ е неважечки
|
||||
SuccessURLMissing: URL за успех недостасува во барањето
|
||||
FailureURLMissing: URL за неуспех недостасува во барањето
|
||||
StateMissing: Параметарот State недостасува во барањето
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -487,6 +487,8 @@ Errors:
|
||||
NoChallenge: 没有 WebAuthN 质询的会话
|
||||
Intent:
|
||||
IDPMissing: 请求中缺少IDP ID
|
||||
IDPInvalid: 请求的 IDP 无效
|
||||
ResponseInvalid: IDP 响应无效
|
||||
SuccessURLMissing: 请求中缺少成功URL
|
||||
FailureURLMissing: 请求中缺少失败的URL
|
||||
StateMissing: 请求中缺少状态参数
|
||||
|
@ -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;
|
||||
|
@ -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) = {
|
||||
|
@ -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 [
|
||||
|
@ -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},
|
||||
|
@ -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"
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user