From e76394af5e9909d33568ac541c0ce669cb7577d7 Mon Sep 17 00:00:00 2001 From: dorsha Date: Mon, 24 Aug 2026 11:58:54 +0300 Subject: [PATCH] feat(sso): add ConfigureAuthType to enable or disable an SSO configuration Wraps POST /v1/mgmt/sso/settings/authtype: descope.SSOAuthTypeNone disables one SSO configuration without deleting it, SSOAuthTypeSaml/SSOAuthTypeOidc enable it on that protocol with its stored settings. Takes an optional ssoID, so a multi-SSO tenant can have a single connection taken out of service and put back without replaying its IdP payload, and without the new ACS URL a delete plus recreate hands a SAML tenant. Requires descope/backend#2355. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 7 ++++ descope/api/client.go | 6 +++ descope/internal/mgmt/sso.go | 21 ++++++++++ descope/internal/mgmt/sso_test.go | 45 ++++++++++++++++++++++ descope/sdk/mgmt.go | 11 ++++++ descope/tests/mocks/mgmt/managementmock.go | 10 +++++ descope/types.go | 9 +++++ 7 files changed, 109 insertions(+) diff --git a/README.md b/README.md index f1ad7c48..ec1e132b 100644 --- a/README.md +++ b/README.md @@ -1289,6 +1289,13 @@ createdSSOSettings, err := descopeClient.Management.SSO().NewSettings(context.Ba // To delete SSO settings, call the following method // You can pass ssoID in case using multi SSO and you want to delete specific SSO configuration err := descopeClient.Management.SSO().DeleteSettings(context.Background(), "tenant-id") + +// To disable an SSO configuration without deleting it, set its auth type to none. Its settings, +// mappings and domains are kept, so re-enabling it needs no payload. +// You can pass ssoID in case using multi SSO and you want to disable a specific SSO configuration +err := descopeClient.Management.SSO().ConfigureAuthType(context.Background(), "tenant-id", descope.SSOAuthTypeNone, ssoID) +// Enable it again on the protocol it is configured for +err = descopeClient.Management.SSO().ConfigureAuthType(context.Background(), "tenant-id", descope.SSOAuthTypeSaml, ssoID) ``` Note: Certificates should have a similar structure to: diff --git a/descope/api/client.go b/descope/api/client.go index 092136a2..c760dd1c 100644 --- a/descope/api/client.go +++ b/descope/api/client.go @@ -183,6 +183,7 @@ var ( ssoLoadSettings: "mgmt/sso/settings", // v2 only ssoLoadAllSettings: "mgmt/sso/settings/all", // v2 only ssoSettingsNew: "mgmt/sso/settings/new", + ssoAuthType: "mgmt/sso/settings/authtype", ssoSAMLSettings: "mgmt/sso/saml", ssoSAMLSettingsByMetadata: "mgmt/sso/saml/metadata", ssoRedirectURL: "mgmt/sso/redirect", @@ -531,6 +532,7 @@ type mgmtEndpoints struct { ssoLoadSettings string ssoLoadAllSettings string ssoSettingsNew string + ssoAuthType string ssoSAMLSettings string ssoSAMLSettingsByMetadata string ssoRedirectURL string @@ -1300,6 +1302,10 @@ func (e *endpoints) ManagementNewSSOSettings() string { return path.Join(e.version, e.mgmt.ssoSettingsNew) } +func (e *endpoints) ManagementSSOAuthType() string { + return path.Join(e.version, e.mgmt.ssoAuthType) +} + func (e *endpoints) ManagementSSOSAMLSettings() string { return path.Join(e.version, e.mgmt.ssoSAMLSettings) } diff --git a/descope/internal/mgmt/sso.go b/descope/internal/mgmt/sso.go index 91032451..8f2af7d0 100644 --- a/descope/internal/mgmt/sso.go +++ b/descope/internal/mgmt/sso.go @@ -263,6 +263,27 @@ func (s *sso) DeleteSettings(ctx context.Context, tenantID string, ssoID string) return nil } +func (s *sso) ConfigureAuthType(ctx context.Context, tenantID string, authType descope.SSOAuthType, ssoID string) error { + if tenantID == "" { + return utils.NewInvalidArgumentError("tenantID") + } + + if authType == "" { + return utils.NewInvalidArgumentError("authType") + } + + req := map[string]any{ + "tenantId": tenantID, + "authType": authType, + } + if len(ssoID) > 0 { + req["ssoId"] = ssoID + } + + _, err := s.client.DoPostRequest(ctx, api.Routes.ManagementSSOAuthType(), req, nil, "") + return err +} + // * Deprecated (use ConfigureSAMLSettings() instead) *// func (s *sso) ConfigureSettings(ctx context.Context, tenantID, idpURL, idpCert, entityID, redirectURL string, domains []string) error { if tenantID == "" { diff --git a/descope/internal/mgmt/sso_test.go b/descope/internal/mgmt/sso_test.go index a906a5e5..c7064cd9 100644 --- a/descope/internal/mgmt/sso_test.go +++ b/descope/internal/mgmt/sso_test.go @@ -99,6 +99,51 @@ func TestDeleteSSOSettingsWithSSOIDSuccess(t *testing.T) { assert.NoError(t, err) } +func TestSSOConfigureAuthTypeDisableWithSSOIDSuccess(t *testing.T) { + mgmt := newTestMgmt(nil, helpers.DoOk(func(r *http.Request) { + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + require.Equal(t, "abc", req["tenantId"]) + require.Equal(t, "none", req["authType"]) + require.Equal(t, "somessoid", req["ssoId"]) + })) + err := mgmt.SSO().ConfigureAuthType(context.Background(), "abc", descope.SSOAuthTypeNone, "somessoid") + require.NoError(t, err) +} + +func TestSSOConfigureAuthTypeDefaultConfigSuccess(t *testing.T) { + mgmt := newTestMgmt(nil, helpers.DoOk(func(r *http.Request) { + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + require.Equal(t, "abc", req["tenantId"]) + require.Equal(t, "saml", req["authType"]) + require.Empty(t, req["ssoId"]) + })) + err := mgmt.SSO().ConfigureAuthType(context.Background(), "abc", descope.SSOAuthTypeSaml, "") + require.NoError(t, err) +} + +func TestSSOConfigureAuthTypeMissingTenantID(t *testing.T) { + called := false + mgmt := newTestMgmt(nil, helpers.DoOk(func(_ *http.Request) { + called = true + })) + err := mgmt.SSO().ConfigureAuthType(context.Background(), "", descope.SSOAuthTypeNone, "") + require.Error(t, err) + require.False(t, called) +} + +func TestSSOConfigureAuthTypeMissingAuthType(t *testing.T) { + called := false + mgmt := newTestMgmt(nil, helpers.DoOk(func(_ *http.Request) { + called = true + })) + err := mgmt.SSO().ConfigureAuthType(context.Background(), "abc", "", "") + require.Error(t, err) + require.False(t, called) +} + func TestDeleteSSOSettingsError(t *testing.T) { called := false mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(_ *http.Request) { diff --git a/descope/sdk/mgmt.go b/descope/sdk/mgmt.go index 96ae38b6..48b2985f 100644 --- a/descope/sdk/mgmt.go +++ b/descope/sdk/mgmt.go @@ -724,6 +724,17 @@ type SSO interface { // ssoID (optional) - you can pass ssoID in case using multi SSO and you want to delete specific SSO configuration DeleteSettings(ctx context.Context, tenantID string, ssoID string) error + // Set the authentication type of a single SSO configuration, leaving its stored SAML/OIDC + // settings, mappings and domains untouched. + // + // tenantID and authType are required. + // + // authType - descope.SSOAuthTypeNone disables the configuration without deleting it, + // descope.SSOAuthTypeSaml / descope.SSOAuthTypeOidc enable it on that protocol with its stored + // settings, so re-enabling needs no payload. + // ssoID (optional) - pass ssoID when using multi SSO to change a specific SSO configuration. + ConfigureAuthType(ctx context.Context, tenantID string, authType descope.SSOAuthType, ssoID string) error + // *** Deprecated *** //* Deprecated (use LoadSettings() instead) *// diff --git a/descope/tests/mocks/mgmt/managementmock.go b/descope/tests/mocks/mgmt/managementmock.go index 35c161b3..dd6d6686 100644 --- a/descope/tests/mocks/mgmt/managementmock.go +++ b/descope/tests/mocks/mgmt/managementmock.go @@ -265,6 +265,9 @@ type MockSSO struct { DeleteSettingsAssert func(tenantID string, ssoID string) DeleteSettingsError error + ConfigureAuthTypeAssert func(tenantID string, authType descope.SSOAuthType, ssoID string) + ConfigureAuthTypeError error + GetSettingsAssert func(tenantID string) GetSettingsResponse *descope.SSOSettingsResponse GetSettingsError error @@ -324,6 +327,13 @@ func (m *MockSSO) ConfigureOIDCSettings(_ context.Context, tenantID string, sett return m.ConfigureOIDCSettingsError } +func (m *MockSSO) ConfigureAuthType(_ context.Context, tenantID string, authType descope.SSOAuthType, ssoID string) error { + if m.ConfigureAuthTypeAssert != nil { + m.ConfigureAuthTypeAssert(tenantID, authType, ssoID) + } + return m.ConfigureAuthTypeError +} + func (m *MockSSO) ConfigureXAASettings(_ context.Context, tenantID string, settings *descope.SSOXAASettings, ssoID string) error { if m.ConfigureXAASettingsAssert != nil { m.ConfigureXAASettingsAssert(tenantID, settings, ssoID) diff --git a/descope/types.go b/descope/types.go index 17b5bb4a..aae297f3 100644 --- a/descope/types.go +++ b/descope/types.go @@ -773,6 +773,15 @@ const RoleInheritanceDefault RoleInheritance = "" const RoleInheritanceNone RoleInheritance = "none" const RoleInheritanceUserOnly RoleInheritance = "userOnly" +// SSOAuthType is the authentication type of an SSO configuration. None means the configuration is +// disabled: it keeps its stored settings, mappings and domains, and serves no logins until it is +// set back to Saml or Oidc. +type SSOAuthType string + +const SSOAuthTypeNone SSOAuthType = "none" +const SSOAuthTypeSaml SSOAuthType = "saml" +const SSOAuthTypeOidc SSOAuthType = "oidc" + type Tenant struct { ID string `json:"id"` Name string `json:"name"`