mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 16:37:31 +00:00
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:
@@ -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,6 +2105,8 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Instance.ID(),
|
||||
},
|
||||
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,
|
||||
|
@@ -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 {
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
|
||||
}, nil
|
||||
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_PostForm{
|
||||
PostForm: []byte(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 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) {
|
||||
|
@@ -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,6 +2106,8 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Instance.ID(),
|
||||
},
|
||||
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,
|
||||
|
@@ -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 {
|
||||
return &user.StartIdentityProviderIntentResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
|
||||
}, nil
|
||||
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_PostForm{
|
||||
PostForm: []byte(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 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) {
|
||||
|
@@ -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,17 +213,23 @@ 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))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// handleExternalLoginCallbackForm handles the callback from a IDP with form_post.
|
||||
|
@@ -432,8 +432,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
|
||||
samlRootURL string
|
||||
}
|
||||
type res struct {
|
||||
content string
|
||||
redirect bool
|
||||
auth idp.Auth
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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=\"}";
|
||||
}
|
||||
];
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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=\"}";
|
||||
}
|
||||
];
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user