Summary

Multi-tenant SaaS applications using SAML SSO often fail to bind users to their authenticating Identity Provider (IdP). When a user authenticates, the application validates the SAML signature and extracts the email from the assertion, but does not verify that the user should be authenticated by this specific IdP. This allows tenant administrators who control IdP configuration to impersonate any user that exists within their tenant—potentially including users who belong to other tenants.

Background

SAML authentication relies on trust between a Service Provider (SP) and an Identity Provider (IdP). The SP trusts assertions signed by the IdP’s certificate, and the IdP attests to user identities. Most SAML implementations correctly validate signatures, but many fail to consider a fundamental question: should this IdP be allowed to authenticate this user?

In single-tenant applications, this is rarely an issue—there’s one IdP, it authenticates all users, and only high-level administrators can modify IdP settings. However, if another vulnerability exists that allows unauthorised modification of IdP configuration (such as weak access controls, IDOR, or privilege escalation), the lack of user-IdP binding escalates from a design weakness to full account takeover of any user in the application. IdP configuration is often treated as an administrative setup task rather than a security-critical control, which can lead to insufficient protection.

In multi-tenant applications where each tenant configures their own IdP, the trust model becomes more complex. A tenant administrator can configure any IdP they control, including one that will assert arbitrary email addresses.

SAML Libraries and User-IdP Binding

Common SAML libraries focus on protocol-level validation—signature verification, assertion timing, audience restrictions—but do not provide built-in mechanisms for binding users to their authenticating IdP. This is by design: SAML libraries handle the cryptographic and protocol aspects, while user-IdP association is considered application logic.

Libraries that do not enforce user-IdP binding (application must implement):

Library Language Notes
python3-saml (OneLogin) Python Validates signatures and assertions; returns parsed attributes for application to process
ruby-saml (OneLogin) Ruby Same scope—protocol validation only
passport-saml Node.js Authentication strategy; leaves user creation/lookup to application callback
saml2-js Node.js Parses and validates SAML; user management is application responsibility
Spring Security SAML Java Provides authentication filter; user details service must handle IdP binding
omniauth-saml Ruby OmniAuth strategy; returns identity hash for application processing
django-saml2-auth Python Convenience wrapper; uses get_or_create by email without IdP binding
PySAML2 Python Full SAML2 implementation; user management left to application

Platforms with tenant-scoped identity management:

Some identity platforms and full-stack solutions handle tenant isolation at the platform level, though the specific implementation varies:

  • Okta, Auth0, Azure AD B2C: When used as the identity layer (not just an IdP), these platforms can enforce tenant-scoped identity. However, applications integrating via SAML still need to implement proper user-IdP binding.
  • Keycloak: Supports realm-based multi-tenancy, but applications consuming SAML assertions must still validate that users authenticate through their expected realm/IdP.

The absence of built-in user-IdP binding in SAML libraries is not a vulnerability in the libraries themselves. However, this makes it trivially easy for developers to write vulnerable code—especially when following typical “get user by email” patterns common in tutorials and documentation.

Scenario Context

Consider a multi-tenant project management application (“ProjectHub”) with the following characteristics:

  • Multiple organisations (acme-corp, techstart) each with their own subdomain
  • Tenant administrators can configure their own SAML IdP
  • Users can be members of multiple organisations
  • No IdP binding occurs during user registration—users are not locked to the IdP that initially authenticated them

The authentication flow works as follows:

  1. User visits https://projecthub.local/acme-corp/login
  2. Application redirects to the configured IdP (e.g., Azure AD)
  3. IdP authenticates user and returns a signed SAML assertion
  4. Application validates the signature against the configured IdP certificate
  5. Application extracts the email from the assertion
  6. Application looks up or creates a user by email: User.objects.get_or_create(email=email.lower())
  7. Application issues a JWT session token

The vulnerability exists at step 6. The application does not verify that this user should be authenticated by the IdP that just vouched for them. If a user exists in multiple tenants, and an attacker controls the IdP configuration for one of those tenants, they can impersonate that user across all tenants.

Attack Walkthrough

Prerequisites

This attack requires the ability to configure or change the IdP settings for a tenant in the target application.

Legitimate Login Flow

The login page for acme-corp presents a standard SSO option.

Custom SSO Login Page

Clicking “Continue with SSO” redirects the user to the configured IdP (Microsoft Azure AD).

Redirection to Microsoft

The redirect request initiates the SAML authentication flow.

Initial Redirection

Upon successful authentication, Azure AD returns a signed SAML response.

SAML Response

The decoded response contains two key attributes:

  • displayname: Alex Thompson
  • name: victim@[...].onmicrosoft.com

The response is signed with Azure AD’s certificate, which the application validates against its stored IdP configuration.

<samlp:Response
    Destination="https://projecthub.local/acme-corp/saml/acs"
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">

  <Issuer>https://sts.windows.net/[...tenant-id...]/</Issuer>

  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>

  <Assertion>
    <Issuer>https://sts.windows.net/[...tenant-id...]/</Issuer>

    <Signature>
      <SignatureValue>[...signature...]</SignatureValue>
      <X509Certificate>[...certificate...]</X509Certificate>
    </Signature>

    <Subject>
      <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
        victim@[...].onmicrosoft.com
      </NameID>
    </Subject>

    <Conditions>
      <AudienceRestriction>
        <Audience>https://projecthub.local/acme-corp/saml/metadata</Audience>
      </AudienceRestriction>
    </Conditions>

    <AttributeStatement>
      <Attribute Name="http://schemas.microsoft.com/identity/claims/displayname">
        <AttributeValue>Alex Thompson</AttributeValue>
      </Attribute>
      <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
        <AttributeValue>victim@[...].onmicrosoft.com</AttributeValue>
      </Attribute>
    </AttributeStatement>
  </Assertion>
</samlp:Response>

The SAML response is posted back to the application, which validates the signature and extracts the email claim to generate a JWT session token.

POSTing SAML Response

After successful authentication, the user sees their dashboard.

ACME Dashboard

Hijacking the User

For this demonstration, a second tenant (techstart) is used, with its own administrator logged in.

TechStart Team Management

The target user must exist within the attacker-controlled tenant. This can be achieved by inviting them.

Inviting User to TechStart

The current IdP settings show Microsoft Azure AD as the configured identity provider.

Current Microsoft IdP

A SimpleSAMLphp instance is configured as a rogue IdP. The configuration below creates a user that will assert the victim’s email address when the attacker authenticates with victim:victim123.

[~/Tools/POCS/IdPHijacking/sso-lab]$ cat docker/simplesaml/authsources.php
<?php

$config = [
    'admin' => [
        'core:AdminPassword',
    ],

    'example-userpass' => [
        'exampleauth:UserPass',

        'victim:victim123' => [
            'uid' => ['victim'],
            'email' => ['victim@[...].onmicrosoft.com'],
            'displayName' => ['Alex Thompson'],
        ],
    ],
];

The tenant’s IdP settings are updated to point to the rogue SimpleSAMLphp instance.

Updating IdP to Rogue Vendor

Attempting to log in now redirects to the rogue IdP instead of Microsoft. Authenticating with the hardcoded credentials (victim/victim123) causes the rogue IdP to assert victim@[...].onmicrosoft.com.

Rogue IdP Login

The application accepts this assertion and issues a JWT for the victim’s account.

JWT for Victim Account

Since the application does not bind users to their original IdP, this JWT grants access to all tenants where the victim has membership—including acme-corp.

Access to Other Tenant

Mitigations

Bind users to their authenticating IdP. When a user first authenticates via SAML, store the IdP entity ID (or certificate fingerprint) on the user record. On subsequent authentications, verify the IdP matches.

# On first authentication, bind the user to this IdP
if not user.enrolled_idp_entity_id:
    user.enrolled_idp_entity_id = idp_config.entity_id
    user.save()

# On subsequent authentications, verify the IdP matches
if user.enrolled_idp_entity_id != idp_config.entity_id:
    return HttpResponseForbidden("Identity provider mismatch")

This creates a mapping:

victim@[...].onmicrosoft.com -> https://sts.windows.net/[tenant-id]/

Subsequent authentication attempts from a different IdP are rejected:

victim@[...].onmicrosoft.com + https://simplesaml.attacker.com/ -> DENIED

Additional considerations:

  • Allow IdP migration with verification. Users may legitimately need to change IdPs (company acquisitions, IdP vendor changes). Implement a secure migration flow—for example, requiring email verification to the user’s registered address, MFA confirmation, or explicit admin approval with an audit trail.

  • Scope IdP trust per tenant. Even with user-IdP binding, consider whether assertions from Tenant A’s IdP should ever create sessions with access to Tenant B. Depending on the application’s trust model, tenant-scoped sessions may be more appropriate than global user sessions.

  • Audit IdP configuration changes. Log all changes to IdP settings with details of who made the change and when. Alert on IdP changes for tenants with sensitive data or high-privilege users.