fix(api): return typed saml form post data in idp intent (#10136)

<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved

The current user V2 API returns a `[]byte` containing a whole HTML
document including the form on `StartIdentifyProviderIntent` for intents
based on form post (e.g. SAML POST bindings). This is not usable for
most clients as they cannot handle that and render a whole page inside
their app.
For redirect based intents, the url to which the client needs to
redirect is returned.

# How the Problems Are Solved

- Changed the returned type to a new `FormData` message containing the
url and a `fields` map.
- internal changes:
- Session.GetAuth now returns an `Auth` interfacce and error instead of
(content string, redirect bool)
- Auth interface has two implementations: `RedirectAuth` and `FormAuth`
- All use of the GetAuth function now type switch on the returned auth
object
- A template has been added to the login UI to execute the form post
automatically (as is).

# Additional Changes

- Some intent integration test did not check the redirect url and were
wrongly configured.

# Additional Context

- relates to zitadel/typescript#410
This commit is contained in:
Livio Spring
2025-06-30 11:07:33 -04:00
committed by GitHub
parent b7d447e313
commit 64a03fba28
26 changed files with 512 additions and 133 deletions

View File

@@ -2057,7 +2057,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "http://" + Instance.Domain + ":8000/sso",
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,
@@ -2081,7 +2081,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "http://" + Instance.Domain + ":8000/sso",
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,
@@ -2105,7 +2105,9 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
postForm: true,
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
postForm: true,
},
wantErr: false,
},
@@ -2143,9 +2145,11 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
}
require.NoError(t, err)
if tt.want.url != "" {
if tt.want.url != "" && !tt.want.postForm {
authUrl, err := url.Parse(got.GetAuthUrl())
require.NoError(t, err)
assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path)
require.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting))
for _, existing := range tt.want.parametersExisting {
@@ -2156,7 +2160,15 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
}
}
if tt.want.postForm {
assert.NotEmpty(t, got.GetPostForm())
assert.Equal(t, tt.want.url, got.GetFormData().GetUrl())
require.Len(t, got.GetFormData().GetFields(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting))
for _, existing := range tt.want.parametersExisting {
assert.Contains(t, got.GetFormData().GetFields(), existing)
}
for key, equal := range tt.want.parametersEqual {
assert.Equal(t, got.GetFormData().GetFields()[key], equal)
}
}
integration.AssertDetails(t, &user.StartIdentityProviderIntentResponse{
Details: tt.want.details,

View File

@@ -52,19 +52,28 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re
if err != nil {
return nil, err
}
content, redirect := session.GetAuth(ctx)
if redirect {
auth, err := session.GetAuth(ctx)
if err != nil {
return nil, err
}
switch a := auth.(type) {
case *idp.RedirectAuth:
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: a.RedirectURL},
}, nil
case *idp.FormAuth:
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_FormData{
FormData: &user.FormData{
Url: a.URL,
Fields: a.Fields,
},
},
}, nil
}
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
PostForm: []byte(content),
},
}, nil
return nil, zerrors.ThrowInvalidArgumentf(nil, "USERv2-3g2j3", "type oneOf %T in method StartIdentityProviderIntent not implemented", auth)
}
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {

View File

@@ -2058,7 +2058,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "http://" + Instance.Domain + ":8000/sso",
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,
@@ -2082,7 +2082,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
url: "http://" + Instance.Domain + ":8000/sso",
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
},
wantErr: false,
@@ -2106,7 +2106,9 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.ID(),
},
postForm: true,
url: "http://localhost:8000/sso",
parametersExisting: []string{"RelayState", "SAMLRequest"},
postForm: true,
},
wantErr: false,
},
@@ -2120,9 +2122,11 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
}
require.NoError(t, err)
if tt.want.url != "" {
if tt.want.url != "" && !tt.want.postForm {
authUrl, err := url.Parse(got.GetAuthUrl())
require.NoError(t, err)
assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path)
require.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting))
for _, existing := range tt.want.parametersExisting {
@@ -2133,7 +2137,15 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
}
}
if tt.want.postForm {
assert.NotEmpty(t, got.GetPostForm())
assert.Equal(t, tt.want.url, got.GetFormData().GetUrl())
require.Len(t, got.GetFormData().GetFields(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting))
for _, existing := range tt.want.parametersExisting {
assert.Contains(t, got.GetFormData().GetFields(), existing)
}
for key, equal := range tt.want.parametersEqual {
assert.Equal(t, got.GetFormData().GetFields()[key], equal)
}
}
integration.AssertDetails(t, &user.StartIdentityProviderIntentResponse{
Details: tt.want.details,

View File

@@ -380,19 +380,28 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re
if err != nil {
return nil, err
}
content, redirect := session.GetAuth(ctx)
if redirect {
auth, err := session.GetAuth(ctx)
if err != nil {
return nil, err
}
switch a := auth.(type) {
case *idp.RedirectAuth:
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: a.RedirectURL},
}, nil
case *idp.FormAuth:
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_FormData{
FormData: &user.FormData{
Url: a.URL,
Fields: a.Fields,
},
},
}, nil
}
return &user.StartIdentityProviderIntentResponse{
Details: object.DomainToDetailsPb(details),
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
PostForm: []byte(content),
},
}, nil
return nil, zerrors.ThrowInvalidArgumentf(nil, "USERv2-3g2j3", "type oneOf %T in method StartIdentityProviderIntent not implemented", auth)
}
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {

View File

@@ -48,6 +48,18 @@ const (
tmplExternalNotFoundOption = "externalnotfoundoption"
)
var (
samlFormPost = template.Must(template.New("saml-post-form").Parse(`<!DOCTYPE html><html><body>
<form method="post" action="{{.URL}}" id="SAMLRequestForm">
{{range $key, $value := .Fields}}
<input type="hidden" name="{{$key}}" value="{{$value}}" />
{{end}}
<input id="SAMLSubmitButton" type="submit" value="Submit" />
</form>
<script>document.getElementById('SAMLSubmitButton').style.visibility="hidden";document.getElementById('SAMLRequestForm').submit();</script>
</body></html>`))
)
type externalIDPData struct {
IDPConfigID string `schema:"idpConfigID"`
}
@@ -201,15 +213,21 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
l.externalAuthFailed(w, r, authReq, err)
return
}
content, redirect := session.GetAuth(r.Context())
if redirect {
http.Redirect(w, r, content, http.StatusFound)
auth, err := session.GetAuth(r.Context())
if err != nil {
l.renderInternalError(w, r, authReq, err)
return
}
_, err = w.Write([]byte(content))
if err != nil {
l.renderError(w, r, authReq, err)
switch a := auth.(type) {
case *idp.RedirectAuth:
http.Redirect(w, r, a.RedirectURL, http.StatusFound)
return
case *idp.FormAuth:
err = samlFormPost.Execute(w, a)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
return
}
}

View File

@@ -432,9 +432,8 @@ func TestCommands_AuthFromProvider(t *testing.T) {
samlRootURL string
}
type res struct {
content string
redirect bool
err error
auth idp.Auth
err error
}
tests := []struct {
name string
@@ -579,8 +578,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
callbackURL: "url",
},
res{
content: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=id",
redirect: true,
auth: &idp.RedirectAuth{RedirectURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=id"},
},
},
{
@@ -671,8 +669,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
callbackURL: "url",
},
res{
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=id",
redirect: true,
auth: &idp.RedirectAuth{RedirectURL: "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=id"},
},
},
}
@@ -686,13 +683,12 @@ func TestCommands_AuthFromProvider(t *testing.T) {
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL)
require.ErrorIs(t, err, tt.res.err)
var content string
var redirect bool
var got idp.Auth
if err == nil {
content, redirect = session.GetAuth(tt.args.ctx)
got, err = session.GetAuth(tt.args.ctx)
assert.Equal(t, tt.res.auth, got)
assert.NoError(t, err)
}
assert.Equal(t, tt.res.redirect, redirect)
assert.Equal(t, tt.res.content, content)
})
}
}
@@ -811,6 +807,97 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
},
},
},
{
"saml post auth",
fields{
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
eventstore: expectEventstore(
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-POST\" 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,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
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-POST\" 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,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
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", "instance").Aggregate,
success,
failure,
"idp",
nil,
)
}(),
),
),
expectRandomPush(
[]eventstore.Command{
idpintent.NewSAMLRequestEvent(
context.Background(),
&idpintent.NewAggregate("id", "instance").Aggregate,
"request",
),
},
),
),
idGenerator: mock.ExpectID(t, "id"),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
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) {
@@ -822,16 +909,30 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL)
require.ErrorIs(t, err, tt.res.err)
content, _ := session.GetAuth(tt.args.ctx)
authURL, err := url.Parse(content)
auth, err := session.GetAuth(tt.args.ctx)
require.NoError(t, err)
var authURL *url.URL
authFields := make(map[string]string)
switch a := auth.(type) {
case *idp.RedirectAuth:
authURL, err = url.Parse(a.RedirectURL)
for key, values := range authURL.Query() {
authFields[key] = values[0]
}
require.NoError(t, err)
case *idp.FormAuth:
authURL, err = url.Parse(a.URL)
require.NoError(t, err)
authFields = a.Fields
}
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))
assert.Contains(t, authFields, k)
if v != "" {
assert.Equal(t, v, query.Get(k))
assert.Equal(t, v, authFields[k])
}
}
})

View File

@@ -62,10 +62,10 @@ func TestProvider_BeginAuth(t *testing.T) {
ctx := context.Background()
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
content, redirect := session.GetAuth(ctx)
contentExpected, redirectExpected := tt.want.GetAuth(ctx)
a.Equal(redirectExpected, redirect)
a.Equal(contentExpected, content)
auth, err := session.GetAuth(ctx)
authExpected, errExpected := tt.want.GetAuth(ctx)
a.ErrorIs(err, errExpected)
a.Equal(authExpected, auth)
})
}
}

View File

@@ -81,10 +81,10 @@ func TestProvider_BeginAuth(t *testing.T) {
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
wantHeaders, wantContent := tt.want.GetAuth(ctx)
gotHeaders, gotContent := session.GetAuth(ctx)
a.Equal(wantHeaders, gotHeaders)
a.Equal(wantContent, gotContent)
wantAuth, wantErr := tt.want.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
})
}
}

View File

@@ -28,7 +28,7 @@ func NewSession(provider *Provider, code string) *Session {
}
// GetAuth implements the [idp.Provider] interface by calling the wrapped [oauth.Session].
func (s *Session) GetAuth(ctx context.Context) (content string, redirect bool) {
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
return s.oauth().GetAuth(ctx)
}

View File

@@ -48,10 +48,10 @@ func TestProvider_BeginAuth(t *testing.T) {
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
wantHeaders, wantContent := tt.want.GetAuth(ctx)
gotHeaders, gotContent := session.GetAuth(ctx)
a.Equal(wantHeaders, gotHeaders)
a.Equal(wantContent, gotContent)
wantAuth, wantErr := tt.want.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
})
}
}

View File

@@ -59,10 +59,10 @@ func TestProvider_BeginAuth(t *testing.T) {
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
wantHeaders, wantContent := tt.want.GetAuth(ctx)
gotHeaders, gotContent := session.GetAuth(ctx)
a.Equal(wantHeaders, gotHeaders)
a.Equal(wantContent, gotContent)
wantAuth, wantErr := tt.want.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
})
}
}

View File

@@ -48,10 +48,10 @@ func TestProvider_BeginAuth(t *testing.T) {
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
wantHeaders, wantContent := tt.want.GetAuth(ctx)
gotHeaders, gotContent := session.GetAuth(ctx)
a.Equal(wantHeaders, gotHeaders)
a.Equal(wantContent, gotContent)
wantAuth, wantErr := tt.want.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
})
}
}

View File

@@ -119,10 +119,10 @@ func TestProvider_BeginAuth(t *testing.T) {
}
if tt.want.err == nil {
a.NoError(err)
wantHeaders, wantContent := tt.want.session.GetAuth(ctx)
gotHeaders, gotContent := session.GetAuth(ctx)
a.Equal(wantHeaders, gotHeaders)
a.Equal(wantContent, gotContent)
wantAuth, wantErr := tt.want.session.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
}
})
}

View File

@@ -42,7 +42,7 @@ func NewSessionFromRequest(provider *Provider, r *http.Request) *Session {
}
// GetAuth implements the [idp.Session] interface.
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
return idp.Redirect(s.AuthURL)
}

View File

@@ -39,7 +39,7 @@ func NewSession(provider *Provider, username, password string) *Session {
}
// GetAuth implements the [idp.Session] interface.
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
return idp.Redirect(s.loginUrl)
}

View File

@@ -80,10 +80,10 @@ func TestProvider_BeginAuth(t *testing.T) {
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
wantHeaders, wantContent := tt.want.GetAuth(ctx)
gotHeaders, gotContent := session.GetAuth(ctx)
a.Equal(wantHeaders, gotHeaders)
a.Equal(wantContent, gotContent)
wantAuth, wantErr := tt.want.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
})
}
}

View File

@@ -37,7 +37,7 @@ func NewSession(provider *Provider, code string, idpArguments map[string]any) *S
}
// GetAuth implements the [idp.Session] interface.
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
return idp.Redirect(s.AuthURL)
}

View File

@@ -98,10 +98,10 @@ func TestProvider_BeginAuth(t *testing.T) {
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
wantHeaders, wantContent := tt.want.GetAuth(ctx)
gotHeaders, gotContent := session.GetAuth(ctx)
a.Equal(wantHeaders, gotHeaders)
a.Equal(wantContent, gotContent)
wantAuth, wantErr := tt.want.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
})
}
}

View File

@@ -33,7 +33,7 @@ func NewSession(provider *Provider, code string, idpArguments map[string]any) *S
}
// GetAuth implements the [idp.Session] interface.
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
return idp.Redirect(s.AuthURL)
}

View File

@@ -1,7 +1,9 @@
package saml
import (
"context"
"encoding/xml"
"net/url"
"testing"
"time"
@@ -11,10 +13,138 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestProvider_BeginAuth(t *testing.T) {
requestTracker := requesttracker.New(
func(ctx context.Context, authRequestID, samlRequestID string) error {
assert.Equal(t, "state", authRequestID)
return nil
},
func(ctx context.Context, authRequestID string) (*samlsp.TrackedRequest, error) {
return &samlsp.TrackedRequest{
SAMLRequestID: "state",
Index: authRequestID,
}, nil
},
)
type fields struct {
name string
rootURL string
metadata []byte
certificate []byte
key []byte
options []ProviderOpts
}
type args struct {
state string
}
type want struct {
err func(error) bool
authType idp.Auth
ssoURL string
relayState string
}
tests := []struct {
name string
fields fields
args args
want want
}{
{
name: "redirect binding, success",
fields: fields{
name: "saml",
rootURL: "https://localhost:8080",
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>"),
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"),
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"),
options: []ProviderOpts{
WithCustomRequestTracker(requestTracker),
},
},
args: args{
state: "state",
},
want: want{
authType: &idp.RedirectAuth{},
ssoURL: "http://localhost:8000/sso",
relayState: "state",
},
},
{
name: "post binding, success",
fields: fields{
name: "saml",
rootURL: "https://localhost:8080",
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-POST\" 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>"),
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"),
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"),
options: []ProviderOpts{
WithCustomRequestTracker(requestTracker),
},
},
args: args{
state: "state",
},
want: want{
authType: &idp.FormAuth{},
ssoURL: "http://localhost:8000/sso",
relayState: "state",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
provider, err := New(
tt.fields.name,
tt.fields.rootURL,
tt.fields.metadata,
tt.fields.certificate,
tt.fields.key,
tt.fields.options...,
)
require.NoError(t, err)
ctx := context.Background()
session, err := provider.BeginAuth(ctx, tt.args.state, nil)
if tt.want.err != nil && !tt.want.err(err) {
a.Fail("invalid error", err)
}
if tt.want.err == nil {
a.NoError(err)
gotAuth, gotErr := session.GetAuth(ctx)
a.NoError(gotErr)
a.IsType(tt.want.authType, gotAuth)
var ssoURL, relayState, samlRequest string
switch auth := gotAuth.(type) {
case *idp.RedirectAuth:
gotRedirect, err := url.Parse(auth.RedirectURL)
a.NoError(err)
gotQuery := gotRedirect.Query()
ssoURL = gotRedirect.Scheme + "://" + gotRedirect.Host + gotRedirect.Path
relayState = gotQuery.Get("RelayState")
samlRequest = gotQuery.Get("SAMLRequest")
case *idp.FormAuth:
ssoURL = auth.URL
relayState = auth.Fields["RelayState"]
samlRequest = auth.Fields["SAMLRequest"]
}
a.Equal(tt.want.ssoURL, ssoURL)
a.Equal(tt.want.relayState, relayState)
a.NotEmpty(samlRequest)
}
})
}
}
func TestProvider_Options(t *testing.T) {
type fields struct {
name string

View File

@@ -1,13 +1,14 @@
package saml
import (
"bytes"
"context"
"encoding/base64"
"errors"
"net/http"
"net/url"
"time"
"github.com/beevik/etree"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
@@ -43,22 +44,15 @@ func NewSession(provider *Provider, requestID string, request *http.Request) (*S
}
// GetAuth implements the [idp.Session] interface.
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
url, _ := url.Parse(s.state)
resp := NewTempResponseWriter()
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
url, err := url.Parse(s.state)
if err != nil {
return nil, err
}
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())
return s.auth(request.WithContext(ctx))
}
// PersistentParameters implements the [idp.Session] interface.
@@ -130,24 +124,57 @@ func (s *Session) transientMappingID() (string, error) {
return "", zerrors.ThrowInvalidArgument(nil, "SAML-swwg2", "Errors.Intent.MissingSingleMappingAttribute")
}
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{}),
// auth is a modified copy of the [samlsp.Middleware.HandleStartAuthFlow] method.
// Instead of writing the response to the http.ResponseWriter, it returns the auth request as an [idp.Auth].
// In case of an error, it returns the error directly and does not write to the response.
func (s *Session) auth(r *http.Request) (idp.Auth, error) {
if r.URL.Path == s.ServiceProvider.ServiceProvider.AcsURL.Path {
// should never occur, but was handled in the original method, so we keep it here
return nil, zerrors.ThrowInvalidArgument(nil, "SAML-Eoi24", "don't wrap Middleware with RequireAccount")
}
var binding, bindingLocation string
if s.ServiceProvider.Binding != "" {
binding = s.ServiceProvider.Binding
bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding)
} else {
binding = saml.HTTPRedirectBinding
bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding)
if bindingLocation == "" {
binding = saml.HTTPPostBinding
bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding)
}
}
authReq, err := s.ServiceProvider.ServiceProvider.MakeAuthenticationRequest(bindingLocation, binding, s.ServiceProvider.ResponseBinding)
if err != nil {
return nil, err
}
relayState, err := s.ServiceProvider.RequestTracker.TrackRequest(nil, r, authReq.ID)
if err != nil {
return nil, err
}
if binding == saml.HTTPRedirectBinding {
redirectURL, err := authReq.Redirect(relayState, &s.ServiceProvider.ServiceProvider)
if err != nil {
return nil, err
}
return idp.Redirect(redirectURL.String())
}
if binding == saml.HTTPPostBinding {
doc := etree.NewDocument()
doc.SetRoot(authReq.Element())
reqBuf, err := doc.WriteToBytes()
if err != nil {
return nil, err
}
encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf)
return idp.Form(authReq.Destination,
map[string]string{
"SAMLRequest": encodedReqBuf,
"RelayState": relayState,
})
}
return nil, zerrors.ThrowInvalidArgument(nil, "SAML-Eoi24", "Errors.Intent.Invalid")
}

View File

@@ -7,12 +7,29 @@ import (
// Session is the minimal implementation for a session of a 3rd party authentication [Provider]
type Session interface {
GetAuth(ctx context.Context) (content string, redirect bool)
GetAuth(ctx context.Context) (Auth, error)
PersistentParameters() map[string]any
FetchUser(ctx context.Context) (User, error)
ExpiresAt() time.Time
}
type Auth interface {
auth()
}
type RedirectAuth struct {
RedirectURL string
}
func (r *RedirectAuth) auth() {}
type FormAuth struct {
URL string
Fields map[string]string
}
func (f *FormAuth) auth() {}
// SessionSupportsMigration is an optional extension to the Session interface.
// It can be implemented to support migrating users, were the initial external id has changed because of a migration of the Provider type.
// E.g. when a user was linked on a generic OIDC provider and this provider has now been migrated to an AzureAD provider.
@@ -22,10 +39,13 @@ type SessionSupportsMigration interface {
RetrievePreviousID() (previousID string, err error)
}
func Redirect(redirectURL string) (string, bool) {
return redirectURL, true
func Redirect(redirectURL string) (*RedirectAuth, error) {
return &RedirectAuth{RedirectURL: redirectURL}, nil
}
func Form(html string) (string, bool) {
return html, false
func Form(url string, fields map[string]string) (*FormAuth, error) {
return &FormAuth{
URL: url,
Fields: fields,
}, nil
}

View File

@@ -162,3 +162,21 @@ message IDPLink {
}
];
}
message FormData {
// The URL to which the form should be submitted using the POST method.
string url = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://idp.com/saml/v2/acs\"";
}
];
// The form fields to be submitted.
// Each field is represented as a key-value pair, where the key is the field / input name
// and the value is the field / input value.
// All fields need to be submitted as is and as input type "text".
map<string, string> fields = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"relayState\":\"state\",\"SAMLRequest\":\"asjfkj3ir2fj248=\"}";
}
];
}

View File

@@ -2895,11 +2895,15 @@ message StartIdentityProviderIntentResponse{
description: "IDP Intent information"
}
];
// POST call information
// Deprecated: Use form_data instead
bytes post_form = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "POST call information"
}
];
// Data for a form POST call
FormData form_data = 5;
}
}

View File

@@ -162,3 +162,21 @@ message IDPLink {
}
];
}
message FormData {
// The URL to which the form should be submitted using the POST method.
string url = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://idp.com/saml/v2/acs\"";
}
];
// The form fields to be submitted.
// Each field is represented as a key-value pair, where the key is the field / input name
// and the value is the field / input value.
// All fields need to be submitted as is and as input type "text".
map<string, string> fields = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"relayState\":\"state\",\"SAMLRequest\":\"asjfkj3ir2fj248=\"}";
}
];
}

View File

@@ -1788,22 +1788,23 @@ message StartIdentityProviderIntentRequest{
message StartIdentityProviderIntentResponse{
zitadel.object.v2beta.Details details = 1;
oneof next_step {
// URL to which the client should redirect
string auth_url = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "URL to which the client should redirect"
example: "\"https://accounts.google.com/o/oauth2/v2/auth?client_id=clientID&callback=https%3A%2F%2Fzitadel.cloud%2Fidps%2Fcallback\"";
}
];
IDPIntent idp_intent = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "IDP Intent information"
}
];
// IDP Intent information
IDPIntent idp_intent = 3;
// POST call information
// Deprecated: Use form_data instead
bytes post_form = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "POST call information"
}
];
// Data for a form POST call
FormData form_data = 5;
}
}