Skip to content

Security: User enumeration via tenant membership check #5

@kusold

Description

@kusold

Description

The error message when a user has no tenant memberships is different from a generic authentication failure, allowing attackers to determine whether an email/identity exists in the system.

Location

// auth/handler.go
func (h *OIDCHandler) CallbackHandler(w http.ResponseWriter, r *http.Request) {
    // ...
    memberships, err := h.store.ListMemberships(r.Context(), userRef.UserID)
    if err != nil {
        http.Error(w, "failed to list memberships", http.StatusInternalServerError)
        return
    }
    if len(memberships) == 0 {
        http.Error(w, "user has no tenant memberships", http.StatusForbidden)  // <-- Enumerates users
        return
    }
    // ...
}

Attack Scenario

  1. Attacker authenticates via OIDC with various emails
  2. If they see "user has no tenant memberships" (403), they know the user exists but isn't provisioned
  3. If they see "authentication failed" (401/500), they know the user doesn't exist

Recommended Fix

Use a consistent error message that doesn't reveal the distinction:

if len(memberships) == 0 {
    slog.Warn("authenticated user has no tenant memberships", 
        "user_id", userRef.UserID, 
        "request_id", requestID)
    http.Error(w, "authentication failed", http.StatusForbidden)
    return
}

Consider also:

  • Provisioning users to a default tenant automatically
  • Requiring admin invitation before users can authenticate

Impact

  • Severity: Low
  • Likelihood: Medium
  • Affected: OAuth callback flow

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions