@mcp-abap-adt/auth-providers 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +773 -77
  3. package/dist/auth/saml2Auth.d.ts +6 -2
  4. package/dist/auth/saml2Auth.d.ts.map +1 -1
  5. package/dist/auth/saml2Auth.js +9 -20
  6. package/dist/auth/samlBearerAssertion.d.ts.map +1 -1
  7. package/dist/auth/samlBearerAssertion.js +6 -2
  8. package/dist/auth/strictXml.d.ts +13 -0
  9. package/dist/auth/strictXml.d.ts.map +1 -0
  10. package/dist/auth/strictXml.js +21 -0
  11. package/dist/errors/AssertionValidationError.d.ts +15 -0
  12. package/dist/errors/AssertionValidationError.d.ts.map +1 -0
  13. package/dist/errors/AssertionValidationError.js +24 -0
  14. package/dist/errors/TokenProviderErrors.d.ts +2 -0
  15. package/dist/errors/TokenProviderErrors.d.ts.map +1 -1
  16. package/dist/errors/TokenProviderErrors.js +3 -1
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +11 -1
  20. package/dist/providers/Saml2BearerProvider.d.ts +1 -0
  21. package/dist/providers/Saml2BearerProvider.d.ts.map +1 -1
  22. package/dist/providers/Saml2BearerProvider.js +17 -2
  23. package/dist/providers/Saml2PureProvider.d.ts +1 -0
  24. package/dist/providers/Saml2PureProvider.d.ts.map +1 -1
  25. package/dist/providers/Saml2PureProvider.js +19 -5
  26. package/dist/providers/saml2Utils.d.ts +50 -2
  27. package/dist/providers/saml2Utils.d.ts.map +1 -1
  28. package/dist/providers/saml2Utils.js +102 -2
  29. package/dist/validation/assertionValidator.d.ts +28 -0
  30. package/dist/validation/assertionValidator.d.ts.map +1 -0
  31. package/dist/validation/assertionValidator.js +519 -0
  32. package/dist/validation/documentIds.d.ts +15 -0
  33. package/dist/validation/documentIds.d.ts.map +1 -0
  34. package/dist/validation/documentIds.js +32 -0
  35. package/dist/validation/inMemoryReplayStore.d.ts +22 -0
  36. package/dist/validation/inMemoryReplayStore.d.ts.map +1 -0
  37. package/dist/validation/inMemoryReplayStore.js +49 -0
  38. package/dist/validation/signedNode.d.ts +54 -0
  39. package/dist/validation/signedNode.d.ts.map +1 -0
  40. package/dist/validation/signedNode.js +182 -0
  41. package/dist/validation/xsdDateTime.d.ts +17 -0
  42. package/dist/validation/xsdDateTime.d.ts.map +1 -0
  43. package/dist/validation/xsdDateTime.js +67 -0
  44. package/package.json +5 -10
  45. package/bin/auth-authorization-code.ts +0 -147
  46. package/bin/auth-client-credentials.ts +0 -109
  47. package/bin/utils/parseConfig.ts +0 -270
package/CHANGELOG.md CHANGED
@@ -7,6 +7,193 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.1.0] - 2026-09-26
11
+
12
+ Three refusals get stricter; no export, configuration field or error class
13
+ changes. The README's *Upgrading from 4.0 to 4.1* lists what now fails.
14
+
15
+ ### Changed
16
+
17
+ - **Every refusal names its rule.** No two rules under one `check` share a
18
+ message. An element that must appear exactly once says which way it failed
19
+ — absent, or more than one — for the Response's direct-child `Assertion`,
20
+ each signature's `ds:Reference`, `Status`, `StatusCode`, the assertion's
21
+ `Issuer`, `Conditions`, `Subject` and each bearer candidate's
22
+ `SubjectConfirmationData`. An empty `Issuer`, a `StatusCode` without a
23
+ `Value`, an `AudienceRestriction` naming no audience, and an absent versus
24
+ an invalid `Conditions/@NotOnOrAfter` each get their own message. The
25
+ signed-node refusal names the element the validator requires
26
+ (`samlp:Response` or `saml:Assertion`). `check` is unchanged for every
27
+ refusal, and every document 4.0.0 refused is still refused; code matching
28
+ on message text must match on `check`. The README lists every message
29
+ the shipped validators produce.
30
+ - **`bearerConfirmation` says why every candidate failed.** Still
31
+ existential: one confirmation passing every sub-rule is enough, and every
32
+ candidate is evaluated. A refusal lists each candidate in document order
33
+ with the first sub-rule it failed, in a fixed order, at most five, then
34
+ `and N more`. A `Subject` absent or repeated, and a `Subject` holding no
35
+ `SubjectConfirmation`, have messages of their own.
36
+ - **Every document value a message interpolates is quoted and cut**
37
+ (`quoteUntrusted`: JSON-quoted, 64 characters at most), including the
38
+ post-signature `Status` code, issuer and `Destination`, the `Conditions`
39
+ dates, xml-crypto's own messages about a malformed `Signature`, which embed
40
+ the offending element, and the parser message in `Saml2BearerProvider`'s
41
+ bearer conversion.
42
+
43
+ ### Fixed
44
+
45
+ - A SAML 1.x `Assertion` (`urn:oasis:names:tc:SAML:1.0:assertion`) outside
46
+ the signed assertion is refused at `signedNode`, as a SAML 2.0 one already
47
+ was. Before, one in `Extensions` passed either validator.
48
+ - An `Assertion` or `EncryptedAssertion` (SAML 2.0) or a SAML 1.x `Assertion`
49
+ inside a `ds:Signature` is refused at `signedNode`. An enveloped signature
50
+ leaves its own subtree out of the digest, so an element in `ds:Object`
51
+ there is unsigned however deep inside the signed assertion it sits; before,
52
+ such an element inside the signed assertion's own `ds:Signature` passed
53
+ either validator, `Saml2BearerProvider`'s default included.
54
+ - `idpInitiated: true` with `authnRequestId` is refused when the provider is
55
+ constructed — a `ValidationError` with `missingFields: ['idpInitiated']` —
56
+ not after `authorize()` returns, so before the user has been through the
57
+ browser.
58
+ - A malformed `Signature` whose `loadSignature` throws something other than
59
+ an `Error` is refused at `signature` quoting what was thrown. Before, the
60
+ refusal carried an empty message, and a thrown `null` or `undefined`
61
+ escaped as a `TypeError` instead of an `AssertionValidationError`.
62
+
63
+ ### Removed
64
+
65
+ - The `bin` commands `auth-authorization-code` and `auth-client-credentials`
66
+ (the `bin` field, and `bin` in `files`), and the devDependency `tsx`. They
67
+ never ran from an npm install: they were `.ts` files with a `tsx` shebang,
68
+ importing `src/`, which is not published. Not a breaking change for that
69
+ reason.
70
+
71
+ ### Documentation
72
+
73
+ - `ValidatedAssertion.nameId` is `undefined` when the `Subject` carries no
74
+ `NameID` or more than one. `NameID` is surfaced, never refused.
75
+
76
+ ### Development
77
+
78
+ - `@mcp-abap-adt/interfaces-auth` `^2.0.1`, whose
79
+ `IAssertionReplayStore.recordIfUnseen` JSDoc now describes the retention this
80
+ package implements: until the last instant a validator would still accept
81
+ the assertion, not its expiry.
82
+ - `@mcp-abap-adt/auth-stores` stays a devDependency: three test suites import
83
+ `AbapServiceKeyStore` from it.
84
+ - The end-to-end SAML suite (`samlValidation.test.ts`) runs about 6× faster,
85
+ about 41 s down to about 7 s: it starts one mock identity provider per
86
+ signed element in `beforeAll` and switches variants, instead of one per
87
+ test, each generating an RSA key. Every test still gets its own replay
88
+ store.
89
+ - The Keycloak suite trusts only the certificates under `KeyDescriptor
90
+ use="signing"` in Keycloak's metadata.
91
+
92
+ ## [4.0.0] - 2026-09-25
93
+
94
+ ### Breaking
95
+
96
+ - **Both SAML providers validate the assertion before trusting it** (#19).
97
+ Until now the only check was that the payload was a non-empty string, and
98
+ `Saml2PureProvider` took its session lifetime from a regular expression over
99
+ the unverified XML. `Saml2BearerProvider` now validates before the token
100
+ exchange, `Saml2PureProvider` before `cookieProvider`, through twelve checks:
101
+ document shape, unique IDs, signature, that the signed node is the node read,
102
+ `Status`, assertion ID, issuer, `Conditions`, `NotBefore`, `NotOnOrAfter`,
103
+ audience, one bearer subject confirmation (request ID, recipient, window) and
104
+ `Destination`, then replay. The README's *SAML assertion validation* lists
105
+ them with what refuses each.
106
+
107
+ **The configuration is required.** Supply `idpCertificates` (PEM or bare
108
+ base64 DER, a list for key rotation) and `idpEntityId`, or an
109
+ `assertionValidator` of your own; without them the provider's constructor
110
+ throws a `ValidationError` naming what is missing. A shipped validator
111
+ supplied as `assertionValidator` still needs `idpEntityId`, and its absence
112
+ fails at construction too; only a custom validator does without.
113
+ `spEntityId` becomes the `Audience` the assertion must name.
114
+
115
+ **Request IDs.** `InResponseTo` must answer the AuthnRequest the package
116
+ minted, or `authnRequestId` when the package did not build the request — a
117
+ pre-built `authorizationUrl`, or a strategy that returns a payload without
118
+ calling `buildAuthorizationUrl`. `idpInitiated: true` declares that no request
119
+ was sent, so the assertion must carry no `InResponseTo`; it gives up the
120
+ login-CSRF defence of a request ID, and is never inferred.
121
+ `Saml2BearerProvider` against UAA or XSUAA needs it, since both refuse an
122
+ assertion carrying `InResponseTo`. Having neither an ID nor the declaration,
123
+ or combining the declaration with an ID, raises a `ValidationError`. With
124
+ `idpInitiated` and no `authorizationUrl`, a strategy that calls
125
+ `buildAuthorizationUrl` is refused inside the builder, before any URL is
126
+ produced — so before a browser opens.
127
+
128
+ **Status, by validator.** Under `createSignedResponseValidator` —
129
+ `Saml2PureProvider`'s default — an identity provider returning a
130
+ non-`Success` status is now refused, where it was accepted before.
131
+ `Saml2BearerProvider`'s default, `createSignedAssertionValidator`, does not
132
+ read `Status`, so a bearer consumer sees no change there: a declining identity
133
+ provider mints no signed assertion, and the login fails for want of one.
134
+
135
+ **Migrating:** see *Migrating from 3.x to 4.0* in the README — add the trust
136
+ configuration, check `spEntityId`, and for `Saml2BearerProvider` against UAA
137
+ or XSUAA add `idpInitiated: true` with a strategy that never calls
138
+ `buildAuthorizationUrl`.
139
+ - **`parseSamlNotOnOrAfter` is removed.** `Saml2PureProvider`'s `expiresAt` is
140
+ now the validated assertion's expiry — the earlier of `Conditions/@NotOnOrAfter`
141
+ and the accepted bearer confirmation's — read from the verified document.
142
+ - **`buildSamlAuthorizationUrl` returns `{ url, requestId? }`**, since the
143
+ minted ID must survive to validation, and **`getSamlAssertion` returns
144
+ `Promise<SamlAssertionResult>`** (payload, request ID, ACS) instead of
145
+ `Promise<string>`. None of these, nor `parseSamlNotOnOrAfter`, was exported
146
+ from the package root; only a deep import of `dist/auth/saml2Auth` or
147
+ `dist/providers/saml2Utils` is affected.
148
+ - **`@mcp-abap-adt/interfaces-auth` `^2.0.0`** (was `^1.2.0`), where
149
+ `AssertionContext.expectedInResponseTo` is optional — a breaking change for
150
+ implementers of `IAssertionValidator`, which must refuse an assertion
151
+ carrying `InResponseTo` when it is absent.
152
+
153
+ ### Added
154
+
155
+ - **`createSignedResponseValidator`** and **`createSignedAssertionValidator`**,
156
+ sharing `ShippedValidatorOptions` (`idpCertificates`, `clockSkewMs`,
157
+ `replayStore`). The first requires the signature to cover the `Response` and
158
+ performs all twelve checks; the second accepts a signature over the
159
+ `Assertion` — bare, or inside a Response — and does not read `Status`,
160
+ `Response/Issuer` or `Destination` at all. `Saml2PureProvider` defaults to the
161
+ first, `Saml2BearerProvider` to the second; `assertionValidator` replaces
162
+ either.
163
+ - **Replay detection**: `defaultReplayStore`, a process-wide in-memory store
164
+ shared by every default validator, keyed by `{issuer, assertionId}` and
165
+ retained until the earlier of `Conditions/@NotOnOrAfter` and the latest
166
+ `NotOnOrAfter` of a bearer confirmation that answers the request and names
167
+ the ACS, plus `clockSkewMs` — as long as the assertion could still be
168
+ accepted, which can outlast `expiresAt`; `createInMemoryReplayStore()` for
169
+ an isolated one; `assertionReplayStore` on the providers for a shared store
170
+ across processes.
171
+ - **`clockSkewMs`**, default `0`.
172
+ - Any XML parse fault — including one `@xmldom/xmldom` would otherwise repair,
173
+ such as an undeclared entity — is a refusal at `document`, and the parser
174
+ never writes to the console; the bearer conversion (`toBearerAssertion`)
175
+ parses the same way.
176
+ - The validators refuse a payload carrying a `<!DOCTYPE` declaration, never
177
+ take a signing certificate from the document's own `KeyInfo`, and accept
178
+ RSA-SHA1 signatures and SHA-1 digests, as `xml-crypto` does by default; a
179
+ consumer wanting to refuse SHA-1 wraps a shipped validator in its own.
180
+ - **`AssertionValidationError`**, with `check: AssertionCheck` naming the check
181
+ that refused the assertion, and code `ASSERTION_VALIDATION_ERROR`.
182
+ - `xml-crypto` becomes a runtime dependency, for signature verification.
183
+
184
+ ### Development
185
+
186
+ - Both validators run end to end in `npm test`, through `Saml2PureProvider`
187
+ and a real callback, against responses from `@mcp-abap-adt/auth-mocks` (the
188
+ devDependency and its range are unchanged). Every corruption variant is refused at the
189
+ check it targets, except `statusFailure` and `wrongDestination`, which the
190
+ assertion-only validator accepts — both halves asserted.
191
+ - The provider stand's SAML suites run every login through the provider's
192
+ validation, including Keycloak's real responses, signed at both levels; the
193
+ live XSUAA suite does the same with its per-run identity provider, and now
194
+ refuses the `InResponseTo` case itself, before XSUAA sees it.
195
+ - `@mcp-abap-adt/interfaces-auth-sap` `^1.0.1`.
196
+
10
197
  ## [3.0.0] - 2026-09-24
11
198
 
12
199
  ### Added