Langfuse has a setting called SSO enforcement, and it has one goal.
Turn it on and nobody at your domain can log in except through your identity provider. Revoke a user in Okta at offboarding, and every other door closes.
That sounds airtight. But it only holds on the login paths that enforce it. And one of those paths didn’t.
You’ve seen the headlines: AI is finding vulnerabilities faster than ever.
In this scenario, it was Maze Code’s AI agents that scanned the Langfuse public repo, and found that the enforcement didn’t hold. One login path, the password reset flow, never checked the rule.
It’s the kind of flaw a person once had to read a whole codebase to catch, and rules-based scanners never could. That’s where LLMs are coming in.
In this blog, we’ll walk through the Langfuse vulnerability, how Maze Code found and validated the vulnerability, and how to tell if you are affected and what to do about it.
Credit to the Langfuse team, who confirmed the issue and shipped a fix in v3.174.0.
Pinpointing the flaw
Langfuse has a setting called AUTH_DOMAINS_WITH_SSO_ENFORCEMENT. An admin sets the setting to a domain (say corp.example) and it means one thing:
Nobody at corp.example can authenticate to Langfuse except through the corporate identity provider.
That is a hard security boundary. It’s what makes MFA, conditional access, device compliance, and session limits binding for that domain. This setting is what IT relies on during offboarding: revoke the user in Okta or Azure AD, and the enforcement setting guarantees no other login path works.
The whole value of the control is the word no. If any path produces a session without the IdP, the guarantee is false.
The vulnerability
Langfuse has three credential-based paths relevant to self-hosted SSO enforcement. The check was wired into two of them.
Password login – enforced. authorize() checks the domain and refuses:
// auth.ts - credentials authorize()
const blockedDomains = getSSOBlockedDomains();
if (domain && blockedDomains.includes(domain)) {
throw new Error("Sign in with email and password is disabled for this domain.");
}
New signup – enforced. Same check in the signup handler.
Email OTP (the password-reset flow) – not enforced. To understand why, you need to know that Langfuse enforces SSO through two separate mechanisms. The first is env-based: getSSOBlockedDomains() reads AUTH_DOMAINS_WITH_SSO_ENFORCEMENT directly. It is called in the credentials provider and the signup handler. The second is database-based: getSsoAuthProviderIdForDomain() checks for a per-domain SSO config stored in the database. This is an Enterprise Edition feature available on Langfuse Cloud.
The env-based check (the one that directly reads AUTH_DOMAINS_WITH_SSO_ENFORCEMENT) was never added to the signIn callback at all. The only SSO-related gate in the email branch was the database check:
// auth.ts - signIn callback
const multiTenantSsoProvider = await getSsoAuthProviderIdForDomain(userDomain);
if (multiTenantSsoProvider && account?.provider !== multiTenantSsoProvider) {
return `…/enterprise-sso-required`; // blocked
}
That looks like enforcement. It isn’t. Not on self-hosted. getSsoAuthProviderIdForDomain depends on multiTenantSsoAvailable, which is:
// multiTenantSsoAvailable.ts
export const multiTenantSsoAvailable = Boolean(
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION, // unset on every self-hosted instance
);
On a self-hosted install that environment variable is never set. So multiTenantSsoAvailable is false, the function returns null, the if is never entered, and execution falls straight through to:
// auth.ts - signIn callback, email branch
if (account?.provider === "email") {
const user = await prisma.user.findUnique({ where: { email } });
if (user) {
return true; // session granted - the enforced-domain list was never consulted
}
}
That is the bug. Two layers of failure compounding each other. The env-based check (getSSOBlockedDomains) was simply never added to this path. The database-based check that was present looked like coverage but was permanently inactive on self-hosted: NEXT_PUBLIC_LANGFUSE_CLOUD_REGION is a cloud-only variable. On self-hosted, the OTP path had no domain enforcement at all.
There was even a developer comment in the file claiming the reset flow was covered. It was wrong for self-hosted. The check it referred to was the cloud-only one.
Why rule based scanners miss this
This is correctly written code. To find this bug you have to understand four things that aren’t in the code file:
- Intent.
AUTH_DOMAINS_WITH_SSO_ENFORCEMENTis a security boundary that must hold across every authentication path. Nothing in the syntax tells you that. - Cross-path consistency. The boundary is enforced on two of three paths. A tool has to notice the third path is missing the guard the other two have.
- What’s missing.
getSSOBlockedDomains()was never added to the email OTP path at all. There is nothing to flag syntactically; the guard simply does not exist. - How the code is deployed. The check that was present (
getSsoAuthProviderIdForDomain) appeared to provide enforcement but depends onmultiTenantSsoAvailable, which is permanently false on self-hosted. It looked like coverage. It wasn’t.
Identifying this flaw required multi-step semantic reasoning across multiple files and a configuration flag. Pattern matching wasn’t built for this. This work used to require an expert security engineer reviewing the code, line by line, asking “is this invariant actually enforced everywhere it needs to be?”.
How Maze Code found the bug
Maze Code flagged this on its own while scanning the Langfuse open-source code. Nobody pointed it at the login code. It reasoned about the SSO rule and saw the reset path didn’t hold it.
Maze Code is a different kind of SAST scanner. It uses AI agents that read and reason over your whole codebase, understanding what your code actually does instead of matching rules like legacy tools. That’s how it catches bugs like this one, where the problem is a missing check rather than a bad line, plus other business logic flaws no rule engine can catch. It read the login code, worked out what the SSO setting was meant to do, and found the one path that ignored it.
This bug doesn’t live in any one file. It only shows up when you connect three of them, the two paths that hold the check, the one that doesn’t, and the config flag that turns the reset-path check into dead code on self-hosted. Read any of those files alone and nothing looks wrong. The signal is in how they relate.
That’s why scale works in Maze Code’s favor instead of against it. The more of the codebase it holds at once, here a 1,000-line auth file and the config around it, the more of those connections it can reason over. A bigger repo or a monorepo is more context, not more noise.
A human then validated the finding and coordinated disclosure. But the discovery was the tool’s.
Finding: AUTH_DOMAINS_WITH_SSO_ENFORCEMENT bypassed via email OTP provider Class: CWE-288 – Authentication Bypass Using an Alternate Path or Channel
Enforcement invariant identified: AUTH_DOMAINS_WITH_SSO_ENFORCEMENT is a security boundary. It must hold on every path that grants a session. Two separate enforcement mechanisms exist in the codebase. The env-based one (getSSOBlockedDomains()) and the database-based one (getSsoAuthProviderIdForDomain()). They do not cover the same ground.
Path comparison – all three verified:
| Path | Location | getSSOBlockedDomains() called? |
|---|---|---|
credentialsProvider.authorize() | auth.ts:107 | Yes ✓ |
| Signup handler | signupApiHandler.ts:53 | Yes ✓ |
signIn callback, email branch | auth.ts:967 | No ✗ |
The email branch falls through to if (user) return true. The enforced-domain list is never consulted.
Maze investigates every finding: Proving the finding is real:
Is the email provider active by default? No. It requires both SMTP_CONNECTION_URL and EMAIL_FROM_ADDRESS to be set (auth.ts:162–183). The finding applies only when SMTP is configured alongside SSO enforcement. Scope narrows; finding stands.
Does the database SSO check in the signIn callback cover this gap? Only when a database SSO config exists for the domain. Self-hosted operators rely on AUTH_DOMAINS_WITH_SSO_ENFORCEMENT (the env var path), not database configs. That variable is not consulted on the email path. (auth.ts:930–947)
Is the session limited to the password-reset page? No. NextAuth mints a full JWT the moment the callback returns true. AUTH_SESSION_MAX_AGE applies; the default is 30 days. The reset page is only the landing URL. The attacker can navigate anywhere after. (auth.ts:760–762)
Conclusion: No middleware, no base class, no decorator, and no session-level check consults AUTH_DOMAINS_WITH_SSO_ENFORCEMENT outside the credentials and signup paths. The email OTP path has no enforcement of this control.
Files read before confirming: auth.ts (lines 100–120, 162–175, 609–742, 760–910, 912–1007), signupApiHandler.ts, credentialsRouter.ts, credentialsUtils.ts, ResetPasswordButton.tsx, ResetPasswordPage.tsx, reset-password.tsx, env.mjs
How to bypass SSO enforcement
The bug is an authentication bypass (CWE-288). It only works under specific conditions. An attacker needs three things:
- A deployment with
AUTH_DOMAINS_WITH_SSO_ENFORCEMENTset and SMTP configured (no SMTP, no email provider, no exposure). - An account that already exists in the database. It does, because the user previously logged in via the real IdP.
- Control of a live mailbox at the enforced domain.
This isn’t a remote, anyone-on-the-internet attack. Pulling it off requires an account and a live mailbox at the domain, so the realistic threat is an insider or someone who just left. And that’s exactly why it’s dangerous.
- An employee is offboarded. IT revokes their SSO account and trusts
AUTH_DOMAINS_WITH_SSO_ENFORCEMENTto close every door. - Their corporate mailbox stays live for the normal deprovisioning window, often a day or two.
- In that window they go to the reset page, request an OTP, read it from their inbox, and submit it.
- The OTP callback returns a full session, not a scoped reset token. They can reach traces, prompts, datasets, API keys, and the member list.
When someone uses the email reset code, NextAuth logs them in right away, before they ever set a new password. The five-minute expiry only applies to changing the password, not to the login itself. The login is already a full session. So by the time the attacker opens the reset page, they’re already in, and they don’t even need to reset anything. The reset page is just their starting point. From there, they can go anywhere in the account.
Am I affected, and next steps
Affected: self-hosted Langfuse (OSS and EE) with AUTH_DOMAINS_WITH_SSO_ENFORCEMENT set and SMTP configured. Langfuse Cloud is also affected in one case: when enforcement is set but no per-domain multi-tenant SSO is configured (possible on non-Enterprise tiers). Cloud instances with per-domain multi-tenant SSO configured are not affected; that path catches the OTP flow.
Not affected: any instance without SMTP configured (the email provider isn’t registered), or Cloud with multi-tenant SSO properly configured.
What to do:
- Upgrade to v3.174.0 or later (released 2026-05-13).
- Interim, regardless of patch status: at offboarding, revoke the mailbox, not just SSO. The whole exploit depends on the departed user still receiving mail at the enforced domain. Closing the inbox closes the window.
- If you don’t need email/password reset on an SSO-enforced instance, leaving SMTP unconfigured removes the exposure entirely.
The fix
The fix is small and exact once the bug is understood: apply the same enforcement to the reset path that the other two paths already had. Check the domain before the database lookup, and redirect enforced-domain users away. In shape, matching the published fix:
// the missing guard, added to the email branch of the signIn callback
const blockedDomains = getSSOBlockedDomains();
if (userDomain && blockedDomains.includes(userDomain)) {
return `…/enterprise-sso-required`;
}
One lesson worth taking to your own codebase: The same check existed in three places and one was missed. That is not bad luck. It is what happens when a security invariant is enforced by copy-pasted guards across N paths. Add a fourth auth path next year and the guard gets forgotten again. A better design is a single choke point every authentication path funnels through, so the rule is enforced once and cannot drift.




