Skip to content

feat: per-request ForceAuthn for createLoginRequest (closes #359)#618

Merged
tngan merged 1 commit into
masterfrom
fix/359-force-authn
May 1, 2026
Merged

feat: per-request ForceAuthn for createLoginRequest (closes #359)#618
tngan merged 1 commit into
masterfrom
fix/359-force-authn

Conversation

@tngan
Copy link
Copy Markdown
Owner

@tngan tngan commented May 1, 2026

Summary

  • Add forceAuthn?: boolean to CreateLoginRequestOptions so callers can request that the IdP re-authenticate the presenter on a single login request, without mutating entity settings.
  • Splice ForceAuthn="{ForceAuthn}" into the default <samlp:AuthnRequest> template; the replaceTagsByValue omission rule from fix: omit AuthnRequest attributes whose value is null/undefined (closes #455) #614 drops the attribute when the value is undefined, so existing callers see no behaviour change.
  • Plumb forceAuthn from ServiceProvider#createLoginRequest through all three login bindings (redirect / post / simpleSign).

Closes #359.

Spec reference

  • saml-core §3.4.1ForceAuthn attribute on <samlp:AuthnRequest> (xs:boolean, use="optional").
  • saml-profiles §4.1.4.1 — Web Browser SSO Profile interpretation: "the identity provider MUST authenticate the presenter directly rather than rely on a previous security context".

Test plan

  • yarn test — all existing + new tests pass (273 / 1 skipped).
  • yarn coverage — all four global thresholds (statements / branches / functions / lines) hold at >= 90%.
  • npx tsc --noEmit — clean.
  • Default AuthnRequest template renders ForceAuthn="true" when set.
  • Default AuthnRequest template omits the attribute when undefined.
  • Redirect binding emits ForceAuthn="true" after deflate + base64 round-trip.
  • Post binding emits ForceAuthn="true" after base64 decode.
  • SimpleSign binding emits ForceAuthn="true" after base64 decode.
  • Each binding omits ForceAuthn= entirely when no options bag is supplied (back-compat).
  • forceAuthn: false renders ForceAuthn="false" verbatim — still a valid xs:boolean.

🤖 Generated with Claude Code

@tngan tngan marked this pull request as ready for review May 1, 2026 08:28
- saml-core §3.4.1 — `ForceAuthn` attribute on `<samlp:AuthnRequest>`
  (`xs:boolean`, `use="optional"`).
- saml-profiles §4.1.4.1 — Web Browser SSO Profile interpretation
  ("the identity provider MUST authenticate the presenter directly
  rather than rely on a previous security context").

Callers had no first-class way to ask the IdP to re-prompt the user
for credentials on a single login request. ForceAuthn is the
spec-defined boolean for exactly this use case (step-up, re-auth on
sensitive operations) and slots cleanly into the per-request options
bag introduced in #612.

- Extend `CreateLoginRequestOptions` with `forceAuthn?: boolean` so
  callers can opt in per request without mutating entity settings.
- Splice `ForceAuthn="{ForceAuthn}"` into the default AuthnRequest
  template; the `replaceTagsByValue` omission rule from #614 already
  drops the attribute when the value is undefined, so backwards-
  compatible callers see no change.
- Plumb `forceAuthn` from `ServiceProvider#createLoginRequest`
  through all three login bindings (redirect / post / simpleSign)
  via a new optional trailing parameter.

- Default template renders `ForceAuthn="true"` when set and omits
  the attribute entirely when undefined.
- Each binding (redirect, post, simpleSign) renders `ForceAuthn="true"`
  end-to-end when `{ forceAuthn: true }` is passed.
- Each binding omits the attribute when the options bag (or the
  whole 3rd parameter) is absent — locks in the back-compat contract.
- `forceAuthn: false` renders `ForceAuthn="false"` verbatim, which
  is still a valid `xs:boolean` per saml-core §3.4.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tngan tngan force-pushed the fix/359-force-authn branch from a46bd5d to 4dfba3b Compare May 1, 2026 09:24
@tngan tngan merged commit 1c2b9fa into master May 1, 2026
3 checks passed
tngan added a commit that referenced this pull request May 14, 2026
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 added a commit that referenced this pull request May 14, 2026
closes #437) (#623)

- 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>
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.

ForceAuthn can not be set in SP settings

1 participant