Reject empty SAML NameIDs on login

Empty NameID values were stored as '' and occupied the
unique saml_subject index, causing duplicate-key failures
on later logins. Reject blank NameIDs during assertion
validation, return a clear error when a NameID is already
linked to another account, and stop returning internal
errors from the SAML consume endpoint.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
Sacha Al Himdani
2026-07-24 09:47:09 +02:00
parent e12351e0f4
commit bcd05a2e55
5 changed files with 234 additions and 13 deletions

View File

@@ -32,6 +32,7 @@ import (
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/iam/saml"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
@@ -59,6 +60,52 @@ func (h *SAMLHandler) renderInternalServerError(w http.ResponseWriter) {
httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error"))
}
func (h *SAMLHandler) renderAssertionError(w http.ResponseWriter, r *http.Request, err error) {
if isClientSAMLError(err) {
httpserver.RenderError(w, http.StatusUnauthorized, err)
return
}
h.logger.ErrorCtx(r.Context(), "cannot handle SAML assertion", log.Error(err))
httpserver.RenderError(w, http.StatusUnauthorized, errors.New("authentication failed"))
}
func isClientSAMLError(err error) bool {
if _, ok := errors.AsType[*saml.ErrSAMLConfigurationNotFound](err); ok {
return true
}
if _, ok := errors.AsType[*saml.ErrSAMLDisabled](err); ok {
return true
}
if _, ok := errors.AsType[*saml.ErrInvalidAssertion](err); ok {
return true
}
if _, ok := errors.AsType[*saml.ErrReplayAttackDetected](err); ok {
return true
}
if _, ok := errors.AsType[*saml.ErrEmailDomainMismatch](err); ok {
return true
}
if _, ok := errors.AsType[*saml.ErrSAMLAutoSignupDisabled](err); ok {
return true
}
if _, ok := errors.AsType[*saml.ErrUserInactive](err); ok {
return true
}
if _, ok := errors.AsType[*saml.ErrSAMLSubjectAlreadyInUse](err); ok {
return true
}
return false
}
func (h *SAMLHandler) MetadataHandler(w http.ResponseWriter, r *http.Request) {
metadataXML, err := h.iam.SAMLService.GenerateSpMetadata()
if err != nil {
@@ -101,7 +148,7 @@ func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) {
user, membership, err := h.iam.SAMLService.HandleAssertion(ctx, samlResponse, configID)
if err != nil {
httpserver.RenderError(w, http.StatusUnauthorized, err)
h.renderAssertionError(w, r, err)
return
}