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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +231 -13
  3. package/dist/__tests__/integration/stand/formLogin.d.ts +68 -0
  4. package/dist/__tests__/integration/stand/formLogin.d.ts.map +1 -0
  5. package/dist/__tests__/integration/stand/formLogin.js +194 -0
  6. package/dist/auth/callbackServer.js +2 -2
  7. package/dist/auth/passcodeAuth.d.ts +25 -0
  8. package/dist/auth/passcodeAuth.d.ts.map +1 -0
  9. package/dist/auth/passcodeAuth.js +62 -0
  10. package/dist/auth/samlBearerAssertion.d.ts +24 -0
  11. package/dist/auth/samlBearerAssertion.d.ts.map +1 -0
  12. package/dist/auth/samlBearerAssertion.js +101 -0
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -2
  16. package/dist/providers/Saml2BearerProvider.d.ts.map +1 -1
  17. package/dist/providers/Saml2BearerProvider.js +4 -1
  18. package/dist/providers/UaaPasscodeProvider.d.ts +43 -0
  19. package/dist/providers/UaaPasscodeProvider.d.ts.map +1 -0
  20. package/dist/providers/UaaPasscodeProvider.js +86 -0
  21. package/dist/providers/index.d.ts +2 -2
  22. package/dist/providers/index.d.ts.map +1 -1
  23. package/dist/providers/index.js +3 -3
  24. package/dist/strategies/index.d.ts +1 -1
  25. package/dist/strategies/index.d.ts.map +1 -1
  26. package/dist/strategies/index.js +2 -1
  27. package/dist/strategies/manualStrategies.d.ts +7 -0
  28. package/dist/strategies/manualStrategies.d.ts.map +1 -1
  29. package/dist/strategies/manualStrategies.js +21 -0
  30. package/package.json +10 -5
  31. package/bin/auth-device-flow.ts +0 -114
  32. package/dist/auth/deviceFlowAuth.d.ts +0 -43
  33. package/dist/auth/deviceFlowAuth.d.ts.map +0 -1
  34. package/dist/auth/deviceFlowAuth.js +0 -168
  35. package/dist/providers/DeviceFlowProvider.d.ts +0 -32
  36. package/dist/providers/DeviceFlowProvider.d.ts.map +0 -1
  37. package/dist/providers/DeviceFlowProvider.js +0 -86
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ /**
3
+ * Plays the user in an interactive login against a stand server's own login
4
+ * page: follows redirects, keeps cookies, finds the form with a password
5
+ * field, fills it in with every hidden field it carries (UAA's CSRF token,
6
+ * Keycloak's session code), and submits it.
7
+ *
8
+ * Deliberately small: enough for UAA's and Keycloak's stock login pages, which
9
+ * the pinned image versions keep stable. It is a test helper, not a browser.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FormBrowser = void 0;
13
+ exports.authorizeByForm = authorizeByForm;
14
+ exports.approveDevice = approveDevice;
15
+ exports.samlResponseByForm = samlResponseByForm;
16
+ const MAX_HOPS = 20;
17
+ class FormBrowser {
18
+ cookies = new Map();
19
+ /** GET `url` and follow redirects until `stop(url)` or a page is served. */
20
+ async open(url, stop = () => false) {
21
+ return this.follow(url, { method: 'GET' }, stop);
22
+ }
23
+ /**
24
+ * Submit the login form on `page`, then follow redirects until `stop` says
25
+ * the next location is the one the caller wants — typically the client's
26
+ * redirect URI carrying the code — without requesting it.
27
+ */
28
+ async submitLogin(page, credentials, stop = () => false) {
29
+ const form = findPasswordForm(page.html ?? '');
30
+ if (!form) {
31
+ throw new Error(`no login form on ${page.url}`);
32
+ }
33
+ const body = new URLSearchParams(form.hidden);
34
+ body.set(form.userField, credentials.username);
35
+ body.set(form.passwordField, credentials.password);
36
+ return this.follow(new URL(form.action, page.url).toString(), {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
39
+ body: body.toString(),
40
+ }, stop);
41
+ }
42
+ /**
43
+ * Accept a consent page ("Do you grant these access privileges?"): submit
44
+ * the form that carries an `accept` button, with its hidden fields.
45
+ */
46
+ async acceptConsent(page) {
47
+ for (const match of (page.html ?? '').matchAll(/<form\b[^>]*>[\s\S]*?<\/form>/gi)) {
48
+ const formHtml = match[0];
49
+ const inputs = [...formHtml.matchAll(/<(?:input|button)\b[^>]*>/gi)].map((m) => m[0]);
50
+ const accept = inputs.find((i) => attribute(i, 'name') === 'accept');
51
+ if (!accept)
52
+ continue;
53
+ const body = new URLSearchParams();
54
+ for (const input of inputs) {
55
+ const name = attribute(input, 'name');
56
+ if (name && attribute(input, 'type') === 'hidden') {
57
+ body.set(name, attribute(input, 'value') ?? '');
58
+ }
59
+ }
60
+ body.set('accept', attribute(accept, 'value') ?? 'Yes');
61
+ const formTag = /<form\b[^>]*>/i.exec(formHtml)?.[0] ?? '';
62
+ return this.follow(new URL(attribute(formTag, 'action') ?? '', page.url).toString(), {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
65
+ body: body.toString(),
66
+ }, () => false);
67
+ }
68
+ throw new Error(`no consent form on ${page.url}`);
69
+ }
70
+ async follow(url, init, stop) {
71
+ let current = url;
72
+ let request = init;
73
+ for (let hop = 0; hop < MAX_HOPS; hop++) {
74
+ const response = await fetch(current, {
75
+ ...request,
76
+ redirect: 'manual',
77
+ headers: { ...(request.headers ?? {}), Cookie: this.cookieHeader() },
78
+ signal: AbortSignal.timeout(15_000),
79
+ });
80
+ this.remember(response);
81
+ const location = response.headers.get('location');
82
+ if (response.status >= 300 && response.status < 400 && location) {
83
+ const next = new URL(location, current).toString();
84
+ if (stop(next))
85
+ return { url: next };
86
+ current = next;
87
+ request = { method: 'GET' };
88
+ continue;
89
+ }
90
+ return { url: current, html: await response.text() };
91
+ }
92
+ throw new Error(`more than ${MAX_HOPS} redirects from ${url}`);
93
+ }
94
+ remember(response) {
95
+ for (const line of response.headers.getSetCookie()) {
96
+ const [pair] = line.split(';');
97
+ const eq = pair.indexOf('=');
98
+ if (eq > 0) {
99
+ this.cookies.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
100
+ }
101
+ }
102
+ }
103
+ cookieHeader() {
104
+ return [...this.cookies].map(([k, v]) => `${k}=${v}`).join('; ');
105
+ }
106
+ }
107
+ exports.FormBrowser = FormBrowser;
108
+ /**
109
+ * Log in through the authorization URL and return the redirect URI the server
110
+ * sends the browser back to, with its `code` — not requested, since nothing
111
+ * listens there.
112
+ */
113
+ async function authorizeByForm(authorizationUrl, redirectUri, credentials) {
114
+ const reached = (next) => next.startsWith(redirectUri);
115
+ const browser = new FormBrowser();
116
+ const page = await browser.open(authorizationUrl, reached);
117
+ const done = page.html === undefined
118
+ ? page
119
+ : await browser.submitLogin(page, credentials, reached);
120
+ if (!reached(done.url)) {
121
+ throw new Error(`login did not return to ${redirectUri}; ended at ${done.url}`);
122
+ }
123
+ return new URL(done.url);
124
+ }
125
+ /**
126
+ * Approve a device authorization the way a user would: open the verification
127
+ * URI (with the user code already in it), log in, and grant access.
128
+ */
129
+ async function approveDevice(verificationUriComplete, credentials) {
130
+ const browser = new FormBrowser();
131
+ const page = await browser.open(verificationUriComplete);
132
+ const consent = await browser.submitLogin(page, credentials);
133
+ await browser.acceptConsent(consent);
134
+ }
135
+ /**
136
+ * A SAML login at an identity provider: open the AuthnRequest URL, log in,
137
+ * and take the SAMLResponse from the auto-posting form the IdP answers with —
138
+ * what a browser would post to the assertion consumer service.
139
+ */
140
+ async function samlResponseByForm(authnRequestUrl, credentials) {
141
+ const browser = new FormBrowser();
142
+ const page = await browser.submitLogin(await browser.open(authnRequestUrl), credentials);
143
+ const html = page.html ?? '';
144
+ const input = [...html.matchAll(/<input\b[^>]*>/gi)]
145
+ .map((m) => m[0])
146
+ .find((i) => attribute(i, 'name') === 'SAMLResponse');
147
+ const samlResponse = input ? attribute(input, 'value') : undefined;
148
+ const formTag = /<form\b[^>]*>/i.exec(html)?.[0] ?? '';
149
+ if (!samlResponse) {
150
+ throw new Error(`no SAMLResponse form on ${page.url}`);
151
+ }
152
+ return { samlResponse, acsUrl: attribute(formTag, 'action') ?? '' };
153
+ }
154
+ const decode = (value) => value
155
+ .replace(/&amp;/g, '&')
156
+ .replace(/&quot;/g, '"')
157
+ .replace(/&#39;/g, "'")
158
+ .replace(/&lt;/g, '<')
159
+ .replace(/&gt;/g, '>');
160
+ const attribute = (tag, name) => {
161
+ const match = new RegExp(`\\s${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, 'i').exec(tag);
162
+ return match ? decode(match[2] ?? match[3] ?? '') : undefined;
163
+ };
164
+ function findPasswordForm(html) {
165
+ for (const match of html.matchAll(/<form\b[^>]*>[\s\S]*?<\/form>/gi)) {
166
+ const formHtml = match[0];
167
+ const inputs = [...formHtml.matchAll(/<input\b[^>]*>/gi)].map((m) => m[0]);
168
+ const password = inputs.find((i) => attribute(i, 'type') === 'password');
169
+ if (!password)
170
+ continue;
171
+ const hidden = {};
172
+ let userField = 'username';
173
+ for (const input of inputs) {
174
+ const type = (attribute(input, 'type') ?? 'text').toLowerCase();
175
+ const name = attribute(input, 'name');
176
+ if (!name)
177
+ continue;
178
+ if (type === 'hidden')
179
+ hidden[name] = attribute(input, 'value') ?? '';
180
+ if ((type === 'text' || type === 'email') &&
181
+ /user|email|login/i.test(name)) {
182
+ userField = name;
183
+ }
184
+ }
185
+ const formTag = /<form\b[^>]*>/i.exec(formHtml)?.[0] ?? '';
186
+ return {
187
+ action: attribute(formTag, 'action') ?? '',
188
+ userField,
189
+ passwordField: attribute(password, 'name') ?? 'password',
190
+ hidden,
191
+ };
192
+ }
193
+ return undefined;
194
+ }
@@ -163,8 +163,8 @@ async function runCallbackScope(options, routes, use) {
163
163
  setTimeout(() => {
164
164
  if (finished)
165
165
  return;
166
- // Grace expired — on Node 18.x `close()` does not end idle connections,
167
- // and an active one may simply be stuck. Force it, bounded.
166
+ // Grace expired — an active connection may simply be stuck. Force it,
167
+ // bounded.
168
168
  server.closeIdleConnections?.();
169
169
  server.closeAllConnections?.();
170
170
  for (const socket of sockets)
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The UAA one-time passcode exchange — what `cf login --sso` does.
3
+ *
4
+ * The user opens `<uaa>/passcode` in any browser, logs in however the
5
+ * identity zone lets them (SSO, a corporate IdP, MFA), and copies the
6
+ * "Temporary Authentication Code" shown there. That code is exchanged here
7
+ * through the password grant, with `passcode` in place of a username and
8
+ * password. It is a UAA extension, not an RFC; XSUAA inherits it.
9
+ *
10
+ * UAA hands the request to its passcode filter chain only when its Accept
11
+ * header names JSON (`passcodeTokenMatcher` accepts `application/json` or
12
+ * `application/x-www-form-urlencoded`). Otherwise it falls through to the
13
+ * ordinary password grant, which answers `invalid_client: No password
14
+ * supplied` — what a bare `fetch`, which sends `*\/*`, gets. axios's default
15
+ * Accept happens to include `application/json`; the header is set explicitly
16
+ * so the exchange does not depend on an HTTP client's defaults.
17
+ */
18
+ import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
19
+ export interface PasscodeTokens {
20
+ accessToken: string;
21
+ refreshToken?: string;
22
+ expiresIn?: number;
23
+ }
24
+ export declare function exchangePasscode(uaaUrl: string, clientId: string, clientSecret: string | undefined, passcode: string, logger?: ILogger): Promise<PasscodeTokens>;
25
+ //# sourceMappingURL=passcodeAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passcodeAuth.d.ts","sourceRoot":"","sources":["../../src/auth/passcodeAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /**
3
+ * The UAA one-time passcode exchange — what `cf login --sso` does.
4
+ *
5
+ * The user opens `<uaa>/passcode` in any browser, logs in however the
6
+ * identity zone lets them (SSO, a corporate IdP, MFA), and copies the
7
+ * "Temporary Authentication Code" shown there. That code is exchanged here
8
+ * through the password grant, with `passcode` in place of a username and
9
+ * password. It is a UAA extension, not an RFC; XSUAA inherits it.
10
+ *
11
+ * UAA hands the request to its passcode filter chain only when its Accept
12
+ * header names JSON (`passcodeTokenMatcher` accepts `application/json` or
13
+ * `application/x-www-form-urlencoded`). Otherwise it falls through to the
14
+ * ordinary password grant, which answers `invalid_client: No password
15
+ * supplied` — what a bare `fetch`, which sends `*\/*`, gets. axios's default
16
+ * Accept happens to include `application/json`; the header is set explicitly
17
+ * so the exchange does not depend on an HTTP client's defaults.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.exchangePasscode = exchangePasscode;
24
+ const axios_1 = __importDefault(require("axios"));
25
+ async function exchangePasscode(uaaUrl, clientId, clientSecret, passcode, logger) {
26
+ const tokenUrl = `${uaaUrl.replace(/\/+$/, '')}/oauth/token`;
27
+ const params = new URLSearchParams();
28
+ params.append('grant_type', 'password');
29
+ params.append('passcode', passcode);
30
+ logger?.info('[UAA] Exchanging passcode for token', { tokenUrl });
31
+ // A public client — `cf` is one — authenticates with an empty secret.
32
+ const basic = Buffer.from(`${clientId}:${clientSecret ?? ''}`).toString('base64');
33
+ let response;
34
+ try {
35
+ response = await axios_1.default.post(tokenUrl, params.toString(), {
36
+ headers: {
37
+ 'Content-Type': 'application/x-www-form-urlencoded',
38
+ Accept: 'application/json',
39
+ Authorization: `Basic ${basic}`,
40
+ },
41
+ });
42
+ }
43
+ catch (error) {
44
+ // UAA says why in the body — "Invalid passcode" for a mistyped or
45
+ // already spent code — which is what the user needs to read.
46
+ if (axios_1.default.isAxiosError(error) && error.response) {
47
+ const body = error.response.data;
48
+ const reason = body?.error_description ?? body?.error ?? 'no reason given';
49
+ throw new Error(`Passcode exchange failed (${error.response.status}): ${reason}`);
50
+ }
51
+ throw error;
52
+ }
53
+ const data = response.data;
54
+ if (!data?.access_token) {
55
+ throw new Error('Passcode exchange returned no access_token');
56
+ }
57
+ return {
58
+ accessToken: data.access_token,
59
+ refreshToken: data.refresh_token,
60
+ expiresIn: data.expires_in,
61
+ };
62
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * What the SAML 2.0 bearer grant accepts, from what a SAML login delivers.
3
+ *
4
+ * RFC 7522 §2.1: the `assertion` parameter is a single SAML 2.0 Assertion,
5
+ * base64url-encoded. An interactive login delivers something else — the
6
+ * identity provider's whole `samlp:Response`, in standard base64 — and a
7
+ * token endpoint that follows the RFC refuses it: Cloud Foundry UAA answers
8
+ * 401 to a Response in either encoding. So the Assertion is taken out of the
9
+ * Response here and re-encoded.
10
+ *
11
+ * The Assertion is serialised as an element of its own, and every namespace
12
+ * declaration it inherited from the Response is copied onto it first. A
13
+ * serializer would add back only the prefixes used in element and attribute
14
+ * names; a prefix used only inside a value — `xsi:type="xs:string"` — would be
15
+ * lost, leaving a QName that no longer resolves. Copying all of them keeps the
16
+ * Assertion's in-scope namespaces exactly what they were. Its signature, over
17
+ * exclusive canonical XML, still verifies: canonicalisation renders a
18
+ * namespace where it is used, or where the signature's InclusiveNamespaces
19
+ * names it, not where it was declared. A signature over the Response alone does not
20
+ * survive the cut; the token endpoint then refuses the Assertion, as it
21
+ * would any unsigned one.
22
+ */
23
+ export declare function toBearerAssertion(payload: string): string;
24
+ //# sourceMappingURL=samlBearerAssertion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"samlBearerAssertion.d.ts","sourceRoot":"","sources":["../../src/auth/samlBearerAssertion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAOH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAmDzD"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * What the SAML 2.0 bearer grant accepts, from what a SAML login delivers.
4
+ *
5
+ * RFC 7522 §2.1: the `assertion` parameter is a single SAML 2.0 Assertion,
6
+ * base64url-encoded. An interactive login delivers something else — the
7
+ * identity provider's whole `samlp:Response`, in standard base64 — and a
8
+ * token endpoint that follows the RFC refuses it: Cloud Foundry UAA answers
9
+ * 401 to a Response in either encoding. So the Assertion is taken out of the
10
+ * Response here and re-encoded.
11
+ *
12
+ * The Assertion is serialised as an element of its own, and every namespace
13
+ * declaration it inherited from the Response is copied onto it first. A
14
+ * serializer would add back only the prefixes used in element and attribute
15
+ * names; a prefix used only inside a value — `xsi:type="xs:string"` — would be
16
+ * lost, leaving a QName that no longer resolves. Copying all of them keeps the
17
+ * Assertion's in-scope namespaces exactly what they were. Its signature, over
18
+ * exclusive canonical XML, still verifies: canonicalisation renders a
19
+ * namespace where it is used, or where the signature's InclusiveNamespaces
20
+ * names it, not where it was declared. A signature over the Response alone does not
21
+ * survive the cut; the token endpoint then refuses the Assertion, as it
22
+ * would any unsigned one.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.toBearerAssertion = toBearerAssertion;
26
+ const xmldom_1 = require("@xmldom/xmldom");
27
+ const SAML_ASSERTION_NS = 'urn:oasis:names:tc:SAML:2.0:assertion';
28
+ const SAML_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:2.0:protocol';
29
+ function toBearerAssertion(payload) {
30
+ // Node's base64 decoder accepts both alphabets, so this reads either.
31
+ const xml = Buffer.from(payload.trim(), 'base64').toString('utf8');
32
+ if (!xml.trimStart().startsWith('<')) {
33
+ throw new Error('SAML bearer payload is not base64-encoded XML');
34
+ }
35
+ let root;
36
+ try {
37
+ root = new xmldom_1.DOMParser().parseFromString(xml, 'text/xml').documentElement;
38
+ }
39
+ catch (error) {
40
+ throw new Error(`SAML bearer payload is not well-formed XML: ${error instanceof Error ? error.message : String(error)}`);
41
+ }
42
+ if (isElement(root, SAML_ASSERTION_NS, 'Assertion')) {
43
+ return Buffer.from(xml, 'utf8').toString('base64url');
44
+ }
45
+ if (!isElement(root, SAML_PROTOCOL_NS, 'Response')) {
46
+ throw new Error('SAML bearer payload is neither a SAML Response nor an Assertion');
47
+ }
48
+ const children = childElements(root);
49
+ const assertions = children.filter((e) => isElement(e, SAML_ASSERTION_NS, 'Assertion'));
50
+ if (assertions.length === 0) {
51
+ if (children.some((e) => isElement(e, SAML_ASSERTION_NS, 'EncryptedAssertion'))) {
52
+ throw new Error('SAML Response carries only an EncryptedAssertion; encrypted Assertions are not supported');
53
+ }
54
+ throw new Error('SAML Response carries no Assertion');
55
+ }
56
+ if (assertions.length > 1) {
57
+ throw new Error(`SAML Response carries ${assertions.length} Assertions; a bearer grant takes one`);
58
+ }
59
+ const assertion = assertions[0];
60
+ declareInheritedNamespaces(assertion);
61
+ const serialized = new xmldom_1.XMLSerializer().serializeToString(assertion);
62
+ return Buffer.from(serialized, 'utf8').toString('base64url');
63
+ }
64
+ function isElement(node, namespace, localName) {
65
+ return (!!node && node.namespaceURI === namespace && node.localName === localName);
66
+ }
67
+ const XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
68
+ const isNamespaceDeclaration = (name) => name === 'xmlns' || name.startsWith('xmlns:');
69
+ /**
70
+ * Copies onto `element` every namespace declaration in scope from its
71
+ * ancestors that it does not make itself. Ancestors are walked innermost
72
+ * first, so the nearest declaration of a prefix wins, as it did in place.
73
+ */
74
+ function declareInheritedNamespaces(element) {
75
+ const declared = new Set();
76
+ for (let i = 0; i < element.attributes.length; i++) {
77
+ const name = element.attributes.item(i)?.name;
78
+ if (name && isNamespaceDeclaration(name))
79
+ declared.add(name);
80
+ }
81
+ for (let ancestor = element.parentNode; ancestor && ancestor.nodeType === 1; ancestor = ancestor.parentNode) {
82
+ const attributes = ancestor.attributes;
83
+ for (let i = 0; i < attributes.length; i++) {
84
+ const attribute = attributes.item(i);
85
+ if (attribute &&
86
+ isNamespaceDeclaration(attribute.name) &&
87
+ !declared.has(attribute.name)) {
88
+ element.setAttributeNS(XMLNS_NS, attribute.name, attribute.value);
89
+ declared.add(attribute.name);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ function childElements(parent) {
95
+ const out = [];
96
+ for (let n = parent.firstChild; n; n = n.nextSibling) {
97
+ if (n.nodeType === 1)
98
+ out.push(n);
99
+ }
100
+ return out;
101
+ }
package/dist/index.d.ts CHANGED
@@ -9,10 +9,10 @@ export type { OidcCallbackResult } from './auth/oidcBrowserAuth';
9
9
  export { withOidcCallbackServer } from './auth/oidcBrowserAuth';
10
10
  export { withSamlCallbackServer } from './auth/saml2Auth';
11
11
  export { BrowserAuthError, RefreshError, ServiceKeyError, SessionDataError, TokenProviderError, ValidationError, } from './errors/TokenProviderErrors';
12
- export type { AuthorizationCodeProviderConfig, ClientCredentialsProviderConfig, DeviceFlowProviderConfig, OidcBrowserProviderConfig, OidcDeviceFlowProviderConfig, OidcPasswordProviderConfig, OidcTokenExchangeProviderConfig, Saml2BearerProviderConfig, Saml2PureProviderConfig, } from './providers';
13
- export { AuthorizationCodeProvider, BaseTokenProvider, ClientCredentialsProvider, DeviceFlowProvider, OidcBrowserProvider, OidcDeviceFlowProvider, OidcPasswordProvider, OidcTokenExchangeProvider, Saml2BearerProvider, Saml2PureProvider, } from './providers';
12
+ export type { AuthorizationCodeProviderConfig, ClientCredentialsProviderConfig, OidcBrowserProviderConfig, OidcDeviceFlowProviderConfig, OidcPasswordProviderConfig, OidcTokenExchangeProviderConfig, Saml2BearerProviderConfig, Saml2PureProviderConfig, UaaPasscodeProviderConfig, } from './providers';
13
+ export { AuthorizationCodeProvider, BaseTokenProvider, ClientCredentialsProvider, OidcBrowserProvider, OidcDeviceFlowProvider, OidcPasswordProvider, OidcTokenExchangeProvider, Saml2BearerProvider, Saml2PureProvider, UaaPasscodeProvider, } from './providers';
14
14
  export { SsoProviderFactory } from './sso/SsoProviderFactory';
15
15
  export type { SsoProviderConfig, SsoProviderInstance } from './sso/types';
16
16
  export type { BrowserCallbackStrategyOptions, CallbackStrategyOptions, ExternalCodeStrategyOptions, ManualStrategyOptions, StaticCodeStrategyOptions, } from './strategies';
17
- export { asOidcResult, BrowserCallbackStrategy, browserCallbackStrategy, DEFAULT_CALLBACK_PORT, DEFAULT_LOGIN_TIMEOUT_MS, externalCodeStrategy, manualPasteStrategy, manualSamlResponseStrategy, oidcCallbackStrategy, samlCallbackStrategy, staticCodeStrategy, } from './strategies';
17
+ export { asOidcResult, BrowserCallbackStrategy, browserCallbackStrategy, DEFAULT_CALLBACK_PORT, DEFAULT_LOGIN_TIMEOUT_MS, externalCodeStrategy, manualPasscodeStrategy, manualPasteStrategy, manualSamlResponseStrategy, oidcCallbackStrategy, samlCallbackStrategy, staticCodeStrategy, } from './strategies';
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,GAChB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,+BAA+B,EAC/B,+BAA+B,EAC/B,wBAAwB,EACxB,yBAAyB,EACzB,4BAA4B,EAC5B,0BAA0B,EAC1B,+BAA+B,EAC/B,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC1E,YAAY,EACV,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,EAC3B,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,GAChB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,+BAA+B,EAC/B,+BAA+B,EAC/B,yBAAyB,EACzB,4BAA4B,EAC5B,0BAA0B,EAC1B,+BAA+B,EAC/B,yBAAyB,EACzB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC1E,YAAY,EACV,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,EAC3B,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Provides token providers
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.staticCodeStrategy = exports.samlCallbackStrategy = exports.oidcCallbackStrategy = exports.manualSamlResponseStrategy = exports.manualPasteStrategy = exports.externalCodeStrategy = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.DEFAULT_CALLBACK_PORT = exports.browserCallbackStrategy = exports.BrowserCallbackStrategy = exports.asOidcResult = exports.SsoProviderFactory = exports.Saml2PureProvider = exports.Saml2BearerProvider = exports.OidcTokenExchangeProvider = exports.OidcPasswordProvider = exports.OidcDeviceFlowProvider = exports.OidcBrowserProvider = exports.DeviceFlowProvider = exports.ClientCredentialsProvider = exports.BaseTokenProvider = exports.AuthorizationCodeProvider = exports.ValidationError = exports.TokenProviderError = exports.SessionDataError = exports.ServiceKeyError = exports.RefreshError = exports.BrowserAuthError = exports.withSamlCallbackServer = exports.withOidcCallbackServer = exports.withBrowserCallbackServer = void 0;
9
+ exports.staticCodeStrategy = exports.samlCallbackStrategy = exports.oidcCallbackStrategy = exports.manualSamlResponseStrategy = exports.manualPasteStrategy = exports.manualPasscodeStrategy = exports.externalCodeStrategy = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.DEFAULT_CALLBACK_PORT = exports.browserCallbackStrategy = exports.BrowserCallbackStrategy = exports.asOidcResult = exports.SsoProviderFactory = exports.UaaPasscodeProvider = exports.Saml2PureProvider = exports.Saml2BearerProvider = exports.OidcTokenExchangeProvider = exports.OidcPasswordProvider = exports.OidcDeviceFlowProvider = exports.OidcBrowserProvider = exports.ClientCredentialsProvider = exports.BaseTokenProvider = exports.AuthorizationCodeProvider = exports.ValidationError = exports.TokenProviderError = exports.SessionDataError = exports.ServiceKeyError = exports.RefreshError = exports.BrowserAuthError = exports.withSamlCallbackServer = exports.withOidcCallbackServer = exports.withBrowserCallbackServer = void 0;
10
10
  // Callback server factories — "take the transport this package gives".
11
11
  var callbackServer_1 = require("./auth/callbackServer");
12
12
  Object.defineProperty(exports, "withBrowserCallbackServer", { enumerable: true, get: function () { return callbackServer_1.withBrowserCallbackServer; } });
@@ -27,13 +27,13 @@ var providers_1 = require("./providers");
27
27
  Object.defineProperty(exports, "AuthorizationCodeProvider", { enumerable: true, get: function () { return providers_1.AuthorizationCodeProvider; } });
28
28
  Object.defineProperty(exports, "BaseTokenProvider", { enumerable: true, get: function () { return providers_1.BaseTokenProvider; } });
29
29
  Object.defineProperty(exports, "ClientCredentialsProvider", { enumerable: true, get: function () { return providers_1.ClientCredentialsProvider; } });
30
- Object.defineProperty(exports, "DeviceFlowProvider", { enumerable: true, get: function () { return providers_1.DeviceFlowProvider; } });
31
30
  Object.defineProperty(exports, "OidcBrowserProvider", { enumerable: true, get: function () { return providers_1.OidcBrowserProvider; } });
32
31
  Object.defineProperty(exports, "OidcDeviceFlowProvider", { enumerable: true, get: function () { return providers_1.OidcDeviceFlowProvider; } });
33
32
  Object.defineProperty(exports, "OidcPasswordProvider", { enumerable: true, get: function () { return providers_1.OidcPasswordProvider; } });
34
33
  Object.defineProperty(exports, "OidcTokenExchangeProvider", { enumerable: true, get: function () { return providers_1.OidcTokenExchangeProvider; } });
35
34
  Object.defineProperty(exports, "Saml2BearerProvider", { enumerable: true, get: function () { return providers_1.Saml2BearerProvider; } });
36
35
  Object.defineProperty(exports, "Saml2PureProvider", { enumerable: true, get: function () { return providers_1.Saml2PureProvider; } });
36
+ Object.defineProperty(exports, "UaaPasscodeProvider", { enumerable: true, get: function () { return providers_1.UaaPasscodeProvider; } });
37
37
  // SSO factory
38
38
  var SsoProviderFactory_1 = require("./sso/SsoProviderFactory");
39
39
  Object.defineProperty(exports, "SsoProviderFactory", { enumerable: true, get: function () { return SsoProviderFactory_1.SsoProviderFactory; } });
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "browserCallbackStrategy", { enumerable: true, ge
45
45
  Object.defineProperty(exports, "DEFAULT_CALLBACK_PORT", { enumerable: true, get: function () { return strategies_1.DEFAULT_CALLBACK_PORT; } });
46
46
  Object.defineProperty(exports, "DEFAULT_LOGIN_TIMEOUT_MS", { enumerable: true, get: function () { return strategies_1.DEFAULT_LOGIN_TIMEOUT_MS; } });
47
47
  Object.defineProperty(exports, "externalCodeStrategy", { enumerable: true, get: function () { return strategies_1.externalCodeStrategy; } });
48
+ Object.defineProperty(exports, "manualPasscodeStrategy", { enumerable: true, get: function () { return strategies_1.manualPasscodeStrategy; } });
48
49
  Object.defineProperty(exports, "manualPasteStrategy", { enumerable: true, get: function () { return strategies_1.manualPasteStrategy; } });
49
50
  Object.defineProperty(exports, "manualSamlResponseStrategy", { enumerable: true, get: function () { return strategies_1.manualSamlResponseStrategy; } });
50
51
  Object.defineProperty(exports, "oidcCallbackStrategy", { enumerable: true, get: function () { return strategies_1.oidcCallbackStrategy; } });
@@ -1 +1 @@
1
- {"version":3,"file":"Saml2BearerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/Saml2BearerProvider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAK9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,cAAc,CAAC;AAOtB,MAAM,WAAW,yBACf,SAAQ,iBAAiB,EACvB,yBAAyB;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAoB,SAAQ,iBAAiB;IACxD,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,yBAAyB;IAiB7C,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAoBrD;;;;OAIG;cACa,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAqBxD"}
1
+ {"version":3,"file":"Saml2BearerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/Saml2BearerProvider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAM9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,cAAc,CAAC;AAOtB,MAAM,WAAW,yBACf,SAAQ,iBAAiB,EACvB,yBAAyB;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAoB,SAAQ,iBAAiB;IACxD,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,yBAAyB;IAiB7C,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAsBrD;;;;OAIG;cACa,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAqBxD"}
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.Saml2BearerProvider = void 0;
9
9
  const interfaces_auth_1 = require("@mcp-abap-adt/interfaces-auth");
10
10
  const saml2TokenExchange_1 = require("../auth/saml2TokenExchange");
11
+ const samlBearerAssertion_1 = require("../auth/samlBearerAssertion");
11
12
  const BaseTokenProvider_1 = require("./BaseTokenProvider");
12
13
  const saml2Utils_1 = require("./saml2Utils");
13
14
  class Saml2BearerProvider extends BaseTokenProvider_1.BaseTokenProvider {
@@ -33,7 +34,9 @@ class Saml2BearerProvider extends BaseTokenProvider_1.BaseTokenProvider {
33
34
  async performLogin() {
34
35
  const samlResponse = await (0, saml2Utils_1.getSamlAssertion)(this.config);
35
36
  const tokenUrl = (0, saml2Utils_1.resolveTokenUrl)(this.config);
36
- const tokens = await (0, saml2TokenExchange_1.exchangeSamlAssertion)(samlResponse, tokenUrl, this.config.clientId, this.config.clientSecret, this.logger);
37
+ // RFC 7522 takes one base64url Assertion; a login delivers the whole
38
+ // Response in standard base64, which a conforming endpoint refuses.
39
+ const tokens = await (0, saml2TokenExchange_1.exchangeSamlAssertion)((0, samlBearerAssertion_1.toBearerAssertion)(samlResponse), tokenUrl, this.config.clientId, this.config.clientSecret, this.logger);
37
40
  return {
38
41
  authorizationToken: tokens.accessToken,
39
42
  refreshToken: tokens.refreshToken,
@@ -0,0 +1,43 @@
1
+ /**
2
+ * UAA / XSUAA one-time passcode provider — the login `cf login --sso` uses.
3
+ *
4
+ * Nothing is opened and nothing listens on this machine: the user fetches a
5
+ * code from `<uaaUrl>/passcode` in any browser, anywhere, logging in however
6
+ * the identity zone asks (SSO through a corporate IdP, MFA), and hands it to
7
+ * the strategy. The provider exchanges it for tokens and refreshes them
8
+ * afterwards, so the user is asked again only when the refresh token is gone.
9
+ */
10
+ import type { IAuthorizationStrategy, ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces-auth';
11
+ import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
12
+ import { BaseTokenProvider } from './BaseTokenProvider';
13
+ export interface UaaPasscodeProviderConfig {
14
+ /** UAA / XSUAA base URL, e.g. `https://<subdomain>.authentication.<region>.hana.ondemand.com`. */
15
+ uaaUrl: string;
16
+ /** A client allowed the `password` grant; add `refresh_token` to keep the session. */
17
+ clientId: string;
18
+ /** Omitted for a public client, which authenticates with an empty secret. */
19
+ clientSecret?: string;
20
+ /**
21
+ * How the user's code reaches the provider. The strategy is handed
22
+ * `<uaaUrl>/passcode` as the URL to send the user to, and returns the code.
23
+ * Defaults to `manualPasscodeStrategy()`: announce the URL, read the code
24
+ * from the terminal.
25
+ */
26
+ authorization?: IAuthorizationStrategy<string>;
27
+ accessToken?: string;
28
+ refreshToken?: string;
29
+ logger?: ILogger;
30
+ }
31
+ export declare class UaaPasscodeProvider extends BaseTokenProvider {
32
+ private readonly config;
33
+ constructor(config: UaaPasscodeProviderConfig);
34
+ protected getAuthType(): OAuth2GrantType;
35
+ private get baseUrl();
36
+ protected performLogin(): Promise<ITokenResult>;
37
+ /**
38
+ * A failure is thrown, not handled: BaseTokenProvider drops the refresh
39
+ * token and asks for a new passcode through performLogin().
40
+ */
41
+ protected performRefresh(): Promise<ITokenResult>;
42
+ }
43
+ //# sourceMappingURL=UaaPasscodeProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UaaPasscodeProvider.d.ts","sourceRoot":"","sources":["../../src/providers/UaaPasscodeProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,sBAAsB,EACtB,YAAY,EACZ,eAAe,EAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAI9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,yBAAyB;IACxC,kGAAkG;IAClG,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,mBAAoB,SAAQ,iBAAiB;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,MAAM,EAAE,yBAAyB;IAa7C,SAAS,CAAC,WAAW,IAAI,eAAe;IAIxC,OAAO,KAAK,OAAO,GAElB;cAEe,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAqCrD;;;OAGG;cACa,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAkBxD"}