From 00ed11e30b1487610eb6094d0e1f0addcc38ef01 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Fri, 24 Jul 2026 21:45:31 +0000 Subject: [PATCH] Keep SAML auth failures generic for users Specific SAML refusal reasons leak organization and account state. Still redirect to the shared auth error page, but always use authentication_failed while logging the real cause server-side. Signed-off-by: Cursor Agent Co-authored-by: Bryan FRIMIN --- .../src/pages/iam/auth/AuthErrorPage.tsx | 42 ------------------- pkg/server/api/connect/v1/auth_error.go | 21 ++++------ pkg/server/api/connect/v1/auth_error_test.go | 28 ++++++------- 3 files changed, 23 insertions(+), 68 deletions(-) diff --git a/apps/console/src/pages/iam/auth/AuthErrorPage.tsx b/apps/console/src/pages/iam/auth/AuthErrorPage.tsx index 2ded2946a..1366a0bd0 100644 --- a/apps/console/src/pages/iam/auth/AuthErrorPage.tsx +++ b/apps/console/src/pages/iam/auth/AuthErrorPage.tsx @@ -74,48 +74,6 @@ function useAuthErrorContent(code: string | null): AuthErrorContent { "This magic link is invalid. Please request a new one.", ), }; - case "saml_disabled": - return { - title: __("SSO unavailable"), - description: __( - "Single sign-on is disabled for this organization. Please contact your administrator.", - ), - }; - case "saml_configuration_not_found": - return { - title: __("SSO configuration not found"), - description: __( - "This single sign-on configuration could not be found. Please contact your administrator.", - ), - }; - case "saml_email_domain_mismatch": - return { - title: __("Email domain not allowed"), - description: __( - "Your email domain is not allowed for this organization's single sign-on. Please use an account from the configured domain.", - ), - }; - case "saml_auto_signup_disabled": - return { - title: __("Account not found"), - description: __( - "No account exists for this email, and automatic signup is disabled. Please contact your administrator.", - ), - }; - case "saml_user_inactive": - return { - title: __("Account inactive"), - description: __( - "Your account is inactive. Please contact your administrator.", - ), - }; - case "saml_subject_already_in_use": - return { - title: __("Account already linked"), - description: __( - "This single sign-on identity is already linked to another account. Please contact your administrator.", - ), - }; default: return { title: __("Authentication failed"), diff --git a/pkg/server/api/connect/v1/auth_error.go b/pkg/server/api/connect/v1/auth_error.go index cb7e781e6..253f3789a 100644 --- a/pkg/server/api/connect/v1/auth_error.go +++ b/pkg/server/api/connect/v1/auth_error.go @@ -36,12 +36,6 @@ const ( authErrorMagicLinkExpired = "magic_link_expired" authErrorMagicLinkAlreadyUsed = "magic_link_already_used" authErrorMagicLinkInvalid = "magic_link_invalid" - authErrorSAMLDisabled = "saml_disabled" - authErrorSAMLConfigurationNotFound = "saml_configuration_not_found" - authErrorSAMLEmailDomainMismatch = "saml_email_domain_mismatch" - authErrorSAMLAutoSignupDisabled = "saml_auto_signup_disabled" - authErrorSAMLUserInactive = "saml_user_inactive" - authErrorSAMLSubjectAlreadyInUse = "saml_subject_already_in_use" ) func redirectAuthError(w http.ResponseWriter, r *http.Request, code string) { @@ -56,29 +50,32 @@ func redirectAuthError(w http.ResponseWriter, r *http.Request, code string) { http.Redirect(w, r, redirectURL.String(), http.StatusFound) } +// authErrorCodeFromSAML maps known SAML assertion failures to a browser error +// code. Specific SAML reasons stay server-side only; users see a generic +// authentication failure to avoid leaking org/account state. func authErrorCodeFromSAML(err error) (string, bool) { if _, ok := errors.AsType[*saml.ErrSAMLDisabled](err); ok { - return authErrorSAMLDisabled, true + return authErrorAuthenticationFailed, true } if _, ok := errors.AsType[*saml.ErrSAMLConfigurationNotFound](err); ok { - return authErrorSAMLConfigurationNotFound, true + return authErrorAuthenticationFailed, true } if _, ok := errors.AsType[*saml.ErrEmailDomainMismatch](err); ok { - return authErrorSAMLEmailDomainMismatch, true + return authErrorAuthenticationFailed, true } if _, ok := errors.AsType[*saml.ErrSAMLAutoSignupDisabled](err); ok { - return authErrorSAMLAutoSignupDisabled, true + return authErrorAuthenticationFailed, true } if _, ok := errors.AsType[*saml.ErrUserInactive](err); ok { - return authErrorSAMLUserInactive, true + return authErrorAuthenticationFailed, true } if _, ok := errors.AsType[*saml.ErrSAMLSubjectAlreadyInUse](err); ok { - return authErrorSAMLSubjectAlreadyInUse, true + return authErrorAuthenticationFailed, true } if _, ok := errors.AsType[*saml.ErrInvalidAssertion](err); ok { diff --git a/pkg/server/api/connect/v1/auth_error_test.go b/pkg/server/api/connect/v1/auth_error_test.go index 4e574e5e2..b4c40e835 100644 --- a/pkg/server/api/connect/v1/auth_error_test.go +++ b/pkg/server/api/connect/v1/auth_error_test.go @@ -63,49 +63,49 @@ func TestAuthErrorCodeFromSAML(t *testing.T) { ok bool }{ { - name: "disabled", + name: "disabled stays generic", err: saml.NewSAMLDisabledError(), - code: authErrorSAMLDisabled, + code: authErrorAuthenticationFailed, ok: true, }, { - name: "configuration not found", + name: "configuration not found stays generic", err: saml.NewSAMLConfigurationNotFoundError(configID), - code: authErrorSAMLConfigurationNotFound, + code: authErrorAuthenticationFailed, ok: true, }, { - name: "email domain mismatch", + name: "email domain mismatch stays generic", err: saml.NewEmailDomainMismatchError(email, "acme.com"), - code: authErrorSAMLEmailDomainMismatch, + code: authErrorAuthenticationFailed, ok: true, }, { - name: "auto signup disabled", + name: "auto signup disabled stays generic", err: saml.NewSAMLAutoSignupDisabledError(configID), - code: authErrorSAMLAutoSignupDisabled, + code: authErrorAuthenticationFailed, ok: true, }, { - name: "user inactive", + name: "user inactive stays generic", err: saml.NewUserInactiveError(configID), - code: authErrorSAMLUserInactive, + code: authErrorAuthenticationFailed, ok: true, }, { - name: "subject already in use", + name: "subject already in use stays generic", err: saml.NewSAMLSubjectAlreadyInUseError("assertion-1"), - code: authErrorSAMLSubjectAlreadyInUse, + code: authErrorAuthenticationFailed, ok: true, }, { - name: "invalid assertion maps to generic failure", + name: "invalid assertion stays generic", err: saml.NewInvalidAssertionError("assertion-1", errors.New("bad signature")), code: authErrorAuthenticationFailed, ok: true, }, { - name: "replay maps to generic failure", + name: "replay stays generic", err: saml.NewReplayAttackDetectedError("assertion-1"), code: authErrorAuthenticationFailed, ok: true,