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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +773 -77
  3. package/dist/auth/saml2Auth.d.ts +6 -2
  4. package/dist/auth/saml2Auth.d.ts.map +1 -1
  5. package/dist/auth/saml2Auth.js +9 -20
  6. package/dist/auth/samlBearerAssertion.d.ts.map +1 -1
  7. package/dist/auth/samlBearerAssertion.js +6 -2
  8. package/dist/auth/strictXml.d.ts +13 -0
  9. package/dist/auth/strictXml.d.ts.map +1 -0
  10. package/dist/auth/strictXml.js +21 -0
  11. package/dist/errors/AssertionValidationError.d.ts +15 -0
  12. package/dist/errors/AssertionValidationError.d.ts.map +1 -0
  13. package/dist/errors/AssertionValidationError.js +24 -0
  14. package/dist/errors/TokenProviderErrors.d.ts +2 -0
  15. package/dist/errors/TokenProviderErrors.d.ts.map +1 -1
  16. package/dist/errors/TokenProviderErrors.js +3 -1
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +11 -1
  20. package/dist/providers/Saml2BearerProvider.d.ts +1 -0
  21. package/dist/providers/Saml2BearerProvider.d.ts.map +1 -1
  22. package/dist/providers/Saml2BearerProvider.js +17 -2
  23. package/dist/providers/Saml2PureProvider.d.ts +1 -0
  24. package/dist/providers/Saml2PureProvider.d.ts.map +1 -1
  25. package/dist/providers/Saml2PureProvider.js +19 -5
  26. package/dist/providers/saml2Utils.d.ts +50 -2
  27. package/dist/providers/saml2Utils.d.ts.map +1 -1
  28. package/dist/providers/saml2Utils.js +102 -2
  29. package/dist/validation/assertionValidator.d.ts +28 -0
  30. package/dist/validation/assertionValidator.d.ts.map +1 -0
  31. package/dist/validation/assertionValidator.js +519 -0
  32. package/dist/validation/documentIds.d.ts +15 -0
  33. package/dist/validation/documentIds.d.ts.map +1 -0
  34. package/dist/validation/documentIds.js +32 -0
  35. package/dist/validation/inMemoryReplayStore.d.ts +22 -0
  36. package/dist/validation/inMemoryReplayStore.d.ts.map +1 -0
  37. package/dist/validation/inMemoryReplayStore.js +49 -0
  38. package/dist/validation/signedNode.d.ts +54 -0
  39. package/dist/validation/signedNode.d.ts.map +1 -0
  40. package/dist/validation/signedNode.js +182 -0
  41. package/dist/validation/xsdDateTime.d.ts +17 -0
  42. package/dist/validation/xsdDateTime.d.ts.map +1 -0
  43. package/dist/validation/xsdDateTime.js +67 -0
  44. package/package.json +5 -10
  45. package/bin/auth-authorization-code.ts +0 -147
  46. package/bin/auth-client-credentials.ts +0 -109
  47. package/bin/utils/parseConfig.ts +0 -270
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Which element the signature covers.
3
+ *
4
+ * Not "is there a valid signature" — that question has a true answer in a
5
+ * document built for a wrapping attack, where a genuinely signed fragment sits
6
+ * beside a forged one. The caller must read the element this returns and no
7
+ * other.
8
+ */
9
+ import type { Document, Element } from '@xmldom/xmldom';
10
+ /**
11
+ * PEM in, PEM out; bare base64 DER gets its armour — and the result is proved
12
+ * to be a certificate before anything uses it.
13
+ *
14
+ * The spec promises `idpCertificates` accepts either, and a consumer copying
15
+ * `<X509Certificate>` out of identity-provider metadata has bare base64 DER in
16
+ * their hand — the armour is not in the metadata. `xml-crypto` accepts only
17
+ * PEM or a Buffer: measured, a bare base64 certificate makes OpenSSL throw
18
+ * `DECODER routines::unsupported`, while the same bytes re-armoured verify.
19
+ *
20
+ * Left unnormalised that throw would be swallowed by the verification loop and
21
+ * reported as "the signature does not verify against any configured
22
+ * certificate" — blaming the assertion for the consumer's formatting.
23
+ *
24
+ * Base64 syntax alone does not make a string a certificate: `"AAAA"` passes
25
+ * the character class, armours cleanly, and fails only inside OpenSSL, which
26
+ * lands back at the same misleading message. So the armoured result is parsed
27
+ * with `node:crypto`'s `X509Certificate`, which rejects `"AAAA"` with
28
+ * `asn1 encoding routines::wrong tag` — measured, not assumed. Called once per
29
+ * certificate at construction, so the cost never falls on a login.
30
+ */
31
+ export declare function toPem(certificate: string): string;
32
+ /**
33
+ * Quotes a value from the document, or a message quoting one, before
34
+ * interpolating it into a refusal: JSON-quoted, so a newline smuggled in as
35
+ * `&#10;` shows as `\n` rather than forging a line in a log, and cut to 64
36
+ * characters, so an attacker cannot fill a log with it.
37
+ */
38
+ export declare function quoteUntrusted(value: string): string;
39
+ /**
40
+ * Verifies every signature in the document against the certificates and
41
+ * returns the elements they reference. Throws when there is no signature, or
42
+ * when any one of them fails a rule below.
43
+ *
44
+ * `doc` must be the parse of `xml`, and nothing else: signatures are found and
45
+ * their references resolved in `doc`, while `xml-crypto` verifies the digests
46
+ * over `xml`. Handed a `doc` from different bytes, the element returned would
47
+ * not be the one whose bytes were verified.
48
+ *
49
+ * Several signatures are normal — identity providers often sign the Response
50
+ * and the Assertion both. Each is held to every rule on its own; the caller
51
+ * then takes the element it requires from the returned list.
52
+ */
53
+ export declare function resolveSignedElements(xml: string, doc: Document, certificates: readonly string[]): Element[];
54
+ //# sourceMappingURL=signedNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signedNode.d.ts","sourceRoot":"","sources":["../../src/validation/signedNode.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAmB,MAAM,gBAAgB,CAAC;AAKzE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAuBjD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,QAAQ,EACb,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B,OAAO,EAAE,CAeX"}
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Which element the signature covers.
4
+ *
5
+ * Not "is there a valid signature" — that question has a true answer in a
6
+ * document built for a wrapping attack, where a genuinely signed fragment sits
7
+ * beside a forged one. The caller must read the element this returns and no
8
+ * other.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.toPem = toPem;
12
+ exports.quoteUntrusted = quoteUntrusted;
13
+ exports.resolveSignedElements = resolveSignedElements;
14
+ const node_crypto_1 = require("node:crypto");
15
+ const xml_crypto_1 = require("xml-crypto");
16
+ const DSIG_NS = 'http://www.w3.org/2000/09/xmldsig#';
17
+ /**
18
+ * PEM in, PEM out; bare base64 DER gets its armour — and the result is proved
19
+ * to be a certificate before anything uses it.
20
+ *
21
+ * The spec promises `idpCertificates` accepts either, and a consumer copying
22
+ * `<X509Certificate>` out of identity-provider metadata has bare base64 DER in
23
+ * their hand — the armour is not in the metadata. `xml-crypto` accepts only
24
+ * PEM or a Buffer: measured, a bare base64 certificate makes OpenSSL throw
25
+ * `DECODER routines::unsupported`, while the same bytes re-armoured verify.
26
+ *
27
+ * Left unnormalised that throw would be swallowed by the verification loop and
28
+ * reported as "the signature does not verify against any configured
29
+ * certificate" — blaming the assertion for the consumer's formatting.
30
+ *
31
+ * Base64 syntax alone does not make a string a certificate: `"AAAA"` passes
32
+ * the character class, armours cleanly, and fails only inside OpenSSL, which
33
+ * lands back at the same misleading message. So the armoured result is parsed
34
+ * with `node:crypto`'s `X509Certificate`, which rejects `"AAAA"` with
35
+ * `asn1 encoding routines::wrong tag` — measured, not assumed. Called once per
36
+ * certificate at construction, so the cost never falls on a login.
37
+ */
38
+ function toPem(certificate) {
39
+ const trimmed = certificate.trim();
40
+ const pem = trimmed.includes('-----BEGIN')
41
+ ? trimmed
42
+ : (() => {
43
+ const body = trimmed.replace(/\s+/g, '');
44
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(body)) {
45
+ throw new Error('a configured certificate is neither PEM nor base64 DER');
46
+ }
47
+ const wrapped = body.replace(/(.{64})/g, '$1\n').trim();
48
+ return `-----BEGIN CERTIFICATE-----\n${wrapped}\n-----END CERTIFICATE-----\n`;
49
+ })();
50
+ try {
51
+ new node_crypto_1.X509Certificate(pem);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`a configured certificate is not a valid X.509 certificate: ${error.message}`);
55
+ }
56
+ return pem;
57
+ }
58
+ /**
59
+ * Quotes a value from the document, or a message quoting one, before
60
+ * interpolating it into a refusal: JSON-quoted, so a newline smuggled in as
61
+ * `&#10;` shows as `\n` rather than forging a line in a log, and cut to 64
62
+ * characters, so an attacker cannot fill a log with it.
63
+ */
64
+ function quoteUntrusted(value) {
65
+ const limit = 64;
66
+ return JSON.stringify(value.length > limit ? `${value.slice(0, limit)}…` : value);
67
+ }
68
+ /**
69
+ * Verifies every signature in the document against the certificates and
70
+ * returns the elements they reference. Throws when there is no signature, or
71
+ * when any one of them fails a rule below.
72
+ *
73
+ * `doc` must be the parse of `xml`, and nothing else: signatures are found and
74
+ * their references resolved in `doc`, while `xml-crypto` verifies the digests
75
+ * over `xml`. Handed a `doc` from different bytes, the element returned would
76
+ * not be the one whose bytes were verified.
77
+ *
78
+ * Several signatures are normal — identity providers often sign the Response
79
+ * and the Assertion both. Each is held to every rule on its own; the caller
80
+ * then takes the element it requires from the returned list.
81
+ */
82
+ function resolveSignedElements(xml, doc, certificates) {
83
+ const signatures = doc.getElementsByTagNameNS(DSIG_NS, 'Signature');
84
+ if (signatures.length === 0) {
85
+ throw new Error('the document carries no signature');
86
+ }
87
+ // Every signature is held to every rule: an extra signature that fails is
88
+ // something that should not be there, not noise to skip. Two verifying
89
+ // signatures over one element cannot occur — under the enveloped-signature
90
+ // transform the later one changes the bytes the earlier one's digest
91
+ // covered — so no separate rule guards against it.
92
+ const covered = [];
93
+ for (let i = 0; i < signatures.length; i++) {
94
+ covered.push(resolveOne(xml, doc, signatures[i], certificates));
95
+ }
96
+ return covered;
97
+ }
98
+ /** One signature: verified, exactly one reference, enveloped by its target. */
99
+ function resolveOne(xml, doc, signatureNode, certificates) {
100
+ let verified = false;
101
+ for (const certificate of certificates) {
102
+ // Already normalised and proved at construction, so a throw here really
103
+ // is a bad signature rather than a formatting mistake.
104
+ // getCertFromKeyInfo is pinned to "none": a certificate the document
105
+ // carries in its own KeyInfo is the sender's claim about who signed it,
106
+ // and xml-crypto prefers it to publicCert when the hook returns one. It
107
+ // defaults to a no-op in 6.x; stating it keeps a future default from
108
+ // quietly letting an attacker's embedded certificate verify their own
109
+ // signature.
110
+ const verifier = new xml_crypto_1.SignedXml({
111
+ publicCert: certificate,
112
+ getCertFromKeyInfo: () => null,
113
+ });
114
+ // loadSignature throws for a malformed Signature, and xml-crypto's
115
+ // message embeds the offending element: document text, so quoted.
116
+ try {
117
+ verifier.loadSignature(signatureNode);
118
+ }
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ throw new Error(`the signature element is malformed: ${quoteUntrusted(message)}`);
122
+ }
123
+ try {
124
+ // Returns false for a digest mismatch and throws when the signature
125
+ // value itself fails. Both mean "not this certificate".
126
+ if (verifier.checkSignature(xml)) {
127
+ verified = true;
128
+ break;
129
+ }
130
+ }
131
+ catch {
132
+ // Try the next certificate in the rotation list.
133
+ }
134
+ }
135
+ if (!verified) {
136
+ throw new Error('the signature does not verify against any configured certificate');
137
+ }
138
+ // The signature is valid; now find what it actually covers. Exactly one
139
+ // reference: two would be two candidate answers to "what is signed", the
140
+ // ambiguity this module exists to remove.
141
+ const references = signatureNode.getElementsByTagNameNS(DSIG_NS, 'Reference');
142
+ if (references.length === 0) {
143
+ throw new Error('the signature carries no ds:Reference');
144
+ }
145
+ if (references.length > 1) {
146
+ throw new Error(`the signature carries ${references.length} ds:Reference; exactly one is allowed`);
147
+ }
148
+ const uri = references[0].getAttribute('URI') ?? '';
149
+ let referenced = null;
150
+ if (uri === '') {
151
+ // An empty URI signs the whole document. It must still satisfy the
152
+ // enveloping rule below — returning early here is exactly how a detached
153
+ // signature with an empty reference walks straight past that rule.
154
+ referenced = doc.documentElement;
155
+ }
156
+ else if (!uri.startsWith('#')) {
157
+ throw new Error(`the signature reference is not a same-document URI: ${quoteUntrusted(uri)}`);
158
+ }
159
+ else {
160
+ const id = uri.slice(1);
161
+ const elements = doc.getElementsByTagName('*');
162
+ for (let i = 0; i < elements.length; i++) {
163
+ if (elements[i].getAttribute('ID') === id) {
164
+ referenced = elements[i];
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ if (!referenced) {
170
+ throw new Error(`the signature references ${quoteUntrusted(uri)}, which is not in the document`);
171
+ }
172
+ // The enveloped signature must sit inside the element it references. A
173
+ // signature moved elsewhere stays cryptographically valid over the bytes it
174
+ // covers, so the maths alone will not catch it — this comparison is what
175
+ // does. It is the check @node-saml/node-saml makes, and the one whose
176
+ // absence made every response @mcp-abap-adt/auth-mocks produced
177
+ // unacceptable to a real library until it was fixed there.
178
+ if (signatureNode.parentNode !== referenced) {
179
+ throw new Error('the signature is not inside the element it references, so it does not envelope it');
180
+ }
181
+ return referenced;
182
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Parsing an `xsd:dateTime` strictly enough to trust the result.
3
+ *
4
+ * `Date.parse` is not this. It normalises `2026-02-30` into 2 March instead of
5
+ * rejecting it, and it will read a timezone offset no calendar has. Both traps
6
+ * were found in `@mcp-abap-adt/auth-mocks` and fixed there the same way: match
7
+ * the lexical shape, then require every component to survive a round trip. The
8
+ * instant is computed from the components rather than handed back to `Date`,
9
+ * whose handling of invalid strings is implementation-defined.
10
+ *
11
+ * Deliberately not implemented: negative (BCE) years, the `24:00:00`
12
+ * end-of-day form, and leap seconds. No identity provider in this family emits
13
+ * them, and pretending to cover them would be worse than saying so.
14
+ */
15
+ /** Returns the instant, or null when the value is not a valid xsd:dateTime. */
16
+ export declare function parseXsdDateTime(value: string | null | undefined): Date | null;
17
+ //# sourceMappingURL=xsdDateTime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xsdDateTime.d.ts","sourceRoot":"","sources":["../../src/validation/xsdDateTime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,IAAI,GAAG,IAAI,CAkDb"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * Parsing an `xsd:dateTime` strictly enough to trust the result.
4
+ *
5
+ * `Date.parse` is not this. It normalises `2026-02-30` into 2 March instead of
6
+ * rejecting it, and it will read a timezone offset no calendar has. Both traps
7
+ * were found in `@mcp-abap-adt/auth-mocks` and fixed there the same way: match
8
+ * the lexical shape, then require every component to survive a round trip. The
9
+ * instant is computed from the components rather than handed back to `Date`,
10
+ * whose handling of invalid strings is implementation-defined.
11
+ *
12
+ * Deliberately not implemented: negative (BCE) years, the `24:00:00`
13
+ * end-of-day form, and leap seconds. No identity provider in this family emits
14
+ * them, and pretending to cover them would be worse than saying so.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.parseXsdDateTime = parseXsdDateTime;
18
+ const SHAPE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
19
+ /** Returns the instant, or null when the value is not a valid xsd:dateTime. */
20
+ function parseXsdDateTime(value) {
21
+ if (!value)
22
+ return null;
23
+ const m = SHAPE.exec(value);
24
+ if (!m)
25
+ return null;
26
+ const [, y, mo, d, h, mi, s, fraction, zone] = m;
27
+ const year = Number(y);
28
+ const month = Number(mo);
29
+ const day = Number(d);
30
+ const hour = Number(h);
31
+ const minute = Number(mi);
32
+ const second = Number(s);
33
+ if (month < 1 || month > 12)
34
+ return null;
35
+ if (hour > 23 || minute > 59 || second > 59)
36
+ return null;
37
+ // The calendar round trip. Date.UTC rolls 2026-02-30 into 2026-03-02, so a
38
+ // component that comes back changed means the date does not exist.
39
+ const utc = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
40
+ if (utc.getUTCFullYear() !== year ||
41
+ utc.getUTCMonth() !== month - 1 ||
42
+ utc.getUTCDate() !== day) {
43
+ return null;
44
+ }
45
+ // Milliseconds from the fraction: pad to 3 digits and truncate extras.
46
+ // Avoid floating-point multiplication: 0.57 * 1000 = 569.99999….
47
+ const ms = fraction
48
+ ? Number(fraction.slice(1).padEnd(3, '0').slice(0, 3))
49
+ : 0;
50
+ // Offset in milliseconds. For zone Z it is 0. Otherwise apply the sign.
51
+ let offsetMs = 0;
52
+ if (zone !== 'Z') {
53
+ const offsetHour = Number(zone.slice(1, 3));
54
+ const offsetMinute = Number(zone.slice(4, 6));
55
+ // xsd:dateTime bounds the offset at ±14:00, and exactly 14 allows no
56
+ // minutes. Both halves matter: +15:00 fails the first, +14:01 the second.
57
+ if (offsetHour > 14 || offsetMinute > 59)
58
+ return null;
59
+ if (offsetHour === 14 && offsetMinute !== 0)
60
+ return null;
61
+ const sign = zone[0] === '-' ? -1 : 1;
62
+ offsetMs = sign * (offsetHour * 60 + offsetMinute) * 60_000;
63
+ }
64
+ // The instant is UTC time + milliseconds - offset, because local time = UTC + offset.
65
+ const instant = utc.getTime() + ms - offsetMs;
66
+ return new Date(instant);
67
+ }
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-providers",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Token providers for MCP ABAP ADT auth-broker",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist",
9
- "bin",
10
9
  "README.md",
11
10
  "CHANGELOG.md",
12
11
  "LICENSE",
@@ -38,10 +37,6 @@
38
37
  "publishConfig": {
39
38
  "access": "public"
40
39
  },
41
- "bin": {
42
- "auth-authorization-code": "./bin/auth-authorization-code.ts",
43
- "auth-client-credentials": "./bin/auth-client-credentials.ts"
44
- },
45
40
  "scripts": {
46
41
  "chrono": "./tools/version-stats.sh",
47
42
  "clean": "rm -rf dist tsconfig.tsbuildinfo",
@@ -62,13 +57,14 @@
62
57
  "node": "^22 || ^24"
63
58
  },
64
59
  "dependencies": {
65
- "@mcp-abap-adt/interfaces-auth": "^1.2.0",
66
- "@mcp-abap-adt/interfaces-auth-sap": "^1.0.0",
60
+ "@mcp-abap-adt/interfaces-auth": "^2.0.1",
61
+ "@mcp-abap-adt/interfaces-auth-sap": "^1.0.1",
67
62
  "@mcp-abap-adt/interfaces-utils": "^1.1.0",
68
63
  "@xmldom/xmldom": "^0.9.12",
69
64
  "axios": "^1.13.5",
70
65
  "express": "^5.1.0",
71
- "open": "^11.0.0"
66
+ "open": "^11.0.0",
67
+ "xml-crypto": "^6.1.2"
72
68
  },
73
69
  "devDependencies": {
74
70
  "@biomejs/biome": "^2.3.14",
@@ -86,7 +82,6 @@
86
82
  "pino": "^10.3.1",
87
83
  "pino-pretty": "^13.1.3",
88
84
  "ts-jest": "^29.2.5",
89
- "tsx": "^4.19.2",
90
85
  "typescript": "^5.9.2"
91
86
  }
92
87
  }
@@ -1,147 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Authorization Code Provider Test Command
4
- *
5
- * Gets tokens using authorization_code grant type
6
- */
7
-
8
- import {
9
- ABAP_AUTHORIZATION_VARS,
10
- ABAP_CONNECTION_VARS,
11
- } from '@mcp-abap-adt/auth-stores';
12
- import { AuthorizationCodeProvider } from '../src/providers/AuthorizationCodeProvider';
13
- import { browserCallbackStrategy } from '../src/strategies';
14
- import {
15
- getUaaCredentials,
16
- parseEnvFile,
17
- parseServiceKey,
18
- writeEnvFile,
19
- } from './utils/parseConfig';
20
-
21
- async function main() {
22
- const args = process.argv.slice(2);
23
- let serviceKeyPath: string | undefined;
24
- let inputEnvPath: string | undefined;
25
- let outputEnvPath: string | undefined;
26
- let browser: string = 'none';
27
- let port: number = 3001;
28
- let authorizationUrl: string | undefined;
29
-
30
- // Parse arguments
31
- for (let i = 0; i < args.length; i++) {
32
- const arg = args[i];
33
- if (arg === '--service-key' && args[i + 1]) {
34
- serviceKeyPath = args[++i];
35
- } else if (arg === '--input-env' && args[i + 1]) {
36
- inputEnvPath = args[++i];
37
- } else if (arg === '--output-env' && args[i + 1]) {
38
- outputEnvPath = args[++i];
39
- } else if (arg === '--browser' && args[i + 1]) {
40
- browser = args[++i];
41
- } else if (arg === '--port' && args[i + 1]) {
42
- port = parseInt(args[++i], 10);
43
- } else if (arg === '--auth-url' && args[i + 1]) {
44
- authorizationUrl = args[++i];
45
- } else if (arg === '--help' || arg === '-h') {
46
- console.log(`
47
- Usage: auth-authorization-code [options]
48
-
49
- Options:
50
- --service-key <path> Path to service key JSON file
51
- --input-env <path> Path to .env file for reading existing tokens (for refresh)
52
- --output-env <path> Path to .env file for saving tokens (required)
53
- --browser <type> Browser type (auto, system, chrome, none) [default: none]
54
- --port <number> Callback server port [default: 3001]
55
- --auth-url <url> Pre-built authorization URL
56
- --help, -h Show this help message
57
-
58
- Example:
59
- auth-authorization-code --service-key ./service-key.json --output-env ./tokens.env --browser auto
60
- auth-authorization-code --input-env ./tokens.env --output-env ./tokens.env --browser none
61
- `);
62
- process.exit(0);
63
- }
64
- }
65
-
66
- if (!serviceKeyPath && !inputEnvPath) {
67
- console.error('❌ Error: Either --service-key or --input-env must be provided');
68
- process.exit(1);
69
- }
70
-
71
- if (!outputEnvPath) {
72
- console.error('❌ Error: --output-env is required to save tokens');
73
- process.exit(1);
74
- }
75
-
76
- try {
77
- // Parse config
78
- const serviceKey = serviceKeyPath ? parseServiceKey(serviceKeyPath) : undefined;
79
- const inputEnv = inputEnvPath ? parseEnvFile(inputEnvPath) : undefined;
80
- const { uaaUrl, clientId, clientSecret } = getUaaCredentials(serviceKey, inputEnv);
81
-
82
- // Read existing tokens from input env if provided (use constants, fallback to legacy)
83
- const existingRefreshToken =
84
- inputEnv?.[ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN] ||
85
- inputEnv?.REFRESH_TOKEN;
86
-
87
- // Build authorization URL if not provided
88
- if (!authorizationUrl) {
89
- const redirectUri = `http://localhost:${port}/callback`;
90
- authorizationUrl = `${uaaUrl}/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`;
91
- }
92
-
93
- // Create provider. `--port` now reaches the strategy that binds the socket;
94
- // it must stay the port baked into the authorization URL above, or the
95
- // provider rejects the mismatch before opening anything.
96
- const provider = new AuthorizationCodeProvider({
97
- authorizationUrl,
98
- uaaUrl,
99
- clientId,
100
- clientSecret,
101
- authorization: browserCallbackStrategy({ browser, port }),
102
- refreshToken: existingRefreshToken,
103
- });
104
-
105
- console.log('🔐 Getting tokens using Authorization Code flow...');
106
- if (serviceKeyPath) {
107
- console.log(`📁 Service Key: ${serviceKeyPath}`);
108
- }
109
- if (inputEnvPath) {
110
- console.log(`📁 Input Env: ${inputEnvPath}`);
111
- }
112
- console.log(`💾 Output Env: ${outputEnvPath}`);
113
- console.log(`🌐 Browser: ${browser}`);
114
- console.log(`🔗 Authorization URL: ${authorizationUrl}\n`);
115
-
116
- // Get tokens
117
- const result = await provider.getTokens();
118
-
119
- // Save tokens to output env file
120
- writeEnvFile(outputEnvPath, {
121
- authorizationToken: result.authorizationToken,
122
- refreshToken: result.refreshToken,
123
- uaaUrl,
124
- clientId,
125
- clientSecret,
126
- });
127
-
128
- console.log('✅ Tokens obtained and saved successfully!');
129
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
130
- console.log(`🔑 Authorization Token: ${result.authorizationToken.substring(0, 50)}...`);
131
- if (result.refreshToken) {
132
- console.log(`🔄 Refresh Token: ${result.refreshToken.substring(0, 50)}...`);
133
- }
134
- console.log(`📋 Auth Type: ${result.authType}`);
135
- if (result.expiresIn) {
136
- console.log(`⏰ Expires In: ${result.expiresIn} seconds`);
137
- }
138
- console.log(`💾 Saved to: ${outputEnvPath}`);
139
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
140
- } catch (error) {
141
- console.error('❌ Error:', error instanceof Error ? error.message : String(error));
142
- process.exit(1);
143
- }
144
- }
145
-
146
- main();
147
-
@@ -1,109 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Client Credentials Provider Test Command
4
- *
5
- * Gets tokens using client_credentials grant type
6
- */
7
-
8
- import { ClientCredentialsProvider } from '../src/providers/ClientCredentialsProvider';
9
- import {
10
- getUaaCredentials,
11
- parseEnvFile,
12
- parseServiceKey,
13
- writeEnvFile,
14
- } from './utils/parseConfig';
15
-
16
- async function main() {
17
- const args = process.argv.slice(2);
18
- let serviceKeyPath: string | undefined;
19
- let inputEnvPath: string | undefined;
20
- let outputEnvPath: string | undefined;
21
-
22
- // Parse arguments
23
- for (let i = 0; i < args.length; i++) {
24
- const arg = args[i];
25
- if (arg === '--service-key' && args[i + 1]) {
26
- serviceKeyPath = args[++i];
27
- } else if (arg === '--input-env' && args[i + 1]) {
28
- inputEnvPath = args[++i];
29
- } else if (arg === '--output-env' && args[i + 1]) {
30
- outputEnvPath = args[++i];
31
- } else if (arg === '--help' || arg === '-h') {
32
- console.log(`
33
- Usage: auth-client-credentials [options]
34
-
35
- Options:
36
- --service-key <path> Path to service key JSON file
37
- --input-env <path> Path to .env file for reading credentials
38
- --output-env <path> Path to .env file for saving tokens (required)
39
- --help, -h Show this help message
40
-
41
- Example:
42
- auth-client-credentials --service-key ./service-key.json --output-env ./tokens.env
43
- auth-client-credentials --input-env ./tokens.env --output-env ./tokens.env
44
- `);
45
- process.exit(0);
46
- }
47
- }
48
-
49
- if (!serviceKeyPath && !inputEnvPath) {
50
- console.error('❌ Error: Either --service-key or --input-env must be provided');
51
- process.exit(1);
52
- }
53
-
54
- if (!outputEnvPath) {
55
- console.error('❌ Error: --output-env is required to save tokens');
56
- process.exit(1);
57
- }
58
-
59
- try {
60
- // Parse config
61
- const serviceKey = serviceKeyPath ? parseServiceKey(serviceKeyPath) : undefined;
62
- const inputEnv = inputEnvPath ? parseEnvFile(inputEnvPath) : undefined;
63
- const { uaaUrl, clientId, clientSecret } = getUaaCredentials(serviceKey, inputEnv);
64
-
65
- // Create provider
66
- const provider = new ClientCredentialsProvider({
67
- uaaUrl,
68
- clientId,
69
- clientSecret,
70
- });
71
-
72
- console.log('🔐 Getting tokens using Client Credentials flow...');
73
- if (serviceKeyPath) {
74
- console.log(`📁 Service Key: ${serviceKeyPath}`);
75
- }
76
- if (inputEnvPath) {
77
- console.log(`📁 Input Env: ${inputEnvPath}`);
78
- }
79
- console.log(`💾 Output Env: ${outputEnvPath}\n`);
80
-
81
- // Get tokens
82
- const result = await provider.getTokens();
83
-
84
- // Save tokens to output env file
85
- writeEnvFile(outputEnvPath, {
86
- authorizationToken: result.authorizationToken,
87
- refreshToken: result.refreshToken,
88
- uaaUrl,
89
- clientId,
90
- clientSecret,
91
- });
92
-
93
- console.log('✅ Tokens obtained and saved successfully!');
94
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
95
- console.log(`🔑 Authorization Token: ${result.authorizationToken.substring(0, 50)}...`);
96
- console.log(`📋 Auth Type: ${result.authType}`);
97
- if (result.expiresIn) {
98
- console.log(`⏰ Expires In: ${result.expiresIn} seconds`);
99
- }
100
- console.log(`💾 Saved to: ${outputEnvPath}`);
101
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
102
- } catch (error) {
103
- console.error('❌ Error:', error instanceof Error ? error.message : String(error));
104
- process.exit(1);
105
- }
106
- }
107
-
108
- main();
109
-