@mcp-abap-adt/auth-providers 0.2.10 → 1.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 (63) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +89 -0
  3. package/dist/__tests__/helpers/netHelpers.d.ts +3 -0
  4. package/dist/__tests__/helpers/netHelpers.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/netHelpers.js +63 -0
  6. package/dist/auth/manualInput.d.ts +5 -0
  7. package/dist/auth/manualInput.d.ts.map +1 -0
  8. package/dist/auth/manualInput.js +19 -0
  9. package/dist/auth/oidcBrowserAuth.d.ts +9 -0
  10. package/dist/auth/oidcBrowserAuth.d.ts.map +1 -0
  11. package/dist/auth/oidcBrowserAuth.js +144 -0
  12. package/dist/auth/oidcDiscovery.d.ts +14 -0
  13. package/dist/auth/oidcDiscovery.d.ts.map +1 -0
  14. package/dist/auth/oidcDiscovery.js +33 -0
  15. package/dist/auth/oidcPkce.d.ts +6 -0
  16. package/dist/auth/oidcPkce.d.ts.map +1 -0
  17. package/dist/auth/oidcPkce.js +22 -0
  18. package/dist/auth/oidcToken.d.ts +26 -0
  19. package/dist/auth/oidcToken.d.ts.map +1 -0
  20. package/dist/auth/oidcToken.js +165 -0
  21. package/dist/auth/saml2Auth.d.ts +15 -0
  22. package/dist/auth/saml2Auth.d.ts.map +1 -0
  23. package/dist/auth/saml2Auth.js +205 -0
  24. package/dist/auth/saml2TokenExchange.d.ts +12 -0
  25. package/dist/auth/saml2TokenExchange.d.ts.map +1 -0
  26. package/dist/auth/saml2TokenExchange.js +39 -0
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +10 -1
  30. package/dist/providers/BaseTokenProvider.d.ts +1 -0
  31. package/dist/providers/BaseTokenProvider.d.ts.map +1 -1
  32. package/dist/providers/BaseTokenProvider.js +28 -2
  33. package/dist/providers/OidcBrowserProvider.d.ts +24 -0
  34. package/dist/providers/OidcBrowserProvider.d.ts.map +1 -0
  35. package/dist/providers/OidcBrowserProvider.js +76 -0
  36. package/dist/providers/OidcDeviceFlowProvider.d.ts +22 -0
  37. package/dist/providers/OidcDeviceFlowProvider.d.ts.map +1 -0
  38. package/dist/providers/OidcDeviceFlowProvider.js +68 -0
  39. package/dist/providers/OidcPasswordProvider.d.ts +24 -0
  40. package/dist/providers/OidcPasswordProvider.d.ts.map +1 -0
  41. package/dist/providers/OidcPasswordProvider.js +55 -0
  42. package/dist/providers/OidcTokenExchangeProvider.d.ts +27 -0
  43. package/dist/providers/OidcTokenExchangeProvider.d.ts.map +1 -0
  44. package/dist/providers/OidcTokenExchangeProvider.js +43 -0
  45. package/dist/providers/Saml2BearerProvider.d.ts +21 -0
  46. package/dist/providers/Saml2BearerProvider.d.ts.map +1 -0
  47. package/dist/providers/Saml2BearerProvider.js +49 -0
  48. package/dist/providers/Saml2PureProvider.d.ts +20 -0
  49. package/dist/providers/Saml2PureProvider.d.ts.map +1 -0
  50. package/dist/providers/Saml2PureProvider.js +39 -0
  51. package/dist/providers/index.d.ts +12 -0
  52. package/dist/providers/index.d.ts.map +1 -1
  53. package/dist/providers/index.js +13 -1
  54. package/dist/providers/saml2Utils.d.ts +30 -0
  55. package/dist/providers/saml2Utils.d.ts.map +1 -0
  56. package/dist/providers/saml2Utils.js +49 -0
  57. package/dist/sso/SsoProviderFactory.d.ts +6 -0
  58. package/dist/sso/SsoProviderFactory.d.ts.map +1 -0
  59. package/dist/sso/SsoProviderFactory.js +37 -0
  60. package/dist/sso/types.d.ts +34 -0
  61. package/dist/sso/types.d.ts.map +1 -0
  62. package/dist/sso/types.js +2 -0
  63. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.0] - 2026-02-10
11
+
12
+ ### Added
13
+ - SSO providers for OIDC and SAML2, plus `SsoProviderFactory` for DI-friendly creation.
14
+ - OIDC flows: browser (PKCE), device flow, password grant, token exchange.
15
+ - SAML2 flows: browser/manual/assertion input, bearer exchange, and pure SAML (cookie-based) output.
16
+ - `BaseTokenProvider` now supports `tokenType` and `expiresAt` from `ITokenResult`.
17
+
10
18
  ## [0.2.10] - 2025-12-25
11
19
 
12
20
  ### Changed
package/README.md CHANGED
@@ -99,6 +99,95 @@ const clientCredsBroker = new AuthBroker({
99
99
  }, 'none');
100
100
  ```
101
101
 
102
+ ### SSO Providers
103
+
104
+ This package also includes SSO providers for OIDC and SAML2, plus a small factory for DI-friendly creation.
105
+
106
+ Available providers:
107
+ - `OidcBrowserProvider` (authorization code + PKCE)
108
+ - `OidcDeviceFlowProvider`
109
+ - `OidcPasswordProvider`
110
+ - `OidcTokenExchangeProvider`
111
+ - `Saml2BearerProvider` (SAML assertion exchange)
112
+ - `Saml2PureProvider` (returns SAMLResponse as token)
113
+
114
+ Factory example:
115
+
116
+ ```typescript
117
+ import { AuthBroker } from '@mcp-abap-adt/auth-broker';
118
+ import { SsoProviderFactory } from '@mcp-abap-adt/auth-providers';
119
+
120
+ const tokenProvider = SsoProviderFactory.create({
121
+ protocol: 'oidc',
122
+ flow: 'browser',
123
+ config: {
124
+ issuerUrl: 'https://example-idp/.well-known/openid-configuration',
125
+ clientId: '...',
126
+ clientSecret: '...',
127
+ scopes: ['openid', 'profile', 'email'],
128
+ browser: 'system',
129
+ },
130
+ });
131
+
132
+ const broker = new AuthBroker({ tokenProvider }, 'none');
133
+ ```
134
+
135
+ SAML bearer example (manual flow):
136
+
137
+ ```typescript
138
+ import { AuthBroker } from '@mcp-abap-adt/auth-broker';
139
+ import { Saml2BearerProvider } from '@mcp-abap-adt/auth-providers';
140
+
141
+ const provider = new Saml2BearerProvider({
142
+ assertionFlow: 'manual',
143
+ idpSsoUrl: 'https://idp.example.com/sso',
144
+ spEntityId: 'my-sp-entity',
145
+ uaaUrl: 'https://uaa.example.com',
146
+ clientId: '...',
147
+ clientSecret: '...',
148
+ });
149
+
150
+ const broker = new AuthBroker({ tokenProvider: provider }, 'none');
151
+ ```
152
+
153
+ SAML bearer example (headless, assertion provider):
154
+
155
+ ```typescript
156
+ import { AuthBroker } from '@mcp-abap-adt/auth-broker';
157
+ import { Saml2BearerProvider } from '@mcp-abap-adt/auth-providers';
158
+
159
+ const provider = new Saml2BearerProvider({
160
+ assertionFlow: 'assertion',
161
+ assertionProvider: async () => {
162
+ return getSamlResponseFromSsoProxy();
163
+ },
164
+ uaaUrl: 'https://uaa.example.com',
165
+ clientId: '...',
166
+ clientSecret: '...',
167
+ });
168
+
169
+ const broker = new AuthBroker({ tokenProvider: provider }, 'none');
170
+ ```
171
+
172
+ Pure SAML example (cookie-based):
173
+
174
+ ```typescript
175
+ import { AuthBroker } from '@mcp-abap-adt/auth-broker';
176
+ import { Saml2PureProvider } from '@mcp-abap-adt/auth-providers';
177
+
178
+ const provider = new Saml2PureProvider({
179
+ assertionFlow: 'manual',
180
+ idpSsoUrl: 'https://idp.example.com/sso',
181
+ spEntityId: 'my-sp-entity',
182
+ // Convert SAMLResponse to session cookies for SAP (implementation-specific)
183
+ cookieProvider: async (samlResponse) => {
184
+ return exchangeSamlForCookies(samlResponse);
185
+ },
186
+ });
187
+
188
+ const broker = new AuthBroker({ tokenProvider: provider }, 'none');
189
+ ```
190
+
102
191
  ### With Stores
103
192
 
104
193
  **Important**: BTP and ABAP are different entities:
@@ -0,0 +1,3 @@
1
+ export declare function getAvailablePort(): Promise<number>;
2
+ export declare function canListenOnLocalhost(): Promise<boolean>;
3
+ //# sourceMappingURL=netHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"netHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/netHelpers.ts"],"names":[],"mappings":"AAEA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAcxD;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAQ7D"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getAvailablePort = getAvailablePort;
37
+ exports.canListenOnLocalhost = canListenOnLocalhost;
38
+ const net = __importStar(require("node:net"));
39
+ async function getAvailablePort() {
40
+ return new Promise((resolve, reject) => {
41
+ const server = net.createServer();
42
+ server.once('error', reject);
43
+ server.listen(0, '127.0.0.1', () => {
44
+ const address = server.address();
45
+ if (typeof address === 'object' && address?.port) {
46
+ const port = address.port;
47
+ server.close(() => resolve(port));
48
+ }
49
+ else {
50
+ server.close(() => reject(new Error('Failed to acquire a port')));
51
+ }
52
+ });
53
+ });
54
+ }
55
+ async function canListenOnLocalhost() {
56
+ return new Promise((resolve) => {
57
+ const server = net.createServer();
58
+ server.once('error', () => resolve(false));
59
+ server.listen(0, '127.0.0.1', () => {
60
+ server.close(() => resolve(true));
61
+ });
62
+ });
63
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Manual input helper (stdin)
3
+ */
4
+ export declare function readManualInput(prompt: string): Promise<string>;
5
+ //# sourceMappingURL=manualInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manualInput.d.ts","sourceRoot":"","sources":["../../src/auth/manualInput.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYrE"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /**
3
+ * Manual input helper (stdin)
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readManualInput = readManualInput;
7
+ const node_readline_1 = require("node:readline");
8
+ async function readManualInput(prompt) {
9
+ const rl = (0, node_readline_1.createInterface)({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ });
13
+ return new Promise((resolve) => {
14
+ rl.question(prompt, (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * OIDC browser authorization code flow (capture code)
3
+ */
4
+ import type { ILogger } from '@mcp-abap-adt/interfaces';
5
+ export declare function startOidcBrowserAuth(authorizationUrl: string, browser: string, logger?: ILogger, port?: number): Promise<{
6
+ code: string;
7
+ state?: string;
8
+ }>;
9
+ //# sourceMappingURL=oidcBrowserAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidcBrowserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/oidcBrowserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAqExD,wBAAsB,oBAAoB,CACxC,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2C3C"}
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ /**
3
+ * OIDC browser authorization code flow (capture code)
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.startOidcBrowserAuth = startOidcBrowserAuth;
43
+ const http = __importStar(require("node:http"));
44
+ const net = __importStar(require("node:net"));
45
+ const express_1 = __importDefault(require("express"));
46
+ const BROWSER_MAP = {
47
+ chrome: 'chrome',
48
+ edge: 'msedge',
49
+ firefox: 'firefox',
50
+ system: undefined,
51
+ auto: undefined,
52
+ headless: null,
53
+ none: null,
54
+ };
55
+ function isPortAvailable(port) {
56
+ return new Promise((resolve) => {
57
+ const server = net.createServer();
58
+ server.listen(port, () => {
59
+ server.once('close', () => resolve(true));
60
+ server.close();
61
+ });
62
+ server.on('error', () => resolve(false));
63
+ });
64
+ }
65
+ async function openBrowserUrl(authorizationUrl, browser, logger) {
66
+ const browserApp = BROWSER_MAP[browser];
67
+ if (browserApp === null) {
68
+ logger?.info('[OIDC] Browser suppressed, open URL manually', {
69
+ authorizationUrl,
70
+ });
71
+ return;
72
+ }
73
+ if (browser === 'auto') {
74
+ try {
75
+ const openModule = await Promise.resolve().then(() => __importStar(require('open')));
76
+ const open = openModule.default;
77
+ await open(authorizationUrl);
78
+ logger?.info('[OIDC] Browser opened');
79
+ return;
80
+ }
81
+ catch (error) {
82
+ logger?.warn('[OIDC] Failed to open browser automatically', {
83
+ error: error instanceof Error ? error.message : String(error),
84
+ });
85
+ logger?.info('[OIDC] Open URL manually', { authorizationUrl });
86
+ return;
87
+ }
88
+ }
89
+ try {
90
+ const openModule = await Promise.resolve().then(() => __importStar(require('open')));
91
+ const open = openModule.default;
92
+ if (browserApp) {
93
+ await open(authorizationUrl, { app: { name: browserApp } });
94
+ }
95
+ else {
96
+ await open(authorizationUrl);
97
+ }
98
+ }
99
+ catch (error) {
100
+ logger?.warn('[OIDC] Failed to open browser', {
101
+ error: error instanceof Error ? error.message : String(error),
102
+ });
103
+ logger?.info('[OIDC] Open URL manually', { authorizationUrl });
104
+ }
105
+ }
106
+ async function startOidcBrowserAuth(authorizationUrl, browser, logger, port = 3001) {
107
+ const portAvailable = await isPortAvailable(port);
108
+ if (!portAvailable) {
109
+ throw new Error(`Port ${port} is already in use. Please specify a different port or free the port.`);
110
+ }
111
+ return new Promise((resolve, reject) => {
112
+ const app = (0, express_1.default)();
113
+ const server = http.createServer(app);
114
+ server.keepAliveTimeout = 0;
115
+ server.headersTimeout = 0;
116
+ const PORT = port;
117
+ let resolved = false;
118
+ const cleanup = () => {
119
+ if (resolved)
120
+ return;
121
+ resolved = true;
122
+ server.close();
123
+ };
124
+ app.get('/callback', (req, res) => {
125
+ const code = req.query.code;
126
+ const state = req.query.state;
127
+ if (!code || typeof code !== 'string') {
128
+ res.status(400).send('Missing authorization code');
129
+ cleanup();
130
+ reject(new Error('Missing authorization code'));
131
+ return;
132
+ }
133
+ res
134
+ .status(200)
135
+ .send('Authentication complete. You can close this window.');
136
+ cleanup();
137
+ resolve({ code, state: typeof state === 'string' ? state : undefined });
138
+ });
139
+ server.listen(PORT, async () => {
140
+ logger?.info('[OIDC] Callback server listening', { port: PORT });
141
+ await openBrowserUrl(authorizationUrl, browser, logger);
142
+ });
143
+ });
144
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * OIDC discovery helper
3
+ */
4
+ import type { ILogger } from '@mcp-abap-adt/interfaces';
5
+ export interface OidcDiscoveryDocument {
6
+ issuer: string;
7
+ authorization_endpoint?: string;
8
+ token_endpoint: string;
9
+ device_authorization_endpoint?: string;
10
+ jwks_uri?: string;
11
+ end_session_endpoint?: string;
12
+ }
13
+ export declare function discoverOidc(issuerOrDiscoveryUrl: string, logger?: ILogger): Promise<OidcDiscoveryDocument>;
14
+ //# sourceMappingURL=oidcDiscovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidcDiscovery.d.ts","sourceRoot":"","sources":["../../src/auth/oidcDiscovery.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAGxD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAWD,wBAAsB,YAAY,CAChC,oBAAoB,EAAE,MAAM,EAC5B,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,qBAAqB,CAAC,CAkBhC"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * OIDC discovery helper
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.discoverOidc = discoverOidc;
10
+ const axios_1 = __importDefault(require("axios"));
11
+ const discoveryCache = new Map();
12
+ function normalizeDiscoveryUrl(issuerOrDiscoveryUrl) {
13
+ if (issuerOrDiscoveryUrl.endsWith('/.well-known/openid-configuration')) {
14
+ return issuerOrDiscoveryUrl;
15
+ }
16
+ return `${issuerOrDiscoveryUrl.replace(/\/+$/, '')}/.well-known/openid-configuration`;
17
+ }
18
+ async function discoverOidc(issuerOrDiscoveryUrl, logger) {
19
+ const discoveryUrl = normalizeDiscoveryUrl(issuerOrDiscoveryUrl);
20
+ const cached = discoveryCache.get(discoveryUrl);
21
+ if (cached) {
22
+ return cached;
23
+ }
24
+ logger?.info('[OIDC] Fetching discovery document', { discoveryUrl });
25
+ const response = await axios_1.default.get(discoveryUrl, {
26
+ headers: { Accept: 'application/json' },
27
+ });
28
+ if (!response.data?.token_endpoint) {
29
+ throw new Error('OIDC discovery document missing token_endpoint');
30
+ }
31
+ discoveryCache.set(discoveryUrl, response.data);
32
+ return response.data;
33
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * OIDC PKCE helpers
3
+ */
4
+ export declare function generatePkceVerifier(length?: number): string;
5
+ export declare function generatePkceChallenge(verifier: string): string;
6
+ //# sourceMappingURL=oidcPkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidcPkce.d.ts","sourceRoot":"","sources":["../../src/auth/oidcPkce.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAEhE;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG9D"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * OIDC PKCE helpers
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generatePkceVerifier = generatePkceVerifier;
7
+ exports.generatePkceChallenge = generatePkceChallenge;
8
+ const node_crypto_1 = require("node:crypto");
9
+ function base64UrlEncode(buffer) {
10
+ return buffer
11
+ .toString('base64')
12
+ .replace(/\+/g, '-')
13
+ .replace(/\//g, '_')
14
+ .replace(/=+$/, '');
15
+ }
16
+ function generatePkceVerifier(length = 32) {
17
+ return base64UrlEncode((0, node_crypto_1.randomBytes)(length));
18
+ }
19
+ function generatePkceChallenge(verifier) {
20
+ const hash = (0, node_crypto_1.createHash)('sha256').update(verifier).digest();
21
+ return base64UrlEncode(hash);
22
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * OIDC token endpoint helpers
3
+ */
4
+ import type { ILogger } from '@mcp-abap-adt/interfaces';
5
+ export interface OidcTokenResponse {
6
+ accessToken: string;
7
+ refreshToken?: string;
8
+ idToken?: string;
9
+ expiresIn?: number;
10
+ tokenType?: string;
11
+ }
12
+ export declare function exchangeAuthorizationCode(tokenEndpoint: string, clientId: string, clientSecret: string | undefined, code: string, redirectUri: string, codeVerifier: string, logger?: ILogger): Promise<OidcTokenResponse>;
13
+ export declare function refreshOidcToken(tokenEndpoint: string, clientId: string, clientSecret: string | undefined, refreshToken: string, logger?: ILogger): Promise<OidcTokenResponse>;
14
+ export interface OidcDeviceFlowInitResponse {
15
+ deviceCode: string;
16
+ userCode: string;
17
+ verificationUri: string;
18
+ verificationUriComplete?: string;
19
+ interval?: number;
20
+ expiresIn?: number;
21
+ }
22
+ export declare function initiateDeviceAuthorization(deviceEndpoint: string, clientId: string, scope: string | undefined, logger?: ILogger): Promise<OidcDeviceFlowInitResponse>;
23
+ export declare function pollDeviceTokens(tokenEndpoint: string, clientId: string, clientSecret: string | undefined, deviceCode: string, interval?: number, logger?: ILogger): Promise<OidcTokenResponse>;
24
+ export declare function passwordGrant(tokenEndpoint: string, clientId: string, clientSecret: string | undefined, username: string, password: string, scope: string | undefined, logger?: ILogger): Promise<OidcTokenResponse>;
25
+ export declare function tokenExchange(tokenEndpoint: string, clientId: string, clientSecret: string | undefined, subjectToken: string, subjectTokenType: string, scope: string | undefined, audience: string | undefined, actorToken?: string, actorTokenType?: string, logger?: ILogger): Promise<OidcTokenResponse>;
26
+ //# sourceMappingURL=oidcToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidcToken.d.ts","sourceRoot":"","sources":["../../src/auth/oidcToken.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAGxD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA6BD,wBAAsB,yBAAyB,CAC7C,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,iBAAiB,CAAC,CAoB5B;AAED,wBAAsB,gBAAgB,CACpC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,iBAAiB,CAAC,CAgB5B;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,2BAA2B,CAC/C,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CA0BrC;AAED,wBAAsB,gBAAgB,CACpC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,MAAU,EACpB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,iBAAiB,CAAC,CA8B5B;AAED,wBAAsB,aAAa,CACjC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,iBAAiB,CAAC,CAoB5B;AAED,wBAAsB,aAAa,CACjC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,MAAM,EACvB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,iBAAiB,CAAC,CAgC5B"}
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * OIDC token endpoint helpers
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
10
+ exports.refreshOidcToken = refreshOidcToken;
11
+ exports.initiateDeviceAuthorization = initiateDeviceAuthorization;
12
+ exports.pollDeviceTokens = pollDeviceTokens;
13
+ exports.passwordGrant = passwordGrant;
14
+ exports.tokenExchange = tokenExchange;
15
+ const axios_1 = __importDefault(require("axios"));
16
+ function toBasicAuth(clientId, clientSecret) {
17
+ return Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
18
+ }
19
+ function buildAuthHeaders(clientId, clientSecret) {
20
+ if (clientSecret) {
21
+ return { Authorization: `Basic ${toBasicAuth(clientId, clientSecret)}` };
22
+ }
23
+ return {};
24
+ }
25
+ function mapTokenResponse(data) {
26
+ if (!data?.access_token) {
27
+ throw new Error('Token response missing access_token');
28
+ }
29
+ return {
30
+ accessToken: data.access_token,
31
+ refreshToken: data.refresh_token,
32
+ idToken: data.id_token,
33
+ expiresIn: data.expires_in,
34
+ tokenType: data.token_type,
35
+ };
36
+ }
37
+ async function exchangeAuthorizationCode(tokenEndpoint, clientId, clientSecret, code, redirectUri, codeVerifier, logger) {
38
+ const params = new URLSearchParams();
39
+ params.append('grant_type', 'authorization_code');
40
+ params.append('code', code);
41
+ params.append('redirect_uri', redirectUri);
42
+ params.append('code_verifier', codeVerifier);
43
+ params.append('client_id', clientId);
44
+ logger?.info('[OIDC] Exchanging authorization code for tokens', {
45
+ tokenEndpoint,
46
+ });
47
+ const response = await axios_1.default.post(tokenEndpoint, params.toString(), {
48
+ headers: {
49
+ 'Content-Type': 'application/x-www-form-urlencoded',
50
+ ...buildAuthHeaders(clientId, clientSecret),
51
+ },
52
+ });
53
+ return mapTokenResponse(response.data);
54
+ }
55
+ async function refreshOidcToken(tokenEndpoint, clientId, clientSecret, refreshToken, logger) {
56
+ const params = new URLSearchParams();
57
+ params.append('grant_type', 'refresh_token');
58
+ params.append('refresh_token', refreshToken);
59
+ params.append('client_id', clientId);
60
+ logger?.info('[OIDC] Refreshing token', { tokenEndpoint });
61
+ const response = await axios_1.default.post(tokenEndpoint, params.toString(), {
62
+ headers: {
63
+ 'Content-Type': 'application/x-www-form-urlencoded',
64
+ ...buildAuthHeaders(clientId, clientSecret),
65
+ },
66
+ });
67
+ return mapTokenResponse(response.data);
68
+ }
69
+ async function initiateDeviceAuthorization(deviceEndpoint, clientId, scope, logger) {
70
+ const params = new URLSearchParams();
71
+ params.append('client_id', clientId);
72
+ if (scope) {
73
+ params.append('scope', scope);
74
+ }
75
+ logger?.info('[OIDC] Initiating device authorization', { deviceEndpoint });
76
+ const response = await axios_1.default.post(deviceEndpoint, params.toString(), {
77
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
78
+ });
79
+ const data = response.data;
80
+ if (!data?.device_code || !data?.user_code || !data?.verification_uri) {
81
+ throw new Error('Device authorization response missing required fields');
82
+ }
83
+ return {
84
+ deviceCode: data.device_code,
85
+ userCode: data.user_code,
86
+ verificationUri: data.verification_uri,
87
+ verificationUriComplete: data.verification_uri_complete,
88
+ interval: data.interval,
89
+ expiresIn: data.expires_in,
90
+ };
91
+ }
92
+ async function pollDeviceTokens(tokenEndpoint, clientId, clientSecret, deviceCode, interval = 5, logger) {
93
+ const params = new URLSearchParams();
94
+ params.append('grant_type', 'urn:ietf:params:oauth:grant-type:device_code');
95
+ params.append('device_code', deviceCode);
96
+ params.append('client_id', clientId);
97
+ while (true) {
98
+ try {
99
+ const response = await axios_1.default.post(tokenEndpoint, params.toString(), {
100
+ headers: {
101
+ 'Content-Type': 'application/x-www-form-urlencoded',
102
+ ...buildAuthHeaders(clientId, clientSecret),
103
+ },
104
+ });
105
+ return mapTokenResponse(response.data);
106
+ }
107
+ catch (error) {
108
+ const status = error?.response?.status;
109
+ const errorCode = error?.response?.data?.error;
110
+ if (status === 400 &&
111
+ (errorCode === 'authorization_pending' || errorCode === 'slow_down')) {
112
+ const wait = errorCode === 'slow_down' ? interval + 5 : interval;
113
+ logger?.debug('[OIDC] Device authorization pending', { wait });
114
+ await new Promise((resolve) => setTimeout(resolve, wait * 1000));
115
+ continue;
116
+ }
117
+ throw error;
118
+ }
119
+ }
120
+ }
121
+ async function passwordGrant(tokenEndpoint, clientId, clientSecret, username, password, scope, logger) {
122
+ const params = new URLSearchParams();
123
+ params.append('grant_type', 'password');
124
+ params.append('username', username);
125
+ params.append('password', password);
126
+ params.append('client_id', clientId);
127
+ if (scope) {
128
+ params.append('scope', scope);
129
+ }
130
+ logger?.info('[OIDC] Performing password grant', { tokenEndpoint });
131
+ const response = await axios_1.default.post(tokenEndpoint, params.toString(), {
132
+ headers: {
133
+ 'Content-Type': 'application/x-www-form-urlencoded',
134
+ ...buildAuthHeaders(clientId, clientSecret),
135
+ },
136
+ });
137
+ return mapTokenResponse(response.data);
138
+ }
139
+ async function tokenExchange(tokenEndpoint, clientId, clientSecret, subjectToken, subjectTokenType, scope, audience, actorToken, actorTokenType, logger) {
140
+ const params = new URLSearchParams();
141
+ params.append('grant_type', 'urn:ietf:params:oauth:grant-type:token-exchange');
142
+ params.append('subject_token', subjectToken);
143
+ params.append('subject_token_type', subjectTokenType);
144
+ params.append('client_id', clientId);
145
+ if (scope) {
146
+ params.append('scope', scope);
147
+ }
148
+ if (audience) {
149
+ params.append('audience', audience);
150
+ }
151
+ if (actorToken) {
152
+ params.append('actor_token', actorToken);
153
+ }
154
+ if (actorTokenType) {
155
+ params.append('actor_token_type', actorTokenType);
156
+ }
157
+ logger?.info('[OIDC] Performing token exchange', { tokenEndpoint });
158
+ const response = await axios_1.default.post(tokenEndpoint, params.toString(), {
159
+ headers: {
160
+ 'Content-Type': 'application/x-www-form-urlencoded',
161
+ ...buildAuthHeaders(clientId, clientSecret),
162
+ },
163
+ });
164
+ return mapTokenResponse(response.data);
165
+ }