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(`
+
+
+`))
+)
+
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;
}
}