sso: SAML ACS from samlConfig.callbackUrl never reaches expectedRecipients (binding URN passed where samlify expects "post")
Is this suited for github?
- Yes, this is suited for github
Reproduction
Summary
With a SAML provider whose Assertion Consumer Service is not the plugin's own
/sso/saml2/sp/acs/:providerId path, every login fails with
SAML_RECIPIENT_MISMATCH, even though the configured ACS is correct and the IdP
posts a valid, signed assertion to it.
The plugin looks the configured ACS up from samlify's SP metadata using the full
binding URN. samlify keys that lookup by short binding name (post), so the
lookup returns undefined and the configured ACS is dropped from the accepted
recipients.
Current vs. Expected behavior
Versions
@better-auth/sso1.6.25, and unchanged in 1.7.5 (latest at the time of writing)better-auth1.6.25samlify2.13.1
Where
@better-auth/sso dist/index.mjs:
const SAML_HTTP_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
// ...
const assertionConsumerServiceUrl =
sp.entityMeta.getAssertionConsumerService(SAML_HTTP_POST_BINDING);samlify build/src/metadata-sp.js, SpMetadata.prototype.getAssertionConsumerService:
var bindName_1 = namespace.binding[binding]; // namespace.binding = { redirect, post, simpleSign, artifact }namespace.binding["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"] is
undefined, no entry matches, and the function returns undefined.
getExpectedSAMLRecipients(...) then filters that out. What is left is
currentCallbackPath and ${baseURL}/sso/saml2/sp/acs/${providerId}, which are
the same URL.
Reproduce
- Register a SAML provider with
samlConfig.callbackUrlset to an ACS that is not the plugin path, e.g.https://app.example/callback, and nospMetadata.metadataXML. - At the IdP, register
https://app.example/callbackas the relying party's ACS. - Forward that endpoint's POST to the plugin's ACS handler, and sign in.
Expected: the assertion's Recipient (https://app.example/callback) is
accepted.
Actual:
code: 'SAML_RECIPIENT_MISMATCH',
expectedRecipients: [
'https://app.example/api/auth/sso/saml2/sp/acs/<providerId>',
'https://app.example/api/auth/sso/saml2/sp/acs/<providerId>'
]Fix
Pass samlify's short binding key:
- sp.entityMeta.getAssertionConsumerService(SAML_HTTP_POST_BINDING)
+ sp.entityMeta.getAssertionConsumerService("post")getSAMLPostAssertionConsumerServiceUrls, which parses supplied SP metadata XML,
already compares the full URN correctly. So supplying spMetadata.metadata works
around it, and only the callbackUrl path is affected.
What version of Better Auth are you using?
@better-auth/sso1.6.25, and unchanged in 1.7.5 (latest at the time of writing) -better-auth1.6.25 -samlify2.13.1
System info
### Versions
- `@better-auth/sso` 1.6.25, and unchanged in 1.7.5 (latest at the time of writing)
- `better-auth` 1.6.25
- `samlify` 2.13.1Which area(s) are affected? (Select all that apply)
Package
Auth config (if applicable)
import { betterAuth } from "better-auth"
export const auth = betterAuth({
emailAndPassword: {
enabled: true
},
});Additional context
Why it matters
A deployment whose IdP already has a registered ACS, commonly an enterprise AD-FS trust that is costly to change, cannot adapt to it. The one configuration knob meant for that case is silently ignored, and the failure names neither the configured ACS nor the lookup that lost it.
Source: better-auth/better-auth