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

@@ -257,14 +257,16 @@ func (s *Service) HandleAssertion(
return NewEmailDomainMismatchError(email, config.EmailDomain)
}
samlSubject := strings.TrimSpace(assertion.Subject.NameID.Value)
err = identity.LoadByEmail(ctx, tx, email)
if err == coredata.ErrResourceNotFound && !config.AutoSignupEnabled {
if errors.Is(err, coredata.ErrResourceNotFound) && !config.AutoSignupEnabled {
return NewSAMLAutoSignupDisabledError(config.ID)
} else if err == coredata.ErrResourceNotFound && config.AutoSignupEnabled {
} else if errors.Is(err, coredata.ErrResourceNotFound) && config.AutoSignupEnabled {
*identity = coredata.Identity{
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
EmailAddress: email,
SAMLSubject: &assertion.Subject.NameID.Value,
SAMLSubject: &samlSubject,
FullName: fullname,
HashedPassword: nil,
EmailAddressVerified: true,
@@ -272,8 +274,11 @@ func (s *Service) HandleAssertion(
UpdatedAt: now,
}
err := identity.Insert(ctx, tx)
if err != nil {
if err := identity.Insert(ctx, tx); err != nil {
if errors.Is(err, coredata.ErrSAMLSubjectAlreadyExists) {
return NewSAMLSubjectAlreadyInUseError(assertion.ID)
}
return fmt.Errorf("cannot insert identity: %w", err)
}
} else if err != nil {
@@ -282,16 +287,18 @@ func (s *Service) HandleAssertion(
identity.EmailAddress = email
identity.FullName = fullname
// Identity can exist (e.g. provisioned via SCIM) but not have a SAML subject
if identity.SAMLSubject == nil {
identity.SAMLSubject = &assertion.Subject.NameID.Value
if !hasSAMLSubject(identity) {
identity.SAMLSubject = &samlSubject
}
identity.EmailAddressVerified = true
identity.UpdatedAt = now
err = identity.Update(ctx, tx)
if err != nil {
if err = identity.Update(ctx, tx); err != nil {
if errors.Is(err, coredata.ErrSAMLSubjectAlreadyExists) {
return NewSAMLSubjectAlreadyInUseError(assertion.ID)
}
return fmt.Errorf("cannot update identity: %w", err)
}
}
@@ -415,6 +422,10 @@ func (s *Service) validateAssertion(assertion *saml.Assertion, config *coredata.
return fmt.Errorf("subject or NameID missing")
}
if strings.TrimSpace(assertion.Subject.NameID.Value) == "" {
return NewSAMLSubjectRequiredError()
}
if assertion.Issuer.Value != config.IdPEntityID {
return fmt.Errorf("assertion issuer %q does not match expected issuer %q",
assertion.Issuer.Value, config.IdPEntityID)
@@ -479,3 +490,7 @@ func (s *Service) baseServiceProvider() *saml.ServiceProvider {
AllowIDPInitiated: true,
}
}
func hasSAMLSubject(identity *coredata.Identity) bool {
return identity.SAMLSubject != nil && strings.TrimSpace(*identity.SAMLSubject) != ""
}