[GH-ISSUE #8607] SAML InResponseTo validation always fails — extract.inResponseTo is undefined, should be extract.response?.inResponseTo #19765

Closed
opened 2026-04-15 19:06:15 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @evanenochs on GitHub (Mar 14, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8607

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Configure the SSO plugin with strict InResponseTo validation:
sso({
  saml: {
    enableInResponseToValidation: true,
    allowIdpInitiated: false,
  },
})
  1. Register a SAML provider (e.g., Microsoft Entra ID / Azure AD)
  2. Initiate an SP-initiated SSO login from your application (not from the IdP portal)
  3. Azure AD authenticates the user and sends a SAML Response back to the ACS endpoint with a valid InResponseTo attribute
  4. Login is rejected with: SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false

Current vs. Expected behavior

Expected: SP-initiated SAML login succeeds. The plugin reads InResponseTo from the parsed SAML Response, matches it against the stored AuthnRequest ID, and allows the login.

Actual: Every SP-initiated login is rejected. The plugin reads extract.inResponseTo which is always undefined. The InResponseTo value from Azure AD is actually present at extract.response.inResponseTosamlify's extractor nests it under the response key. Because the plugin sees undefined, it misclassifies every SP-initiated response as IdP-initiated and rejects it.

The flow is SP-initiated. Azure AD correctly includes InResponseTo in its SAML Response. The plugin simply reads the wrong field path, so it can never find the value.

Proof using samlify's extractor directly:

const { extract, loginResponseFields } = require('samlify/build/src/extractor');

const xml = `<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  ID="_resp1" Version="2.0" IssueInstant="2026-01-01T00:00:00Z"
  Destination="http://localhost/acs" InResponseTo="_req1">
  <saml:Issuer>https://idp.example.com</saml:Issuer>
  <samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
  <saml:Assertion ID="_a1" Version="2.0" IssueInstant="2026-01-01T00:00:00Z">
    <saml:Issuer>https://idp.example.com</saml:Issuer>
    <saml:Subject><saml:NameID>user@example.com</saml:NameID></saml:Subject>
    <saml:Conditions NotBefore="2026-01-01T00:00:00Z" NotOnOrAfter="2026-01-01T01:00:00Z">
      <saml:AudienceRestriction><saml:Audience>http://localhost</saml:Audience></saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2026-01-01T00:00:00Z" SessionIndex="_s1"/>
  </saml:Assertion>
</samlp:Response>`;

const result = extract(xml, loginResponseFields(null));
console.log(result.inResponseTo);           // undefined  ← what the plugin reads
console.log(result.response.inResponseTo);  // "_req1"    ← where the value actually is

The logout response handler in the same file does it correctly:

// handleLogoutResponse — correct
const inResponseTo = extract?.response?.inResponseTo;

But both login handlers (callbackSSOSAML and acsEndpoint) do it wrong:

// callbackSSOSAML — incorrect
const inResponseTo = extract.inResponseTo;

// acsEndpoint — incorrect
const inResponseToAcs = extract.inResponseTo;

Suggested fix:

- const inResponseTo = extract.inResponseTo;
+ const inResponseTo = extract.response?.inResponseTo;

What version of Better Auth are you using?

1.5.5

System info

**System info:**
- `@better-auth/sso`: 1.5.5
- `samlify`: 2.10.2
- IdP: Microsoft Entra ID (Azure AD) with SAML 2.0
- Node.js 24+
- macOS

Which area(s) are affected? (Select all that apply)

Package

Auth config (if applicable)


Additional context

Additional context:

This is reproducible locally. The bug likely affects all environments since it's a code path issue, not environment-specific. The feature (enableInResponseToValidation) defaults to false, which is probably why it hasn't been reported — most users never enable strict validation. I verified the extract structure by running samlify's extractor directly against a valid SAML Response XML (reproduction script above).

Originally created by @evanenochs on GitHub (Mar 14, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8607 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Configure the SSO plugin with strict InResponseTo validation: ```ts sso({ saml: { enableInResponseToValidation: true, allowIdpInitiated: false, }, }) ``` 2. Register a SAML provider (e.g., Microsoft Entra ID / Azure AD) 3. Initiate an SP-initiated SSO login from your application (not from the IdP portal) 4. Azure AD authenticates the user and sends a SAML Response back to the ACS endpoint with a valid `InResponseTo` attribute 5. Login is rejected with: `SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false` ### Current vs. Expected behavior **Expected:** SP-initiated SAML login succeeds. The plugin reads `InResponseTo` from the parsed SAML Response, matches it against the stored AuthnRequest ID, and allows the login. **Actual:** Every SP-initiated login is rejected. The plugin reads `extract.inResponseTo` which is always `undefined`. The `InResponseTo` value from Azure AD is actually present at `extract.response.inResponseTo` — `samlify`'s extractor nests it under the `response` key. Because the plugin sees `undefined`, it misclassifies every SP-initiated response as IdP-initiated and rejects it. **The flow is SP-initiated.** Azure AD correctly includes `InResponseTo` in its SAML Response. The plugin simply reads the wrong field path, so it can never find the value. Proof using samlify's extractor directly: ```js const { extract, loginResponseFields } = require('samlify/build/src/extractor'); const xml = `<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_resp1" Version="2.0" IssueInstant="2026-01-01T00:00:00Z" Destination="http://localhost/acs" InResponseTo="_req1"> <saml:Issuer>https://idp.example.com</saml:Issuer> <samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status> <saml:Assertion ID="_a1" Version="2.0" IssueInstant="2026-01-01T00:00:00Z"> <saml:Issuer>https://idp.example.com</saml:Issuer> <saml:Subject><saml:NameID>user@example.com</saml:NameID></saml:Subject> <saml:Conditions NotBefore="2026-01-01T00:00:00Z" NotOnOrAfter="2026-01-01T01:00:00Z"> <saml:AudienceRestriction><saml:Audience>http://localhost</saml:Audience></saml:AudienceRestriction> </saml:Conditions> <saml:AuthnStatement AuthnInstant="2026-01-01T00:00:00Z" SessionIndex="_s1"/> </saml:Assertion> </samlp:Response>`; const result = extract(xml, loginResponseFields(null)); console.log(result.inResponseTo); // undefined ← what the plugin reads console.log(result.response.inResponseTo); // "_req1" ← where the value actually is ``` The logout response handler in the same file does it correctly: ```js // handleLogoutResponse — correct const inResponseTo = extract?.response?.inResponseTo; ``` But both login handlers (callbackSSOSAML and acsEndpoint) do it wrong: ```js // callbackSSOSAML — incorrect const inResponseTo = extract.inResponseTo; // acsEndpoint — incorrect const inResponseToAcs = extract.inResponseTo; ``` **Suggested fix:** ```diff - const inResponseTo = extract.inResponseTo; + const inResponseTo = extract.response?.inResponseTo; ``` ### What version of Better Auth are you using? 1.5.5 ### System info ```bash **System info:** - `@better-auth/sso`: 1.5.5 - `samlify`: 2.10.2 - IdP: Microsoft Entra ID (Azure AD) with SAML 2.0 - Node.js 24+ - macOS ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript ``` ### Additional context Additional context: This is reproducible locally. The bug likely affects all environments since it's a code path issue, not environment-specific. The feature (enableInResponseToValidation) defaults to false, which is probably why it hasn't been reported — most users never enable strict validation. I verified the extract structure by running samlify's extractor directly against a valid SAML Response XML (reproduction script above).
GiteaMirror added the identitybug labels 2026-04-15 19:06:15 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 14, 2026):

Hi @evanenochs! I'm Dosu and I’m helping the better-auth team.

I can confirm this bug exists. Looking at the code in packages/sso/src/routes/sso.ts, both callbackSSOSAML and acsEndpoint incorrectly read:

const inResponseTo = (extract as SAMLAssertionExtract).inResponseTo as
    | string
    | undefined;

Meanwhile, handleLogoutResponse correctly uses:

const inResponseTo = extract?.response?.inResponseTo;

Your suggested fix is correct — the login handlers should use extract.response?.inResponseTo to match the samlify structure and the existing logout handler pattern. This bug effectively breaks the replay attack protection that was introduced in PR #6557.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4061132551 --> @dosubot[bot] commented on GitHub (Mar 14, 2026): <!-- Greeting --> Hi @evanenochs! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> I can confirm this bug exists. Looking at the code in [`packages/sso/src/routes/sso.ts`](https://github.com/better-auth/better-auth/blob/64339f06459b0ff940cae8bbda705371c216de39/packages/sso/src/routes/sso.ts#L2176-L2178), both `callbackSSOSAML` and `acsEndpoint` incorrectly read: ```typescript const inResponseTo = (extract as SAMLAssertionExtract).inResponseTo as | string | undefined; ``` Meanwhile, [`handleLogoutResponse`](https://github.com/better-auth/better-auth/blob/64339f06459b0ff940cae8bbda705371c216de39/packages/sso/src/routes/sso.ts#L3112) correctly uses: ```typescript const inResponseTo = extract?.response?.inResponseTo; ``` Your suggested fix is correct — the login handlers should use `extract.response?.inResponseTo` to match the samlify structure and the existing logout handler pattern. This bug effectively breaks the replay attack protection that was introduced in [PR #6557](https://github.com/better-auth/better-auth/pull/6557). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/f077d0b4-d5ab-4089-96e2-14d427113c77?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/f077d0b4-d5ab-4089-96e2-14d427113c77?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/f077d0b4-d5ab-4089-96e2-14d427113c77?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/f077d0b4-d5ab-4089-96e2-14d427113c77?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/f077d0b4-d5ab-4089-96e2-14d427113c77?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/f077d0b4-d5ab-4089-96e2-14d427113c77?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/f077d0b4-d5ab-4089-96e2-14d427113c77?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=I%20can%20confirm%20this%20bug%20exists.%20Looking%20at%20the%20code%20in%20%5B%60packages/sso/src/routes/sso.ts%60%5D%28https%3A//github.com/better-auth/better-auth/blob/64339f06459b0ff940cae8bbda705371c216de39/packages/sso/src/routes/sso.ts%23L2176-L2178%29%2C%20both%20%60callbackSSOSAML%60%20and%20%60acsEndpoint%60%20incorrectly%20read%3A%0A%0A%60%60%60typescript%0Aconst%20inResponseTo%20%3D%20%28extract%20as%20SAMLAssertionExtract%29.inResponseTo%20as%0A%20%20%20%20%7C%20string%0A%20%20%20%20%7C%20undefined%3B%0A%60%60%60%0A%0AMeanwhile%2C%20%5B%60handleLogoutResponse%60%5D%28https%3A//github.com/better-auth/better-auth/blob/64339f06459b0ff940cae8bbda705371c216de39/packages/sso/src/routes/sso.ts%23L3112%29%20correctly%20uses%3A%0A%0A%60%60%60typescript%0Aconst%20inResponseTo%20%3D%20extract%3F.response%3F.inResponseTo%3B%0A%60%60%60%0A%0AYour%20suggested%20fix%20is%20correct%20%E2%80%94%20the%20login%20handlers%20should%20use%20%60extract.response%3F.inResponseTo%60%20to%20match%20the%20samlify%20structure%20and%20the%20existing%20logout%20handler%20pattern.%20This%20bug%20effectively%20breaks%20the%20replay%20attack%20protection%20that%20was%20introduced%20in%20%5BPR%20%236557%5D%28https%3A//github.com/better-auth/better-auth/pull/6557%29.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/8607)
Author
Owner

@evanenochs commented on GitHub (Mar 14, 2026):

@dosu I have a pr for it https://github.com/better-auth/better-auth/pull/8608

<!-- gh-comment-id:4061167801 --> @evanenochs commented on GitHub (Mar 14, 2026): @dosu I have a pr for it https://github.com/better-auth/better-auth/pull/8608
Author
Owner

@gustavovalverde commented on GitHub (Apr 13, 2026):

Fixed in #9055, ships in v1.7.0-beta.1.

<!-- gh-comment-id:4239720750 --> @gustavovalverde commented on GitHub (Apr 13, 2026): Fixed in #9055, ships in v1.7.0-beta.1.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19765