Skip to content

feat: per-request AssertionConsumerServiceIndex for createLoginRequest (closes #437)#623

Merged
tngan merged 1 commit into
masterfrom
feat/437-acs-index
May 14, 2026
Merged

feat: per-request AssertionConsumerServiceIndex for createLoginRequest (closes #437)#623
tngan merged 1 commit into
masterfrom
feat/437-acs-index

Conversation

@tngan

@tngan tngan commented May 5, 2026

Copy link
Copy Markdown
Owner

Summary

Spec reference

  • saml-core §3.4.1<samlp:AuthnRequest> schema: AssertionConsumerServiceIndex, AssertionConsumerServiceURL, and ProtocolBinding are all use="optional" and mutually exclusive ("If the <AssertionConsumerServiceIndex> attribute is present, neither <AssertionConsumerServiceURL> nor <ProtocolBinding> may be set").
  • saml-profiles §4.1.4.1 — Web Browser SSO Profile permits the index form; the IdP resolves the endpoint from the SP's metadata.

Implementation notes

Test plan

  • Default template renders AssertionConsumerServiceIndex and drops AssertionConsumerServiceURL + ProtocolBinding when the index is set.
  • Default template renders AssertionConsumerServiceURL + ProtocolBinding and drops the index when AssertionConsumerServiceIndex is undefined (regression for legacy default behaviour).
  • Byte-identical legacy snapshot pinned via literal-string compare on the rendered AuthnRequest (modulo ID= / IssueInstant=).
  • sp.createLoginRequest(idp, 'redirect', { assertionConsumerServiceIndex: 1 }) — inflated SAMLRequest contains AssertionConsumerServiceIndex="1", no URL, no ProtocolBinding.
  • Same shape verified for 'post' and 'simpleSign' bindings.
  • index=0 edge case — falsy but defined; the attribute still renders.
  • Backwards-compat: each binding without the options bag continues to emit ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" and AssertionConsumerServiceURL=.
  • yarn test — 302 passing / 1 skipped.
  • yarn coverage — ≥90 % on every metric (statements 97.33, branches 90.87, functions 99.15, lines 97.33).
  • npx tsc — clean.

🤖 Generated with Claude Code

@tngan tngan marked this pull request as ready for review May 5, 2026 06:07
closes #437)

- saml-core §3.4.1 — `<samlp:AuthnRequest>` schema:
  `AssertionConsumerServiceIndex`, `AssertionConsumerServiceURL`, and
  `ProtocolBinding` are all `use="optional"` and mutually exclusive
  (the spec text: "If the `<AssertionConsumerServiceIndex>` attribute
  is present, neither `<AssertionConsumerServiceURL>` nor
  `<ProtocolBinding>` may be set").
- saml-profiles §4.1.4.1 — Web Browser SSO Profile permits the index
  form; the IdP looks up the endpoint in the SP's metadata.

Some IdPs (legacy Shibboleth deployments, certain ADFS configurations)
prefer the metadata-indexed ACS form so the SP can declare multiple ACS
endpoints once and select one by index per request, rather than echoing
the URL+ProtocolBinding pair in every AuthnRequest. samlify previously
hardcoded `ProtocolBinding="…HTTP-POST"` and only emitted the URL form,
which left those peers unaddressable from this library.

- `CreateLoginRequestOptions.assertionConsumerServiceIndex?: number` —
  per-request opt-in. JSDoc spells out mutual exclusion semantics: when
  set, the URL and ProtocolBinding are both omitted (index wins).
- `defaultLoginRequestTemplate` now uses placeholders for all three
  attributes; the omission rule from #614 (`replaceTagsByValue` drops
  attributes whose value is `null`/`undefined`) handles the swap so no
  new omission logic is added.
- `entity-sp.ts:createLoginRequest` plumbs the new option to all three
  binding builders (`redirect`, `post`, `simpleSign`), mirroring the
  shape introduced for `forceAuthn` in #618.
- Each binding builder accepts `assertionConsumerServiceIndex?: number`
  and sets `ProtocolBinding` / `AssertionConsumerServiceURL` to
  `undefined` when the index is supplied; the metadata-derived ACS URL
  is suppressed in that branch.

- New `describe('AssertionConsumerServiceIndex (#437, saml-core §3.4.1)')`
  block in `test/units.ts` covering:
  - Default template renders the index attribute and drops URL +
    ProtocolBinding when the index is set (incl. `index=0`).
  - Default template renders URL + ProtocolBinding (and drops the
    index) when the index is undefined.
  - Byte-identical pin: literal-string compare on the rendered XML for
    a backwards-compatible legacy call (modulo `ID=` and
    `IssueInstant=`).
  - Redirect / post / simpleSign bindings each verified end-to-end on
    the real default-template path.
  - Backwards-compat regression for each binding when no options are
    passed.
- `yarn test` (302 / 1 skipped) and `yarn coverage` (≥90 across all
  metrics) pass; `npx tsc` is clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tngan tngan force-pushed the feat/437-acs-index branch from 1418d57 to 13e9cd7 Compare May 14, 2026 17:01
@tngan tngan merged commit 0235cab into master May 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant