package integration import ( "context" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/xml" "fmt" "io" "net/http" "net/url" "strings" "github.com/brianvoe/gofakeit/v6" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "github.com/zitadel/logging" http_util "github.com/zitadel/zitadel/internal/api/http" oidc_internal "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/pkg/grpc/management" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" session_pb "github.com/zitadel/zitadel/pkg/grpc/session/v2" ) const spCertificate = `-----BEGIN CERTIFICATE----- MIIDITCCAgmgAwIBAgIUUo5urYkuUHAe7LQ9sZSL+xXAqBwwDQYJKoZIhvcNAQEL BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIwNDEz MTE1MFoXDTI1MDEwMzEzMTE1MFowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoACwbGIh8udK Um1r+yQoPtfswEX6Cb6Y1KwR6WZDYgzHdMyUC5Sy8Bg1H2puUZZukDLuyu6Pqvum 8kfnzhjUR6nNCoUlidwE+yz020w5oOBofRKgJK/FVUuWD3k6kjdP9CrBFLG0PQQ3 N2e4wilP4czCxizKero2a0e7Eq8OjHAPf8gjM+GWFZgVAbV8uf2Mjt1O2Vfbx5PZ sLuBZtl5jokx3NiC7my/yj81MbGEDPcQo0emeVBz3J3nVG6Yr4kdCKkvv2dhJ26C 5cL7NIIUY4IQomJNwYC2NaYgSpQOxJHL/HsOPusO4Ia2WtUTXEZUFkxn1u0YuoSx CkGehF/1OwIDAQABo1MwUTAdBgNVHQ4EFgQUr6S0wA2l3MdfnvfveWDueQtaoJMw HwYDVR0jBBgwFoAUr6S0wA2l3MdfnvfveWDueQtaoJMwDwYDVR0TAQH/BAUwAwEB /zANBgkqhkiG9w0BAQsFAAOCAQEAH3Q9obyWJaMKFuGJDkIp1RFot79RWTVcAcwA XTJNfCseLONRIs4MkRxOn6GQBwV2IEqs1+hFG80dcd/c6yYyJ8bziKEyNMtPWrl6 fdVD+1WnWcD1ZYrS8hgdz0FxXxl/+GjA8Pu6icmnhKgUDTYWns6Rj/gtQtZS8ZoA JY+T/1mGze2+Xx6pjuArZ7+hnH6EWwo+ckcmXAKyhnkhX7xIo1UFvNY2VWaGl2wU K2yyJA4Lu/NNmqPnpAcRDsnGP6r4frMhjnPq/ifC3B+6FT3p8dubV9PA0y86bAy5 0yIgNje4DyWLy/DM9EpdPfJmvUAL6hOtyb8Aa9hR+a8stu7h6g== -----END CERTIFICATE-----` const spKey = `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCgALBsYiHy50pS bWv7JCg+1+zARfoJvpjUrBHpZkNiDMd0zJQLlLLwGDUfam5Rlm6QMu7K7o+q+6by R+fOGNRHqc0KhSWJ3AT7LPTbTDmg4Gh9EqAkr8VVS5YPeTqSN0/0KsEUsbQ9BDc3 Z7jCKU/hzMLGLMp6ujZrR7sSrw6McA9/yCMz4ZYVmBUBtXy5/YyO3U7ZV9vHk9mw u4Fm2XmOiTHc2ILubL/KPzUxsYQM9xCjR6Z5UHPcnedUbpiviR0IqS+/Z2EnboLl wvs0ghRjghCiYk3BgLY1piBKlA7Ekcv8ew4+6w7ghrZa1RNcRlQWTGfW7Ri6hLEK QZ6EX/U7AgMBAAECggEAD1aRkwpDO+BdORKhP9WDACc93F647fc1+mk2XFv/yKX1 9uXnqUaLcsW3TfgrdCnKFouzZYPCBP+TzPUErTanHumRrNj/tLwBRDzWijE/8wKg MaE39dxdu+P/kiMqcLrZsMvqb3vrjc/aJTcNuJsyO7Cf2VSQ4nv4XIdnUQ60A9VR OmUp//VULZxImnPx/R304/p5VfOhyXfzBeoxUPogBurjtzkyXVG0EG2enJMMiTix 900fTDez0TQ8V6O59vM04fhtPXvH51OkMTW/HU1QQvlnAJuX06I7k4CaBpF3xPII QpEbFILq5y6yAQJWELRGWzeoxK6kn6bNfI8S0+oKqQKBgQDg2UM7ruMASpY7B4zj XNztGDOx9BCdYyHH1O05r+ILmltBC7jFImwIYrHbaX+dg52l0PPImZuBz852IqrC VAEF30yBn2gWyVzIdo7W3mw9Jgqc4LrhStaJxOuXVoT2/PAuDBF8TJMNH9oLNqiD aPAI0cVn9BRV7AziEsrMlDLLiQKBgQC2K4Z/caAvwx/AescsN6lp+/m7MeLUpZzQ myZt44bnR5LouUo3vCYl+Bk8wu6PTd41LUYW/SW26HDDFTKgkBb1zVHfk5QRApaB VPwZnhcUvNapPOnDp75Qoq238wpfayQlKF1xCawS3N5AWkDaEdfzuH7umFJxVss2 1tfDsn01owKBgAYWG3nMHBzv5+0lIS0uYFSSqSOSBbkc69cq7lj3Z9kEjp/OH2xG qEH52fKkgm3TGDta0p6Fee4jn+UWvySPfY+ZIcsIc5raTIaonuk2EBv/oZ3pf2WF zxTfnbj1AJhm9GFqtjZ1JC3gxNg03I7iEk1K0FsmAj7pKtgbxh2PjWhxAoGBAKBx BSwJbwOh3r0vZWvUOilV+0SbUyPmGI7Blr8BvTbFGuZNCsi7tP2L3O5e4Kzl7+b1 0N0+Z5EIdwfaC5TOUup5wroeyDGTDesqZj5JthpVltnHBDuF6WArZsS0EVaojlUL kACWfC7AyB31X1iwjnng7CpHjZS01JWf8rgw44XxAoGAQ5YYd4WmGYZoJJak7zhb xnYG7hU7nS7pBPGob1FvjYMw1x/htuJCjxLh08dlzJGM6SFlDn7HVM9ou99w5n+d xtqmbthw2E9VjSk3zSYb4uFc6mv0C/kRPTDUFH+9CpQTBBx/O016hmcatxlBS6JL VAV6oE8sEJYHtR6YdZiMWWo= -----END PRIVATE KEY-----` func CreateSAMLSP(root string, idpMetadata *saml.EntityDescriptor, binding string) (*samlsp.Middleware, error) { rootURL, err := url.Parse(root) if err != nil { return nil, err } keyPair, err := tls.X509KeyPair([]byte(spCertificate), []byte(spKey)) if err != nil { return nil, err } keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { return nil, err } sp, err := samlsp.New(samlsp.Options{ URL: *rootURL, Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, IDPMetadata: idpMetadata, UseArtifactResponse: false, }) if err != nil { return nil, err } sp.Binding = binding sp.ResponseBinding = binding return sp, nil } func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *samlsp.Middleware) (*management.AddSAMLAppResponse, error) { spMetadata, err := xml.MarshalIndent(m.ServiceProvider.Metadata(), "", " ") if err != nil { return nil, err } if m.ResponseBinding == saml.HTTPRedirectBinding { metadata := strings.Replace(string(spMetadata), saml.HTTPPostBinding, saml.HTTPRedirectBinding, 2) spMetadata = []byte(metadata) } resp, err := i.Client.Mgmt.AddSAMLApp(ctx, &management.AddSAMLAppRequest{ ProjectId: projectID, Name: fmt.Sprintf("app-%s", gofakeit.AppName()), Metadata: &management.AddSAMLAppRequest_MetadataXml{MetadataXml: spMetadata}, }) if err != nil { return nil, err } return resp, await(func() error { _, err := i.Client.Mgmt.GetProjectByID(ctx, &management.GetProjectByIDRequest{ Id: projectID, }) if err != nil { return err } _, err = i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ ProjectId: projectID, AppId: resp.GetAppId(), }) return err }) } func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient string, acs saml.Endpoint, relayState string, responseBinding string) (authRequestID string, err error) { authReq, err := m.ServiceProvider.MakeAuthenticationRequest(acs.Location, acs.Binding, responseBinding) if err != nil { return "", err } redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider) if err != nil { return "", err } req, err := GetRequest(redirectURL.String(), map[string]string{oidc_internal.LoginClientHeader: loginClient}) if err != nil { return "", fmt.Errorf("get request: %w", err) } loc, err := CheckRedirect(req) if err != nil { return "", fmt.Errorf("check redirect: %w", err) } prefixWithHost := i.Issuer() + i.Config.LoginURLV2 if !strings.HasPrefix(loc.String(), prefixWithHost) { return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String()) } return strings.TrimPrefix(loc.String(), prefixWithHost), nil } func (i *Instance) FailSAMLAuthRequest(ctx context.Context, id string, reason saml_pb.ErrorReason) *saml_pb.CreateResponseResponse { resp, err := i.Client.SAMLv2.CreateResponse(ctx, &saml_pb.CreateResponseRequest{ SamlRequestId: id, ResponseKind: &saml_pb.CreateResponseRequest_Error{Error: &saml_pb.AuthorizationError{Error: reason}}, }) logging.OnError(err).Panic("create human user") return resp } func (i *Instance) SuccessfulSAMLAuthRequest(ctx context.Context, userId, id string) *saml_pb.CreateResponseResponse { respSession, err := i.Client.SessionV2.CreateSession(ctx, &session_pb.CreateSessionRequest{ Checks: &session_pb.Checks{ User: &session_pb.CheckUser{ Search: &session_pb.CheckUser_UserId{ UserId: userId, }, }, }, }) logging.OnError(err).Panic("create session") resp, err := i.Client.SAMLv2.CreateResponse(ctx, &saml_pb.CreateResponseRequest{ SamlRequestId: id, ResponseKind: &saml_pb.CreateResponseRequest_Session{ Session: &saml_pb.Session{ SessionId: respSession.GetSessionId(), SessionToken: respSession.GetSessionToken(), }, }, }) logging.OnError(err).Panic("create human user") return resp } func (i *Instance) GetSAMLIDPMetadata() (*saml.EntityDescriptor, error) { idpEntityID := http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure) + "/saml/v2/metadata" resp, err := http.Get(idpEntityID) if err != nil { return nil, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } entityDescriptor := new(saml.EntityDescriptor) if err := xml.Unmarshal(data, entityDescriptor); err != nil { return nil, err } return entityDescriptor, nil } func (i *Instance) Issuer() string { return http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure) }