A multi-tenant “Sign in with Microsoft” button trusted the wrong field. Because Entra’s mail attribute isn’t verified and isn’t unique, anyone with a free Microsoft 365 developer tenant could mint a login for someone else’s account with no password, no 2FA, no click from the victim.
The Azure/Entra SSO login flow POST /api/o365s/azure-auth resolved the authenticating Redacted company user from the email claim returned by Microsoft Graph’s /oidc/userinfo, and never checked which Entra tenant issued the token.
The app registration was multi-tenant, pointed at
https://login.microsoftonline.com/common/. That means a token minted by any Entra tenant including one an attacker spins up for free is accepted, as long as Microsoft’s signature checks out. Combine that with the fact that Entra’s mail attribute is unverified and settable by any tenant owner, and you get a straight line from “attacker registers a free Microsoft 365 developer tenant” to “attacker is logged in as someone else.”
⚠ The Core Problem
Login trusted email (mutable, unverified) instead of tid + oid (immutable, tied to the real tenant). Microsoft’s own docs say email must not be used as an identifier and that’s exactly what this endpoint was doing.

The Attack Chain

Every step here uses legitimate Microsoft infrastructure. Nothing about it looks like an “attack” from Microsoft’s side; the token really is validly signed. The flaw is entirely in what Redacted company chose to trust.
- Confirm the app is still multi-tenant
An unauthenticated GET /api/o365s/azure-sign-in-url returns an authorize URL pointed at /common/ meaning any Entra tenant is accepted, not just Redacted company’s own customers.
- Identify an Entra-linked Redacted company address
GET /api/o365s/pre-sign-in?email= behaves differently for real vs. non-existent accounts an HTTP 500 vs. HTTP 200 split acts as a user-existence oracle, unauthenticated.
- Set the mail attribute in an attacker-owned tenant
In any Entra directory a free trial is enough create a cloud-only user and set its mail attribute to the target’s Redacted company address. Microsoft never checks domain ownership for this field.
- Click “Sign in with Microsoft” — as the attacker
Authenticate normally against the attacker’s own tenant and consent to the Redacted Company multi-tenant app. Microsoft issues a validly signed token containing the spoofed email claim.
- Redacted company resolves the session to the victim
/api/o365s/azure-auth looks the user up by email, finds the victim’s account, and returns a fully authenticated session cookie for them — to the attacker.
Trusting the Wrong Field
OpenID Connect gives you several claims to identify a user. tid (tenant ID) and oid (object ID) are immutable and cryptographically bound to the specific Entra directory that issued the token. Email is neither Microsoft’s own documentation nor is it explicit that it’s not verified and not guaranteed unique, and that it must not be used as an identifier.
This endpoint did exactly that: User.objects.filter(email=email).first(). Paired with a multi-tenant app pointed at /common/, that one line is the entire vulnerability. This is a well-documented pattern — Descope published the “nOAuth” class in 2023, and it keeps showing up because email feels like a reasonable identifier until you remember who’s allowed to set it.

How to Fix It
- Bind identity to tid + oid, not email. After validating the Microsoft signature, require the token’s tid to equal the tenant ID already stored for that user, and persist oid as the immutable subject. Reject on mismatch.
- Stop pointing at /common/ if you don’t need to. If every customer has a known home tenant, use /<tenant_id>/v2.0 or organizations plus an explicit allow-list. Keep the app ID fixed server-side — never take it from the query string.
- Never treat Graph email as a unique identifier. Look users up by the already-linked account row (tid + oid), never by an email filter against the whole user table.
- Don’t let the SSO path skip 2FA or audit logging. The password login path shouldn’t be the only one enforcing 2FA — apply the same check unless the token’s tenant is confirmed to be the user’s home tenant, and log SSO logins the same way password logins are logged.
- Close the unauthenticated existence oracle. Require authentication or rate-limit endpoints like pre-sign-in that currently reveal whether an email is a real account through a status-code difference.
The Bigger Picture
SSO integrations tend to get built once and trusted forever, which is exactly why claims-handling bugs like this one survive so long. The identity provider did its job Microsoft correctly signed a token containing exactly what the attacker’s own tenant said was true. The failure was entirely on the relying-party side, in choosing a mutable, unverified field as the key to someone else’s account.
If a claim can be set by the party you don’t trust, it can’t be the thing you trust to tell you who’s logging in.
