6 minutes
IdP Hijacking: SAML Binding Flaws in Multi-Tenant Apps
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:
- User visits
https://projecthub.local/acme-corp/login - Application redirects to the configured IdP (e.g., Azure AD)
- IdP authenticates user and returns a signed SAML assertion
- Application validates the signature against the configured IdP certificate
- Application extracts the email from the assertion
- Application looks up or creates a user by email:
User.objects.get_or_create(email=email.lower()) - 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.

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

The redirect request initiates the SAML authentication flow.

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

The decoded response contains two key attributes:
displayname:Alex Thompsonname: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.

After successful authentication, the user sees their dashboard.

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

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

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

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.

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.

The application accepts this assertion and issues a JWT for the victim’s 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.

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.