@mcp-abap-adt/auth-providers 4.0.0 → 4.1.2

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 (39) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +180 -27
  3. package/dist/auth/clientCredentialsAuth.d.ts.map +1 -1
  4. package/dist/auth/clientCredentialsAuth.js +2 -1
  5. package/dist/auth/oauthErrorBody.d.ts +15 -0
  6. package/dist/auth/oauthErrorBody.d.ts.map +1 -0
  7. package/dist/auth/oauthErrorBody.js +58 -0
  8. package/dist/auth/saml2TokenExchange.d.ts.map +1 -1
  9. package/dist/auth/saml2TokenExchange.js +9 -2
  10. package/dist/auth/samlBearerAssertion.d.ts.map +1 -1
  11. package/dist/auth/samlBearerAssertion.js +4 -1
  12. package/dist/auth/tokenRefresher.d.ts.map +1 -1
  13. package/dist/auth/tokenRefresher.js +2 -1
  14. package/dist/providers/BaseTokenProvider.d.ts +3 -1
  15. package/dist/providers/BaseTokenProvider.d.ts.map +1 -1
  16. package/dist/providers/BaseTokenProvider.js +4 -4
  17. package/dist/providers/saml2Utils.d.ts +3 -2
  18. package/dist/providers/saml2Utils.d.ts.map +1 -1
  19. package/dist/providers/saml2Utils.js +8 -0
  20. package/dist/validation/assertionValidator.d.ts.map +1 -1
  21. package/dist/validation/assertionValidator.js +207 -132
  22. package/dist/validation/signedNode.d.ts +2 -2
  23. package/dist/validation/signedNode.js +16 -5
  24. package/package.json +6 -12
  25. package/bin/auth-authorization-code.ts +0 -147
  26. package/bin/auth-client-credentials.ts +0 -109
  27. package/bin/utils/parseConfig.ts +0 -270
  28. package/dist/__tests__/helpers/configHelpers.d.ts +0 -46
  29. package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
  30. package/dist/__tests__/helpers/configHelpers.js +0 -232
  31. package/dist/__tests__/helpers/netHelpers.d.ts +0 -19
  32. package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
  33. package/dist/__tests__/helpers/netHelpers.js +0 -92
  34. package/dist/__tests__/helpers/testLogger.d.ts +0 -7
  35. package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
  36. package/dist/__tests__/helpers/testLogger.js +0 -45
  37. package/dist/__tests__/integration/stand/formLogin.d.ts +0 -68
  38. package/dist/__tests__/integration/stand/formLogin.d.ts.map +0 -1
  39. package/dist/__tests__/integration/stand/formLogin.js +0 -194
package/CHANGELOG.md CHANGED
@@ -7,6 +7,150 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.1.2] - 2026-09-26
11
+
12
+ ### Security
13
+
14
+ - **No token the provider holds reaches a log line.** Every provider logged tokens through
15
+ `BaseTokenProvider.formatToken`. That function returned a token of 50
16
+ characters or fewer whole, and a longer one's first and last 25 characters.
17
+ UAA and XSUAA refresh tokens are opaque and about 34 characters, so they were
18
+ logged in full, at `info`, by `AuthorizationCodeProvider` on creation, on
19
+ refresh (old and new) and by `BaseTokenProvider` on every token update. Access
20
+ tokens leaked 50 characters. A log line now carries only
21
+ `<redacted, N chars>`. Anyone who shipped logs from an earlier version at
22
+ `info` or `debug` should treat the refresh tokens in them as exposed, and
23
+ revoke them.
24
+ - **No token endpoint's error body reaches a log line or an error message
25
+ whole.** The SAML exchange and SAML refresh logged the whole error body, and
26
+ `refreshJwtToken` and `getTokenWithClientCredentials` serialised it into the
27
+ thrown `Error`'s message, which `BaseTokenProvider` then logged. Only the
28
+ body's `error` and `error_description` are kept now, each quoted and capped:
29
+ `error` at 64 characters, and `error_description` at 512, so a server's
30
+ diagnosis survives. Both are redacted first. Every secret the request itself
31
+ sent (refresh token, assertion, client secret), however short, whether it
32
+ comes back as sent or form- or percent-encoded, and anything shaped like a JWT
33
+ becomes `<redacted>`. A server that echoes one of those, in the body or in
34
+ its description, no longer leaks it. **Limit:** an opaque token the request
35
+ did not send cannot be told from an ordinary identifier, so if a server writes
36
+ a new one into `error_description` it passes through, capped at 512
37
+ characters. Scrubbing every long string would also erase the IDs that make a
38
+ refusal diagnosable.
39
+
40
+ ## [4.1.1] - 2026-09-26
41
+
42
+ ### Changed
43
+
44
+ - The `signedNode` refusal for an assertion inside a `ds:Signature` now reads
45
+ "…inside a ds:Signature, which is never accepted". The 4.1.0 wording, "where
46
+ no signature covers it", was untrue when a signed Response also covers that
47
+ signature. `check` and the fragment `inside a ds:Signature` are unchanged.
48
+
49
+ ### Fixed
50
+
51
+ - The tarball no longer ships `dist/__tests__/…`. Test helpers and stand
52
+ fixtures were built into `dist` and published with 4.0.0 and 4.1.0. The build
53
+ now uses `tsconfig.build.json`, which leaves `src/__tests__` out, and the
54
+ tarball drops from 140 to 128 files. Nothing a consumer imports changes.
55
+
56
+ ### Development
57
+
58
+ - `@mcp-abap-adt/auth-stores` devDependency `^1.2.1`, which depends on
59
+ `@mcp-abap-adt/interfaces-auth` `^2.0.1`. The development tree now holds a
60
+ single `interfaces-auth` 2.0.1 instead of a second, nested 1.2.0. Nothing
61
+ published changes.
62
+ - The live authorization tests take tokens from any session file:
63
+ `MCP_ABAP_ADT_SESSION_FILE`, or `session_path` in `tests/test-config.yaml`,
64
+ or the stores' folder `<destination_dir>/sessions/<destination>.env`. The
65
+ folder defaults to `~/.config/mcp-abap-adt` on Unix and
66
+ `Documents/mcp-abap-adt` on Windows. Browser logins run only with
67
+ `interactive_login: true` or `MCP_ABAP_ADT_INTERACTIVE=1`, so a default
68
+ `npm test` never waits for one.
69
+
70
+ ## [4.1.0] - 2026-09-26
71
+
72
+ Three refusals get stricter; no export, configuration field or error class
73
+ changes. The README's *Upgrading from 4.0 to 4.1* lists what now fails.
74
+
75
+ ### Changed
76
+
77
+ - **Every refusal names its rule.** No two rules under one `check` share a
78
+ message. An element that must appear exactly once says which way it failed
79
+ — absent, or more than one — for the Response's direct-child `Assertion`,
80
+ each signature's `ds:Reference`, `Status`, `StatusCode`, the assertion's
81
+ `Issuer`, `Conditions`, `Subject` and each bearer candidate's
82
+ `SubjectConfirmationData`. An empty `Issuer`, a `StatusCode` without a
83
+ `Value`, an `AudienceRestriction` naming no audience, and an absent versus
84
+ an invalid `Conditions/@NotOnOrAfter` each get their own message. The
85
+ signed-node refusal names the element the validator requires
86
+ (`samlp:Response` or `saml:Assertion`). `check` is unchanged for every
87
+ refusal, and every document 4.0.0 refused is still refused; code matching
88
+ on message text must match on `check`. The README lists every message
89
+ the shipped validators produce.
90
+ - **`bearerConfirmation` says why every candidate failed.** Still
91
+ existential: one confirmation passing every sub-rule is enough, and every
92
+ candidate is evaluated. A refusal lists each candidate in document order
93
+ with the first sub-rule it failed, in a fixed order, at most five, then
94
+ `and N more`. A `Subject` absent or repeated, and a `Subject` holding no
95
+ `SubjectConfirmation`, have messages of their own.
96
+ - **Every document value a message interpolates is quoted and cut**
97
+ (`quoteUntrusted`: JSON-quoted, 64 characters at most), including the
98
+ post-signature `Status` code, issuer and `Destination`, the `Conditions`
99
+ dates, xml-crypto's own messages about a malformed `Signature`, which embed
100
+ the offending element, and the parser message in `Saml2BearerProvider`'s
101
+ bearer conversion.
102
+
103
+ ### Fixed
104
+
105
+ - A SAML 1.x `Assertion` (`urn:oasis:names:tc:SAML:1.0:assertion`) outside
106
+ the signed assertion is refused at `signedNode`, as a SAML 2.0 one already
107
+ was. Before, one in `Extensions` passed either validator.
108
+ - An `Assertion` or `EncryptedAssertion` (SAML 2.0) or a SAML 1.x `Assertion`
109
+ inside a `ds:Signature` is refused at `signedNode`. An enveloped signature
110
+ leaves its own subtree out of the digest, so an element in `ds:Object`
111
+ there is unsigned however deep inside the signed assertion it sits; before,
112
+ such an element inside the signed assertion's own `ds:Signature` passed
113
+ either validator, `Saml2BearerProvider`'s default included.
114
+ - `idpInitiated: true` with `authnRequestId` is refused when the provider is
115
+ constructed — a `ValidationError` with `missingFields: ['idpInitiated']` —
116
+ not after `authorize()` returns, so before the user has been through the
117
+ browser. A `Saml2BearerProvider` seeded with a `refreshToken` did work in 4.0
118
+ until that token lapsed, since a refresh never reaches the strategy; it now
119
+ fails at construction.
120
+ - A malformed `Signature` whose `loadSignature` throws something other than
121
+ an `Error` is refused at `signature` quoting what was thrown. Before, the
122
+ refusal carried an empty message, and a thrown `null` or `undefined`
123
+ escaped as a `TypeError` instead of an `AssertionValidationError`.
124
+
125
+ ### Removed
126
+
127
+ - The `bin` commands `auth-authorization-code` and `auth-client-credentials`
128
+ (the `bin` field, and `bin` in `files`), and the devDependency `tsx`. They
129
+ never ran from an npm install: they were `.ts` files with a `tsx` shebang,
130
+ importing `src/`, which is not published. Not a breaking change for that
131
+ reason.
132
+
133
+ ### Documentation
134
+
135
+ - `ValidatedAssertion.nameId` is `undefined` when the `Subject` carries no
136
+ `NameID` or more than one. `NameID` is surfaced, never refused.
137
+
138
+ ### Development
139
+
140
+ - `@mcp-abap-adt/interfaces-auth` `^2.0.1`, whose
141
+ `IAssertionReplayStore.recordIfUnseen` JSDoc now describes the retention this
142
+ package implements: until the last instant a validator would still accept
143
+ the assertion, not its expiry.
144
+ - `@mcp-abap-adt/auth-stores` stays a devDependency: three test suites import
145
+ `AbapServiceKeyStore` from it.
146
+ - The end-to-end SAML suite (`samlValidation.test.ts`) runs about 6× faster,
147
+ about 41 s down to about 7 s: it starts one mock identity provider per
148
+ signed element in `beforeAll` and switches variants, instead of one per
149
+ test, each generating an RSA key. Every test still gets its own replay
150
+ store.
151
+ - The Keycloak suite trusts only the certificates under `KeyDescriptor
152
+ use="signing"` in Keycloak's metadata.
153
+
10
154
  ## [4.0.0] - 2026-09-25
11
155
 
12
156
  ### Breaking
package/README.md CHANGED
@@ -43,7 +43,8 @@ and whether it has been seen before — and must therefore be told which identit
43
43
  provider to trust. This is a breaking change: a 3.x SAML configuration fails at
44
44
  construction. See [SAML assertion validation](#saml-assertion-validation).
45
45
 
46
- If you are on an earlier major, see
46
+ If you are on an earlier version, see
47
+ [Upgrading from 4.0 to 4.1](#upgrading-from-40-to-41),
47
48
  [Migrating from 3.x to 4.0](#migrating-from-3x-to-40),
48
49
  [Migrating from 2.x to 3.0](#migrating-from-2x-to-30) and
49
50
  [Migrating from 1.x to 2.0](#migrating-from-1x-to-20).
@@ -609,17 +610,17 @@ names the row. Rows marked *(signed-Response only)* are not performed by
609
610
  |---|---|---|---|
610
611
  | 1 | Parses as XML, with no `DOCTYPE`; the document element is `samlp:Response` — or, for the assertion-only validator, a bare `saml:Assertion` | it is not, or it carries a `<!DOCTYPE` declaration | `document` |
611
612
  | 1b | Every `ID` attribute in the document is unique | any value appears twice | `duplicateId` |
612
- | 2 | Every signature is valid against `idpCertificates` — never against a certificate the document carries in its own `KeyInfo` | none, wrong key, content altered after signing, more than one reference, a reference outside the document, or a signature not inside the element it references | `signature` |
613
- | 3 | The signed node is the node read | the signature does not cover the element this validator requires — the `Response`, or the bare root `Assertion` or the Response's direct-child `Assertion` — the response does not hold exactly one `Assertion`, or any `saml:Assertion` / `saml:EncryptedAssertion` lies outside the signed assertion | `signedNode` |
614
- | 4 | `samlp:Status` *(signed-Response only)* | absent, or its `StatusCode` is not `…:status:Success` | `status` |
613
+ | 2 | Every signature is valid against `idpCertificates` — never against a certificate the document carries in its own `KeyInfo` | none, wrong key, content altered after signing, a malformed `Signature` element, no `ds:Reference` or more than one, a reference that is not same-document or names no element by `ID`, or a signature not inside the element it references | `signature` |
614
+ | 3 | The signed node is the node read | the Response carries no direct-child `Assertion`, or more than one; the signature does not cover the element this validator requires — the `Response`, or the bare root `Assertion` or the Response's direct-child `Assertion`; or any SAML 2.0 `Assertion` / `EncryptedAssertion`, or SAML 1.x `Assertion`, lies outside the signed assertion or inside a `ds:Signature` | `signedNode` |
615
+ | 4 | `samlp:Status` *(signed-Response only)* | absent or more than one; its `StatusCode` absent or more than one; the `StatusCode` without a `Value`; or a `Value` other than `…:status:Success` | `status` |
615
616
  | 4b | `Assertion/@ID` | absent or empty | `assertionId` |
616
- | 5 | `Assertion/Issuer` | absent, not the expected issuer, or no expected issuer was given | `issuer` |
617
+ | 5 | `Assertion/Issuer` | absent, more than one, empty, not the expected issuer, or no expected issuer was given | `issuer` |
617
618
  | 5b | `Response/Issuer` *(signed-Response only; optional)* | present and disagreeing with `Assertion/Issuer`, or present twice — absent is accepted | `issuer` |
618
- | 6 | `Conditions` | absent | `conditions` |
619
+ | 6 | `Conditions` | absent, or more than one | `conditions` |
619
620
  | 7 | `Conditions/@NotBefore` *(optional)* | present and not a valid `xsd:dateTime`, or in the future beyond `clockSkewMs` — absent is accepted | `notBefore` |
620
621
  | 8 | `Conditions/@NotOnOrAfter` | absent, not a valid `xsd:dateTime`, or in the past beyond `clockSkewMs` | `notOnOrAfter` |
621
- | 9 | `Conditions/AudienceRestriction` | absent, or **any one** restriction fails to name `spEntityId` | `audience` |
622
- | 10 | One bearer `SubjectConfirmation` | no single confirmation satisfies every part of it — see below | `bearerConfirmation` |
622
+ | 9 | `Conditions/AudienceRestriction` | absent, **any one** restriction naming no `Audience` at all, or **any one** failing to name `spEntityId` | `audience` |
623
+ | 10 | One bearer `SubjectConfirmation` | the `Subject` absent or more than one, holding no `SubjectConfirmation`, or no confirmation satisfying every part — see below | `bearerConfirmation` |
623
624
  | 11 | `Response/@Destination` *(signed-Response only)* | absent, or not the ACS the response arrived at | `destination` |
624
625
  | 12 | Replay | the store has already recorded this `{issuer, ID}` | `replay` |
625
626
 
@@ -643,10 +644,13 @@ What the table compresses:
643
644
  the validator resolves which element each signature covers and reads the
644
645
  assertion's fields from that element only. And since a payload travels on
645
646
  whole — `Saml2PureProvider` hands it to `cookieProvider` — **every**
646
- SAML-namespace `Assertion` or `EncryptedAssertion` anywhere in the document
647
- must be the signed assertion or inside it, under both validators. An extra
648
- assertion in `Extensions`, a sibling or a wrapper ends the login rather than
649
- being ignored. Encrypted assertions are not supported.
647
+ SAML 2.0 `Assertion` or `EncryptedAssertion`, and every SAML 1.x
648
+ `Assertion` (`urn:oasis:names:tc:SAML:1.0:assertion`), anywhere in the
649
+ document must be the signed assertion or inside it, under both validators,
650
+ but never inside a `ds:Signature`, whose subtree an enveloped signature
651
+ leaves unsigned. An extra assertion in `Extensions`, a sibling or a wrapper
652
+ ends the login rather than being ignored. Encrypted assertions are not
653
+ supported.
650
654
  - **Several signatures are accepted** when every one verifies against
651
655
  `idpCertificates`, carries exactly one same-document reference, and sits
652
656
  directly inside the element it references. A signature that fails refuses the
@@ -655,14 +659,21 @@ What the table compresses:
655
659
  - **Unique IDs (1b)** are the wrapping defence again: XML-DSig resolves its
656
660
  reference by `ID`, so a duplicate makes "which element is signed" ambiguous.
657
661
  They are refused wherever they appear, before any reference is resolved.
658
- - **Check 10 is one element, not four fields.** There must be a single
659
- `SubjectConfirmation` with `Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"`,
660
- under exactly one `Subject`, whose own `SubjectConfirmationData` satisfies
661
- all of: `InResponseTo` equal to the expected request ID — or **absent** for a
662
- login declared `idpInitiated`; `Recipient` equal to the ACS the response
663
- arrived at; `NotOnOrAfter` present, a valid `xsd:dateTime` and not past beyond
664
- `clockSkewMs`; `NotBefore`, if present, not in the future beyond it. Values
665
- scattered across several confirmations do not add up to one.
662
+ - **Check 10 is one element, not four fields — and one is enough.** The
663
+ assertion must carry exactly one `Subject`, holding at least one
664
+ `SubjectConfirmation`. It is accepted when **at least one** confirmation
665
+ passes every sub-rule on its own; values scattered across several
666
+ confirmations do not add up to one. A candidate's sub-rules, in the order
667
+ they are evaluated: `Method` is `urn:oasis:names:tc:SAML:2.0:cm:bearer`; it
668
+ holds exactly one `SubjectConfirmationData`; `InResponseTo` equals the
669
+ expected request ID — or is **absent** for a login declared `idpInitiated`;
670
+ `Recipient` equals the ACS the response arrived at; `NotOnOrAfter` is
671
+ present and a valid `xsd:dateTime`; `NotBefore`, if present, is a valid
672
+ `xsd:dateTime`; `NotOnOrAfter` has not passed beyond `clockSkewMs`;
673
+ `NotBefore` has arrived within it. When none qualifies, the refusal names
674
+ every candidate in document order with the first sub-rule it failed —
675
+ `no bearer confirmation qualifies: #1 Recipient is not the ACS; #2
676
+ NotOnOrAfter has passed` — listing at most five, then `and N more`.
666
677
  - **Check 9 is AND across restrictions, OR within one**, as SAML Core §2.5.1.4
667
678
  says: every `AudienceRestriction` must name you; the `Audience` elements
668
679
  inside one are alternatives.
@@ -686,6 +697,98 @@ What the table compresses:
686
697
  delegating to a shipped validator — and keep `idpEntityId` configured, since
687
698
  the shipped validator inside still refuses without an expected issuer.
688
699
 
700
+ #### Refusal messages
701
+
702
+ Since 4.1.0 no two rules under one `check` share a message, and an element
703
+ that must appear exactly once says which way it failed — absent, or more than
704
+ one. Every value a message takes from the document is JSON-quoted and cut to
705
+ 64 characters, so a newline smuggled in as `&#10;` shows as `\n` and cannot
706
+ forge a log line. Match on `check` in code; the message is for the person
707
+ reading the log. `<n>` is a count of two or more; `"…"` is a quoted document
708
+ value.
709
+
710
+ | `check` | message |
711
+ |---|---|
712
+ | `document` | `the SAMLResponse carries a DOCTYPE declaration, which is never accepted` |
713
+ | `document` | `the SAMLResponse did not parse as XML` |
714
+ | `document` | `expected a samlp:Response or a saml:Assertion, got "…"` |
715
+ | `document` | `expected the document element to be a samlp:Response, got "…"` |
716
+ | `duplicateId` | `the document uses the ID "…" more than once, so which element is signed is ambiguous` |
717
+ | `signature` | `the document carries no signature` |
718
+ | `signature` | `the signature element is malformed: "…"` |
719
+ | `signature` | `the signature does not verify against any configured certificate` |
720
+ | `signature` | `the signature carries no ds:Reference` |
721
+ | `signature` | `the signature carries <n> ds:Reference; exactly one is allowed` |
722
+ | `signature` | `the signature reference is not a same-document URI: "…"` |
723
+ | `signature` | `the signature references "…", which is not in the document` |
724
+ | `signature` | `the signature is not inside the element it references, so it does not envelope it` |
725
+ | `signedNode` | `the response carries no direct-child saml:Assertion` |
726
+ | `signedNode` | `the response carries <n> direct-child saml:Assertion; exactly one is allowed` |
727
+ | `signedNode` | `the signature does not cover the samlp:Response this validator requires` |
728
+ | `signedNode` | `the signature does not cover the saml:Assertion this validator requires` |
729
+ | `signedNode` | `the document carries an Assertion or EncryptedAssertion, SAML 2.0 or 1.x, outside the one the signature covers` |
730
+ | `signedNode` | `the document carries an Assertion or EncryptedAssertion inside a ds:Signature, which is never accepted` |
731
+ | `status` | `the response carries no samlp:Status` |
732
+ | `status` | `the response carries <n> samlp:Status; exactly one is allowed` |
733
+ | `status` | `the samlp:Status carries no samlp:StatusCode` |
734
+ | `status` | `the samlp:Status carries <n> samlp:StatusCode; exactly one is allowed` |
735
+ | `status` | `the samlp:StatusCode carries no Value` |
736
+ | `status` | `the identity provider declined the login: "…"` |
737
+ | `assertionId` | `the assertion carries no ID` |
738
+ | `issuer` | `the assertion carries no saml:Issuer` |
739
+ | `issuer` | `the assertion carries <n> saml:Issuer; exactly one is allowed` |
740
+ | `issuer` | `the assertion's saml:Issuer is empty` |
741
+ | `issuer` | `no expectedIssuer was configured, so the assertion issuer cannot be trusted` |
742
+ | `issuer` | `the assertion was issued by "…", not the trusted issuer` |
743
+ | `issuer` | `the response must carry at most one saml:Issuer` |
744
+ | `issuer` | `the response and the assertion name different issuers` |
745
+ | `conditions` | `the assertion carries no saml:Conditions` |
746
+ | `conditions` | `the assertion carries <n> saml:Conditions; exactly one is allowed` |
747
+ | `notBefore` | `Conditions NotBefore is not a valid xsd:dateTime: "…"` |
748
+ | `notBefore` | `the assertion is not valid yet` |
749
+ | `notOnOrAfter` | `Conditions carries no NotOnOrAfter, so the assertion states no lifetime` |
750
+ | `notOnOrAfter` | `Conditions NotOnOrAfter is not a valid xsd:dateTime: "…"` |
751
+ | `notOnOrAfter` | `the assertion has expired` |
752
+ | `audience` | `the assertion restricts no audience` |
753
+ | `audience` | `an AudienceRestriction names no audience` |
754
+ | `audience` | `an AudienceRestriction on this assertion does not name us` |
755
+ | `bearerConfirmation` | `the assertion carries no saml:Subject` |
756
+ | `bearerConfirmation` | `the assertion carries <n> saml:Subject; exactly one is allowed` |
757
+ | `bearerConfirmation` | `the saml:Subject holds no SubjectConfirmation` |
758
+ | `bearerConfirmation` | `no bearer confirmation qualifies: #1 <reason>; #2 <reason>; …` |
759
+ | `destination` | `the response carries no Destination` |
760
+ | `destination` | `the response is addressed to "…", not to us` |
761
+ | `replay` | `this assertion has been presented before` |
762
+
763
+ A `bearerConfirmation` refusal naming candidates lists each one's first failed
764
+ sub-rule, in document order, joined by `; `; past five candidates it ends
765
+ `; and N more`, N being how many were not listed. The eleven reasons:
766
+
767
+ | # | `<reason>`, in the order a candidate is tested |
768
+ |---|---|
769
+ | 1 | `Method is not bearer` |
770
+ | 2 | `carries no SubjectConfirmationData` |
771
+ | 3 | `carries <n> SubjectConfirmationData; exactly one is allowed` |
772
+ | 4 | `InResponseTo is present, but this login sent no request` |
773
+ | 5 | `InResponseTo does not answer our request` |
774
+ | 6 | `Recipient is not the ACS` |
775
+ | 7 | `SubjectConfirmationData has no NotOnOrAfter` |
776
+ | 8 | `SubjectConfirmationData NotOnOrAfter is not a valid xsd:dateTime` |
777
+ | 9 | `SubjectConfirmationData NotBefore is not a valid xsd:dateTime` |
778
+ | 10 | `NotOnOrAfter has passed` |
779
+ | 11 | `NotBefore has not arrived` |
780
+
781
+ Two messages from outside a validator changed in 4.1.0 and carry no `check`.
782
+ A provider configured with both `idpInitiated: true` and `authnRequestId`
783
+ throws a `ValidationError` (`missingFields: ['idpInitiated']`) at
784
+ construction: `SAML idpInitiated is true and authnRequestId is set: an
785
+ IdP-initiated login sends no request, so the two describe different logins.
786
+ Remove one of them.` And `Saml2BearerProvider`'s conversion of a validated
787
+ payload into the bearer grant's Assertion throws a plain `Error` whose parser
788
+ text is quoted the same way — reachable only when a custom validator accepted
789
+ a payload that does not parse: `SAML bearer payload is not well-formed XML:
790
+ "…"`.
791
+
689
792
  **Expiry comes from the verified document.** A validated assertion's
690
793
  `expiresAt` is the earlier of `Conditions/@NotOnOrAfter` and the `NotOnOrAfter`
691
794
  of the bearer confirmation accepted — the earliest, if several qualify — so a
@@ -740,9 +843,10 @@ assertion without `InResponseTo` does not make a login IdP-initiated; only
740
843
  `idpInitiated: true` does. The other checks apply unchanged.
741
844
 
742
845
  `idpInitiated: true` together with a request ID is a configuration error too:
743
- the two describe different logins. With a declared `authnRequestId` it is a
744
- `ValidationError` (`missingFields: ['idpInitiated']`) after the strategy
745
- returns. A strategy that calls `buildAuthorizationUrl` with no
846
+ the two describe different logins. With a declared `authnRequestId` the
847
+ provider refuses at construction — a `ValidationError` (`missingFields:
848
+ ['idpInitiated']`) — before any browser opens. A strategy that calls
849
+ `buildAuthorizationUrl` with no
746
850
  `authorizationUrl` configured is refused inside the builder, before a URL — and
747
851
  so a request ID — exists: a `ValidationError` with `missingFields:
748
852
  ['authorizationUrl']`. Use a strategy that does not call the builder, and leave
@@ -810,7 +914,9 @@ must outlive it. Neither window nor tolerance cuts a hole in replay detection.
810
914
 
811
915
  You meet a `ValidatedAssertion` when you call a validator yourself or wrap one
812
916
  in an `IAssertionValidator` of your own. The shipped validators fill
813
- `expiresAt`, `assertionId`, `issuer`, `nameId` (when the `Subject` has one),
917
+ `expiresAt`, `assertionId`, `issuer`, `nameId` (the `Subject`'s `NameID`;
918
+ `undefined` when it carries none or more than one — `NameID` is surfaced,
919
+ never refused),
814
920
  `raw` and `signedXml`; they leave `sessionIndex` and `attributes` unset.
815
921
 
816
922
  - **`raw`** is the validator's input, unchanged — a `samlp:Response`, or a bare
@@ -869,7 +975,7 @@ assertion must answer it; absent, the assertion must carry no `InResponseTo`.
869
975
  | Error | When |
870
976
  |---|---|
871
977
  | `AssertionValidationError` | an assertion was refused. `check` (type `AssertionCheck`) names the row above — tell "your IdP declined" (`status`) from "not addressed to us" (`audience`, `bearerConfirmation`, `destination`) without parsing the message. `code` is `'ASSERTION_VALIDATION_ERROR'` (`ASSERTION_ERROR_CODES.VALIDATION_ERROR` from `@mcp-abap-adt/interfaces-auth`) |
872
- | `ValidationError` | configuration: `idpCertificates` or `idpEntityId` missing with no `assertionValidator`, or `idpEntityId` missing with a shipped validator supplied as `assertionValidator` (at construction); `idpInitiated` with no `authorizationUrl` and a strategy that calls `buildAuthorizationUrl` (inside the builder, before any URL is produced); `authnRequestId` missing, or `idpInitiated` combined with a declared `authnRequestId` (at login, after the strategy returns and before the assertion is read). `missingFields` names the field |
978
+ | `ValidationError` | configuration: `idpCertificates` or `idpEntityId` missing with no `assertionValidator`, or `idpEntityId` missing with a shipped validator supplied as `assertionValidator` (at construction); `idpInitiated` with no `authorizationUrl` and a strategy that calls `buildAuthorizationUrl` (inside the builder, before any URL is produced); `idpInitiated` combined with a declared `authnRequestId` (at construction); `authnRequestId` missing (at login, after the strategy returns and before the assertion is read). `missingFields` names the field |
873
979
  | `Error` | a certificate that is neither PEM nor base64 DER, or not a valid X.509 certificate; a `clockSkewMs` that is not a finite non-negative integer; and, for a shipped validator called directly, an empty `idpCertificates` (*"must not be empty"*) — all at construction. Through a provider, an empty `idpCertificates` is a `ValidationError` instead |
874
980
 
875
981
  ### With Stores
@@ -1160,6 +1266,53 @@ try {
1160
1266
 
1161
1267
  All error codes are defined in `@mcp-abap-adt/interfaces-auth` package as `TOKEN_PROVIDER_ERROR_CODES`, and `AssertionValidationError`'s as `ASSERTION_ERROR_CODES`.
1162
1268
 
1269
+ ## Upgrading from 4.0 to 4.1
1270
+
1271
+ 4.1.0 changes no export, configuration field or error class. It closes what
1272
+ the 4.0.0 reviews deferred, and three refusals get stricter.
1273
+
1274
+ **What now fails that used to pass:**
1275
+
1276
+ - a document carrying a SAML 1.x `Assertion`
1277
+ (`urn:oasis:names:tc:SAML:1.0:assertion`) outside the signed assertion — in
1278
+ an unsigned `Extensions`, say — under either validator (`signedNode`). 4.0
1279
+ refused a stray SAML 2.0 assertion but not a SAML 1.x one;
1280
+ - a document carrying an `Assertion` or `EncryptedAssertion` (SAML 2.0) or a
1281
+ SAML 1.x `Assertion` inside a `ds:Signature` — in `ds:Object`, say — under
1282
+ either validator (`signedNode`). An enveloped signature leaves its own
1283
+ subtree unsigned, and 4.0's assertion-only validator accepted such an
1284
+ element inside the signed assertion's signature, `Saml2BearerProvider`'s
1285
+ default included;
1286
+ - a provider configured with both `idpInitiated: true` and `authnRequestId`.
1287
+ Its constructor now throws a `ValidationError` (`missingFields:
1288
+ ['idpInitiated']`); in 4.0 it constructed, and a `ValidationError` with the
1289
+ same `missingFields` came only after `authorize()` returned. It could never
1290
+ log in. A `Saml2BearerProvider` seeded with a `refreshToken` did work in 4.0
1291
+ until that token lapsed, since a refresh never reaches the strategy; it now
1292
+ fails at construction.
1293
+
1294
+ Nothing else 4.0 accepted is refused now, and no refusal moved to a different
1295
+ `check`. Every other count rule — one `ds:Reference` per signature, one
1296
+ `Status`, `StatusCode`, `Issuer`, `Conditions`, `Subject` and
1297
+ `SubjectConfirmationData`, an `AudienceRestriction` naming an `Audience` —
1298
+ refused the same documents in 4.0; only its message is new.
1299
+
1300
+ **Also changed:**
1301
+
1302
+ - **Refusal messages are reworded** so each names its rule — see
1303
+ [Refusal messages](#refusal-messages). `check` is unchanged for every
1304
+ refusal. Code matching on message text must match on `check` instead.
1305
+ - `bearerConfirmation` refusals list why each candidate failed, in document
1306
+ order, at most five.
1307
+ - Values from the document are quoted and cut in every message, including
1308
+ the `Status` code, the issuer, `Destination`, the `Conditions` dates,
1309
+ xml-crypto's own messages about a malformed signature, and the parser
1310
+ message in `Saml2BearerProvider`'s bearer conversion.
1311
+ - The `bin` commands `auth-authorization-code` and `auth-client-credentials`
1312
+ are removed. They never ran from an npm install: they pointed at `.ts`
1313
+ files needing `tsx`, a devDependency, and imported `src/`, which is not
1314
+ published.
1315
+
1163
1316
  ## Migrating from 3.x to 4.0
1164
1317
 
1165
1318
  4.0.0 changes nothing outside the two SAML providers. For those, it validates
@@ -1615,14 +1768,14 @@ Example output:
1615
1768
  ```
1616
1769
 
1617
1770
  **Logging Features**:
1618
- - **Token Formatting**: Tokens are logged in truncated format (start...end) for security
1771
+ - **No tokens in logs**: a token the provider holds or sent is never logged, not even in part. A log line carries only `<redacted, N chars>` (since 4.1.2; earlier versions logged a short refresh token whole). A token endpoint's error body contributes only `error` and `error_description`, with the request's secrets and anything shaped like a JWT redacted. A new opaque token that a server writes into `error_description` cannot be recognised and passes through, capped at 512 characters.
1619
1772
  - **Date Formatting**: Expiration dates are displayed in readable format (YYYY-MM-DD HH:MM:SS UTC) instead of ISO format
1620
1773
  - **Browser Information**: Logs browser type and authorization URL for debugging
1621
1774
  - **Token Lifecycle**: Detailed logging of token acquisition, validation, and refresh operations
1622
1775
 
1623
1776
  ## Dependencies
1624
1777
 
1625
- - `@mcp-abap-adt/interfaces-auth` (^2.0.0) - Token provider, authorization and assertion-validation contracts (`ITokenProvider`, `IAuthorizationStrategy`, `CallbackServerFactory`, `IAssertionValidator`, `IAssertionReplayStore`) and error code constants
1778
+ - `@mcp-abap-adt/interfaces-auth` (^2.0.1) - Token provider, authorization and assertion-validation contracts (`ITokenProvider`, `IAuthorizationStrategy`, `CallbackServerFactory`, `IAssertionValidator`, `IAssertionReplayStore`) and error code constants
1626
1779
  - `@mcp-abap-adt/interfaces-auth-sap` (^1.0.1) - XSUAA authorization configuration (`IAuthorizationConfig`)
1627
1780
  - `@mcp-abap-adt/interfaces-utils` (^1.1.0) - `ILogger`
1628
1781
  - `@xmldom/xmldom` - XML parsing: SAML assertion validation, and taking the Assertion out of a SAMLResponse for the saml2-bearer grant
@@ -1 +1 @@
1
- {"version":3,"file":"clientCredentialsAuth.d.ts","sourceRoot":"","sources":["../../src/auth/clientCredentialsAuth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAmDlC"}
1
+ {"version":3,"file":"clientCredentialsAuth.d.ts","sourceRoot":"","sources":["../../src/auth/clientCredentialsAuth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAmDlC"}
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getTokenWithClientCredentials = getTokenWithClientCredentials;
13
13
  const axios_1 = __importDefault(require("axios"));
14
+ const oauthErrorBody_1 = require("./oauthErrorBody");
14
15
  /**
15
16
  * Get access token using client_credentials grant type
16
17
  * @param uaaUrl UAA URL (e.g., https://your-account.authentication.eu10.hana.ondemand.com)
@@ -54,7 +55,7 @@ async function getTokenWithClientCredentials(uaaUrl, clientId, clientSecret) {
54
55
  'status' in error.response &&
55
56
  'data' in error.response) {
56
57
  const axiosError = error;
57
- throw new Error(`Client credentials authentication failed (${axiosError.response.status}): ${JSON.stringify(axiosError.response.data)}`);
58
+ throw new Error(`Client credentials authentication failed (${axiosError.response.status}): ${(0, oauthErrorBody_1.describeOAuthErrorBody)(axiosError.response.data, [clientSecret])}`);
58
59
  }
59
60
  else {
60
61
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What of an OAuth error response may reach a log line or an error message.
3
+ *
4
+ * Only RFC 6749 §5.2's `error` and `error_description`, each quoted and capped.
5
+ * A token endpoint's body is otherwise untrusted: a misbehaving server can echo
6
+ * the request or return tokens in it, so it is never serialised whole.
7
+ * `error_description` is the server's human-readable diagnosis, so it keeps a
8
+ * cap long enough to stay useful (UAA explains assertion refusals in it).
9
+ */
10
+ /**
11
+ * @param knownSecrets what the request sent that must never come back out:
12
+ * the refresh token, the assertion, the client secret.
13
+ */
14
+ export declare function describeOAuthErrorBody(data: unknown, knownSecrets?: readonly (string | undefined)[]): string;
15
+ //# sourceMappingURL=oauthErrorBody.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthErrorBody.d.ts","sourceRoot":"","sources":["../../src/auth/oauthErrorBody.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAsCH;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,EACb,YAAY,GAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAO,GACjD,MAAM,CAaR"}
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * What of an OAuth error response may reach a log line or an error message.
4
+ *
5
+ * Only RFC 6749 §5.2's `error` and `error_description`, each quoted and capped.
6
+ * A token endpoint's body is otherwise untrusted: a misbehaving server can echo
7
+ * the request or return tokens in it, so it is never serialised whole.
8
+ * `error_description` is the server's human-readable diagnosis, so it keeps a
9
+ * cap long enough to stay useful (UAA explains assertion refusals in it).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.describeOAuthErrorBody = describeOAuthErrorBody;
13
+ const ERROR_CAP = 64;
14
+ const DESCRIPTION_CAP = 512;
15
+ const quote = (value, cap) => JSON.stringify(value.length > cap ? `${value.slice(0, cap)}…` : value);
16
+ /** Anything shaped like a JWT: three base64url segments, the first a header. */
17
+ const JWT_SHAPE = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
18
+ /**
19
+ * Removes what a server might echo back: every secret the request itself
20
+ * sent (a refresh token, an assertion, a client secret), in each form it may
21
+ * come back in, and any JWT.
22
+ * Every known secret is redacted, however short: nothing guarantees a client
23
+ * secret is long, and dropping a matching word from a diagnosis is the lesser
24
+ * harm.
25
+ */
26
+ function redact(text, secrets) {
27
+ let out = text;
28
+ for (const secret of secrets) {
29
+ if (!secret)
30
+ continue;
31
+ // As sent, as the request body encoded it (URLSearchParams: + / = become
32
+ // %2B %2F %3D), and percent-encoded: a server may echo any of these.
33
+ const forms = new Set([
34
+ secret,
35
+ new URLSearchParams({ s: secret }).toString().slice(2),
36
+ encodeURIComponent(secret),
37
+ ]);
38
+ for (const form of forms)
39
+ out = out.split(form).join('<redacted>');
40
+ }
41
+ return out.replace(JWT_SHAPE, '<redacted jwt>');
42
+ }
43
+ /**
44
+ * @param knownSecrets what the request sent that must never come back out:
45
+ * the refresh token, the assertion, the client secret.
46
+ */
47
+ function describeOAuthErrorBody(data, knownSecrets = []) {
48
+ if (!data || typeof data !== 'object')
49
+ return 'no error given';
50
+ const { error, error_description } = data;
51
+ const parts = [];
52
+ if (typeof error === 'string')
53
+ parts.push(quote(redact(error, knownSecrets), ERROR_CAP));
54
+ if (typeof error_description === 'string') {
55
+ parts.push(quote(redact(error_description, knownSecrets), DESCRIPTION_CAP));
56
+ }
57
+ return parts.length > 0 ? parts.join(': ') : 'no error given';
58
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"saml2TokenExchange.d.ts","sourceRoot":"","sources":["../../src/auth/saml2TokenExchange.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CAwCrC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CAwCrC"}
1
+ {"version":3,"file":"saml2TokenExchange.d.ts","sourceRoot":"","sources":["../../src/auth/saml2TokenExchange.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAI9D,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CA2CrC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CA2CrC"}
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.exchangeSamlAssertion = exchangeSamlAssertion;
10
10
  exports.refreshSamlBearerToken = refreshSamlBearerToken;
11
11
  const axios_1 = __importDefault(require("axios"));
12
+ const oauthErrorBody_1 = require("./oauthErrorBody");
12
13
  function toBasicAuth(clientId, clientSecret) {
13
14
  return Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
14
15
  }
@@ -34,7 +35,10 @@ async function exchangeSamlAssertion(samlResponse, tokenUrl, clientId, clientSec
34
35
  if (axios_1.default.isAxiosError(error)) {
35
36
  logger?.error('[SAML] Token exchange failed', {
36
37
  status: error.response?.status,
37
- data: error.response?.data,
38
+ error: (0, oauthErrorBody_1.describeOAuthErrorBody)(error.response?.data, [
39
+ samlResponse,
40
+ clientSecret,
41
+ ]),
38
42
  });
39
43
  }
40
44
  throw error;
@@ -79,7 +83,10 @@ async function refreshSamlBearerToken(refreshToken, tokenUrl, clientId, clientSe
79
83
  if (axios_1.default.isAxiosError(error)) {
80
84
  logger?.error('[SAML] Token refresh failed', {
81
85
  status: error.response?.status,
82
- data: error.response?.data,
86
+ error: (0, oauthErrorBody_1.describeOAuthErrorBody)(error.response?.data, [
87
+ refreshToken,
88
+ clientSecret,
89
+ ]),
83
90
  });
84
91
  }
85
92
  throw error;
@@ -1 +1 @@
1
- {"version":3,"file":"samlBearerAssertion.d.ts","sourceRoot":"","sources":["../../src/auth/samlBearerAssertion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAQH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAmDzD"}
1
+ {"version":3,"file":"samlBearerAssertion.d.ts","sourceRoot":"","sources":["../../src/auth/samlBearerAssertion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AASH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqDzD"}
@@ -24,6 +24,7 @@
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.toBearerAssertion = toBearerAssertion;
26
26
  const xmldom_1 = require("@xmldom/xmldom");
27
+ const signedNode_1 = require("../validation/signedNode");
27
28
  const strictXml_1 = require("./strictXml");
28
29
  const SAML_ASSERTION_NS = 'urn:oasis:names:tc:SAML:2.0:assertion';
29
30
  const SAML_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:2.0:protocol';
@@ -38,7 +39,9 @@ function toBearerAssertion(payload) {
38
39
  root = (0, strictXml_1.parseStrictXml)(xml).documentElement;
39
40
  }
40
41
  catch (error) {
41
- throw new Error(`SAML bearer payload is not well-formed XML: ${error instanceof Error ? error.message : String(error)}`);
42
+ // The parser quotes the document (an element name, for one), so its
43
+ // message is quoted and cut like any other document value in a message.
44
+ throw new Error(`SAML bearer payload is not well-formed XML: ${(0, signedNode_1.quoteUntrusted)(error instanceof Error ? error.message : String(error))}`);
42
45
  }
43
46
  if (isElement(root, SAML_ASSERTION_NS, 'Assertion')) {
44
47
  return Buffer.from(xml, 'utf8').toString('base64url');
@@ -1 +1 @@
1
- {"version":3,"file":"tokenRefresher.d.ts","sourceRoot":"","sources":["../../src/auth/tokenRefresher.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAqD7B"}
1
+ {"version":3,"file":"tokenRefresher.d.ts","sourceRoot":"","sources":["../../src/auth/tokenRefresher.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAqD7B"}
@@ -8,6 +8,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.refreshJwtToken = refreshJwtToken;
10
10
  const axios_1 = __importDefault(require("axios"));
11
+ const oauthErrorBody_1 = require("./oauthErrorBody");
11
12
  /**
12
13
  * Refreshes the access token using refresh token
13
14
  * @param refreshToken Refresh token
@@ -53,7 +54,7 @@ async function refreshJwtToken(refreshToken, uaaUrl, clientId, clientSecret) {
53
54
  'status' in error.response &&
54
55
  'data' in error.response) {
55
56
  const axiosError = error;
56
- throw new Error(`Token refresh failed (${axiosError.response.status}): ${JSON.stringify(axiosError.response.data)}`);
57
+ throw new Error(`Token refresh failed (${axiosError.response.status}): ${(0, oauthErrorBody_1.describeOAuthErrorBody)(axiosError.response.data, [refreshToken, clientSecret])}`);
57
58
  }
58
59
  else {
59
60
  const errorMessage = error instanceof Error ? error.message : String(error);