diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 4eee44ab44..1776c57fcb 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -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, diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 5514b6ef03..fd65d61dfb 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -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) { diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index 250322d66f..7b02f7da70 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -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, diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index 93afbde0aa..49f0c7d9c7 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -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) { diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 6202c38c8b..abd20088ba 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -48,6 +48,18 @@ const ( tmplExternalNotFoundOption = "externalnotfoundoption" ) +var ( + samlFormPost = template.Must(template.New("saml-post-form").Parse(` +
+{{range $key, $value := .Fields}} + +{{end}} + +
+ +`)) +) + 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 } } diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 6cf835f521..e0f4e2ffdb 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -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("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + &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("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + &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]) } } }) diff --git a/internal/idp/providers/apple/apple_test.go b/internal/idp/providers/apple/apple_test.go index f3b7e81a1a..7d1f3a8481 100644 --- a/internal/idp/providers/apple/apple_test.go +++ b/internal/idp/providers/apple/apple_test.go @@ -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) }) } } diff --git a/internal/idp/providers/azuread/azuread_test.go b/internal/idp/providers/azuread/azuread_test.go index 122a70bb07..e46815cc8e 100644 --- a/internal/idp/providers/azuread/azuread_test.go +++ b/internal/idp/providers/azuread/azuread_test.go @@ -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) }) } } diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index 169784fb58..f417897893 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -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) } diff --git a/internal/idp/providers/github/github_test.go b/internal/idp/providers/github/github_test.go index 6274b51841..42f03c050d 100644 --- a/internal/idp/providers/github/github_test.go +++ b/internal/idp/providers/github/github_test.go @@ -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) }) } } diff --git a/internal/idp/providers/gitlab/gitlab_test.go b/internal/idp/providers/gitlab/gitlab_test.go index 24b813bc81..99b28c5003 100644 --- a/internal/idp/providers/gitlab/gitlab_test.go +++ b/internal/idp/providers/gitlab/gitlab_test.go @@ -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) }) } } diff --git a/internal/idp/providers/google/google_test.go b/internal/idp/providers/google/google_test.go index b95f8eaf9f..b8f31b86e3 100644 --- a/internal/idp/providers/google/google_test.go +++ b/internal/idp/providers/google/google_test.go @@ -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) }) } } diff --git a/internal/idp/providers/jwt/jwt_test.go b/internal/idp/providers/jwt/jwt_test.go index 5756c58e07..aba337d2ee 100644 --- a/internal/idp/providers/jwt/jwt_test.go +++ b/internal/idp/providers/jwt/jwt_test.go @@ -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) } }) } diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 85b164a9c5..0d91986fc9 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -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) } diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index a78dd02d73..6a56cd6132 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -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) } diff --git a/internal/idp/providers/oauth/oauth2_test.go b/internal/idp/providers/oauth/oauth2_test.go index 984315ac1f..93a0dd404f 100644 --- a/internal/idp/providers/oauth/oauth2_test.go +++ b/internal/idp/providers/oauth/oauth2_test.go @@ -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) }) } } diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index c9e175d1cf..27d38b1740 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -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) } diff --git a/internal/idp/providers/oidc/oidc_test.go b/internal/idp/providers/oidc/oidc_test.go index a46f09f13f..86e23f95d2 100644 --- a/internal/idp/providers/oidc/oidc_test.go +++ b/internal/idp/providers/oidc/oidc_test.go @@ -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) }) } } diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index 9e1e55baf5..08e277a9cc 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -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) } diff --git a/internal/idp/providers/saml/saml_test.go b/internal/idp/providers/saml/saml_test.go index 69ff231ccc..5e76e6dcaa 100644 --- a/internal/idp/providers/saml/saml_test.go +++ b/internal/idp/providers/saml/saml_test.go @@ -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("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + 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("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + 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 diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go index e2a1655a26..e1f32209b0 100644 --- a/internal/idp/providers/saml/session.go +++ b/internal/idp/providers/saml/session.go @@ -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") } diff --git a/internal/idp/session.go b/internal/idp/session.go index fc593eb820..d0df3415bf 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -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 } diff --git a/proto/zitadel/user/v2/idp.proto b/proto/zitadel/user/v2/idp.proto index 73e633fb67..828a035c29 100644 --- a/proto/zitadel/user/v2/idp.proto +++ b/proto/zitadel/user/v2/idp.proto @@ -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 fields = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"relayState\":\"state\",\"SAMLRequest\":\"asjfkj3ir2fj248=\"}"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 79f66266bc..349f3c6c54 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -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; } } diff --git a/proto/zitadel/user/v2beta/idp.proto b/proto/zitadel/user/v2beta/idp.proto index 7d58ec5363..237c8de114 100644 --- a/proto/zitadel/user/v2beta/idp.proto +++ b/proto/zitadel/user/v2beta/idp.proto @@ -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 fields = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"relayState\":\"state\",\"SAMLRequest\":\"asjfkj3ir2fj248=\"}"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index f877252f51..bcb091abf2 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -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; } }