@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.
- package/CHANGELOG.md +144 -0
- package/README.md +180 -27
- package/dist/auth/clientCredentialsAuth.d.ts.map +1 -1
- package/dist/auth/clientCredentialsAuth.js +2 -1
- package/dist/auth/oauthErrorBody.d.ts +15 -0
- package/dist/auth/oauthErrorBody.d.ts.map +1 -0
- package/dist/auth/oauthErrorBody.js +58 -0
- package/dist/auth/saml2TokenExchange.d.ts.map +1 -1
- package/dist/auth/saml2TokenExchange.js +9 -2
- package/dist/auth/samlBearerAssertion.d.ts.map +1 -1
- package/dist/auth/samlBearerAssertion.js +4 -1
- package/dist/auth/tokenRefresher.d.ts.map +1 -1
- package/dist/auth/tokenRefresher.js +2 -1
- package/dist/providers/BaseTokenProvider.d.ts +3 -1
- package/dist/providers/BaseTokenProvider.d.ts.map +1 -1
- package/dist/providers/BaseTokenProvider.js +4 -4
- package/dist/providers/saml2Utils.d.ts +3 -2
- package/dist/providers/saml2Utils.d.ts.map +1 -1
- package/dist/providers/saml2Utils.js +8 -0
- package/dist/validation/assertionValidator.d.ts.map +1 -1
- package/dist/validation/assertionValidator.js +207 -132
- package/dist/validation/signedNode.d.ts +2 -2
- package/dist/validation/signedNode.js +16 -5
- package/package.json +6 -12
- package/bin/auth-authorization-code.ts +0 -147
- package/bin/auth-client-credentials.ts +0 -109
- package/bin/utils/parseConfig.ts +0 -270
- package/dist/__tests__/helpers/configHelpers.d.ts +0 -46
- package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
- package/dist/__tests__/helpers/configHelpers.js +0 -232
- package/dist/__tests__/helpers/netHelpers.d.ts +0 -19
- package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
- package/dist/__tests__/helpers/netHelpers.js +0 -92
- package/dist/__tests__/helpers/testLogger.d.ts +0 -7
- package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
- package/dist/__tests__/helpers/testLogger.js +0 -45
- package/dist/__tests__/integration/stand/formLogin.d.ts +0 -68
- package/dist/__tests__/integration/stand/formLogin.d.ts.map +0 -1
- 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
|
|
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
|
|
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
|
|
614
|
-
| 4 | `samlp:Status` *(signed-Response only)* | absent
|
|
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**
|
|
622
|
-
| 10 | One bearer `SubjectConfirmation` | no
|
|
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
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
`
|
|
665
|
-
|
|
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 ` ` 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`
|
|
744
|
-
`ValidationError` (`missingFields:
|
|
745
|
-
|
|
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` (
|
|
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); `
|
|
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
|
-
- **
|
|
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.
|
|
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;
|
|
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}): ${
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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;
|
|
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}): ${
|
|
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);
|