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 <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
committed by
Cursor Agent
parent
b8e5d279a0
commit
00ed11e30b
@@ -74,48 +74,6 @@ function useAuthErrorContent(code: string | null): AuthErrorContent {
|
|||||||
"This magic link is invalid. Please request a new one.",
|
"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:
|
default:
|
||||||
return {
|
return {
|
||||||
title: __("Authentication failed"),
|
title: __("Authentication failed"),
|
||||||
|
|||||||
@@ -36,12 +36,6 @@ const (
|
|||||||
authErrorMagicLinkExpired = "magic_link_expired"
|
authErrorMagicLinkExpired = "magic_link_expired"
|
||||||
authErrorMagicLinkAlreadyUsed = "magic_link_already_used"
|
authErrorMagicLinkAlreadyUsed = "magic_link_already_used"
|
||||||
authErrorMagicLinkInvalid = "magic_link_invalid"
|
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) {
|
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)
|
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) {
|
func authErrorCodeFromSAML(err error) (string, bool) {
|
||||||
if _, ok := errors.AsType[*saml.ErrSAMLDisabled](err); ok {
|
if _, ok := errors.AsType[*saml.ErrSAMLDisabled](err); ok {
|
||||||
return authErrorSAMLDisabled, true
|
return authErrorAuthenticationFailed, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := errors.AsType[*saml.ErrSAMLConfigurationNotFound](err); ok {
|
if _, ok := errors.AsType[*saml.ErrSAMLConfigurationNotFound](err); ok {
|
||||||
return authErrorSAMLConfigurationNotFound, true
|
return authErrorAuthenticationFailed, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := errors.AsType[*saml.ErrEmailDomainMismatch](err); ok {
|
if _, ok := errors.AsType[*saml.ErrEmailDomainMismatch](err); ok {
|
||||||
return authErrorSAMLEmailDomainMismatch, true
|
return authErrorAuthenticationFailed, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := errors.AsType[*saml.ErrSAMLAutoSignupDisabled](err); ok {
|
if _, ok := errors.AsType[*saml.ErrSAMLAutoSignupDisabled](err); ok {
|
||||||
return authErrorSAMLAutoSignupDisabled, true
|
return authErrorAuthenticationFailed, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := errors.AsType[*saml.ErrUserInactive](err); ok {
|
if _, ok := errors.AsType[*saml.ErrUserInactive](err); ok {
|
||||||
return authErrorSAMLUserInactive, true
|
return authErrorAuthenticationFailed, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := errors.AsType[*saml.ErrSAMLSubjectAlreadyInUse](err); ok {
|
if _, ok := errors.AsType[*saml.ErrSAMLSubjectAlreadyInUse](err); ok {
|
||||||
return authErrorSAMLSubjectAlreadyInUse, true
|
return authErrorAuthenticationFailed, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := errors.AsType[*saml.ErrInvalidAssertion](err); ok {
|
if _, ok := errors.AsType[*saml.ErrInvalidAssertion](err); ok {
|
||||||
|
|||||||
@@ -63,49 +63,49 @@ func TestAuthErrorCodeFromSAML(t *testing.T) {
|
|||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "disabled",
|
name: "disabled stays generic",
|
||||||
err: saml.NewSAMLDisabledError(),
|
err: saml.NewSAMLDisabledError(),
|
||||||
code: authErrorSAMLDisabled,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "configuration not found",
|
name: "configuration not found stays generic",
|
||||||
err: saml.NewSAMLConfigurationNotFoundError(configID),
|
err: saml.NewSAMLConfigurationNotFoundError(configID),
|
||||||
code: authErrorSAMLConfigurationNotFound,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "email domain mismatch",
|
name: "email domain mismatch stays generic",
|
||||||
err: saml.NewEmailDomainMismatchError(email, "acme.com"),
|
err: saml.NewEmailDomainMismatchError(email, "acme.com"),
|
||||||
code: authErrorSAMLEmailDomainMismatch,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "auto signup disabled",
|
name: "auto signup disabled stays generic",
|
||||||
err: saml.NewSAMLAutoSignupDisabledError(configID),
|
err: saml.NewSAMLAutoSignupDisabledError(configID),
|
||||||
code: authErrorSAMLAutoSignupDisabled,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user inactive",
|
name: "user inactive stays generic",
|
||||||
err: saml.NewUserInactiveError(configID),
|
err: saml.NewUserInactiveError(configID),
|
||||||
code: authErrorSAMLUserInactive,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "subject already in use",
|
name: "subject already in use stays generic",
|
||||||
err: saml.NewSAMLSubjectAlreadyInUseError("assertion-1"),
|
err: saml.NewSAMLSubjectAlreadyInUseError("assertion-1"),
|
||||||
code: authErrorSAMLSubjectAlreadyInUse,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid assertion maps to generic failure",
|
name: "invalid assertion stays generic",
|
||||||
err: saml.NewInvalidAssertionError("assertion-1", errors.New("bad signature")),
|
err: saml.NewInvalidAssertionError("assertion-1", errors.New("bad signature")),
|
||||||
code: authErrorAuthenticationFailed,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "replay maps to generic failure",
|
name: "replay stays generic",
|
||||||
err: saml.NewReplayAttackDetectedError("assertion-1"),
|
err: saml.NewReplayAttackDetectedError("assertion-1"),
|
||||||
code: authErrorAuthenticationFailed,
|
code: authErrorAuthenticationFailed,
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user