mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-10 02:53:40 +00:00
c3b97a91a2
# Which Problems Are Solved It is currently not possible to use SAML with the Session API. # How the Problems Are Solved Add SAML service, to get and resolve SAML requests. Add SAML session and SAML request aggregate, which can be linked to the Session to get back a SAMLResponse from the API directly. # Additional Changes Update of dependency zitadel/saml to provide all functionality for handling of SAML requests and responses. # Additional Context Closes #6053 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
224 lines
7.9 KiB
Go
224 lines
7.9 KiB
Go
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)
|
|
}
|