@mcp-abap-adt/auth-providers 1.1.0 → 1.2.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.
- package/CHANGELOG.md +24 -0
- package/README.md +7 -3
- package/dist/auth/browserAuth.d.ts +7 -8
- package/dist/auth/browserAuth.d.ts.map +1 -1
- package/dist/auth/browserAuth.js +149 -498
- package/dist/auth/callbackServer.d.ts +40 -0
- package/dist/auth/callbackServer.d.ts.map +1 -0
- package/dist/auth/callbackServer.js +322 -0
- package/dist/auth/oidcBrowserAuth.d.ts +10 -2
- package/dist/auth/oidcBrowserAuth.d.ts.map +1 -1
- package/dist/auth/oidcBrowserAuth.js +37 -37
- package/dist/auth/saml2Auth.d.ts +8 -1
- package/dist/auth/saml2Auth.d.ts.map +1 -1
- package/dist/auth/saml2Auth.js +42 -42
- package/dist/providers/AuthorizationCodeProvider.d.ts.map +1 -1
- package/dist/providers/AuthorizationCodeProvider.js +11 -12
- package/package.json +4 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped callback servers for interactive authorization flows.
|
|
3
|
+
*
|
|
4
|
+
* One owner, one release point. The socket belongs to the scope, not to the
|
|
5
|
+
* promise a caller happens to be awaiting: it is released on the first terminal
|
|
6
|
+
* outcome — the body returning or throwing, an explicit failure, the timeout, or
|
|
7
|
+
* an abort — and the factory settles only once it is actually free.
|
|
8
|
+
*
|
|
9
|
+
* See `docs/superpowers/specs/2026-07-28-callback-server-contract-design.md`.
|
|
10
|
+
*/
|
|
11
|
+
import type { CallbackServerFactory, ICallbackServerHandle, ICallbackServerOptions } from '@mcp-abap-adt/interfaces';
|
|
12
|
+
import express from 'express';
|
|
13
|
+
/**
|
|
14
|
+
* How a route reports an outcome. Settling is deferred until the response has
|
|
15
|
+
* actually flushed — `res.send()` returning does not mean the bytes have left,
|
|
16
|
+
* and settling earlier races the shutdown against the page being delivered.
|
|
17
|
+
*/
|
|
18
|
+
export interface Settle<TResult> {
|
|
19
|
+
/** The callback delivered a result. Does not end the scope by itself. */
|
|
20
|
+
ok(value: TResult, res?: express.Response): void;
|
|
21
|
+
/** The callback reported a failure. Ends the scope. */
|
|
22
|
+
err(error: Error, res?: express.Response): void;
|
|
23
|
+
}
|
|
24
|
+
export type RouteSetup<TResult> = (app: express.Express, settle: Settle<TResult>) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Owns the socket for the duration of `use`.
|
|
27
|
+
*
|
|
28
|
+
* Every flow registers its routes through `routes` and reports outcomes through
|
|
29
|
+
* `settle`; nothing else touches the internal promise, which is what keeps the
|
|
30
|
+
* "settles exactly once" guarantee in one place.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runCallbackScope<TResult, TReturn>(options: ICallbackServerOptions, routes: RouteSetup<TResult>, use: (server: ICallbackServerHandle<TResult>) => Promise<TReturn>): Promise<TReturn>;
|
|
33
|
+
/**
|
|
34
|
+
* Callback server for the UAA authorization-code flow.
|
|
35
|
+
*
|
|
36
|
+
* Delivers the authorization code. The paste form and `/submit` are kept for
|
|
37
|
+
* the case where the browser runs on another machine.
|
|
38
|
+
*/
|
|
39
|
+
export declare const withBrowserCallbackServer: CallbackServerFactory<string>;
|
|
40
|
+
//# sourceMappingURL=callbackServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callbackServer.d.ts","sourceRoot":"","sources":["../../src/auth/callbackServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,OAAO,MAAM,SAAS,CAAC;AAS9B;;;;GAIG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO;IAC7B,yEAAyE;IACzE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IACjD,uDAAuD;IACvD,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,CAChC,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KACpB,IAAI,CAAC;AAqBV;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EACrD,OAAO,EAAE,sBAAsB,EAC/B,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAC3B,GAAG,EAAE,CAAC,MAAM,EAAE,qBAAqB,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAChE,OAAO,CAAC,OAAO,CAAC,CAuLlB;AAyCD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,qBAAqB,CAAC,MAAM,CA4DjE,CAAC"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scoped callback servers for interactive authorization flows.
|
|
4
|
+
*
|
|
5
|
+
* One owner, one release point. The socket belongs to the scope, not to the
|
|
6
|
+
* promise a caller happens to be awaiting: it is released on the first terminal
|
|
7
|
+
* outcome — the body returning or throwing, an explicit failure, the timeout, or
|
|
8
|
+
* an abort — and the factory settles only once it is actually free.
|
|
9
|
+
*
|
|
10
|
+
* See `docs/superpowers/specs/2026-07-28-callback-server-contract-design.md`.
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
46
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.withBrowserCallbackServer = void 0;
|
|
50
|
+
exports.runCallbackScope = runCallbackScope;
|
|
51
|
+
const http = __importStar(require("node:http"));
|
|
52
|
+
const express_1 = __importDefault(require("express"));
|
|
53
|
+
const browserAuth_1 = require("./browserAuth");
|
|
54
|
+
/** Node's `setTimeout` takes a 32-bit signed delay; above this it fires in 1 ms. */
|
|
55
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
56
|
+
/** How long shutdown waits for `close` before destroying what is left. */
|
|
57
|
+
const SHUTDOWN_GRACE_MS = 500;
|
|
58
|
+
function validate(options) {
|
|
59
|
+
const { port, timeoutMs } = options;
|
|
60
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
61
|
+
throw new Error(`Invalid callback server port: ${String(port)}. Must be an integer in 1..65535.`);
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(timeoutMs) ||
|
|
64
|
+
timeoutMs <= 0 ||
|
|
65
|
+
timeoutMs > MAX_TIMEOUT_MS) {
|
|
66
|
+
throw new Error(`Invalid callback server timeoutMs: ${String(timeoutMs)}. ` +
|
|
67
|
+
`Must be finite and within 1..${MAX_TIMEOUT_MS}.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Owns the socket for the duration of `use`.
|
|
72
|
+
*
|
|
73
|
+
* Every flow registers its routes through `routes` and reports outcomes through
|
|
74
|
+
* `settle`; nothing else touches the internal promise, which is what keeps the
|
|
75
|
+
* "settles exactly once" guarantee in one place.
|
|
76
|
+
*/
|
|
77
|
+
async function runCallbackScope(options, routes, use) {
|
|
78
|
+
validate(options);
|
|
79
|
+
if (options.signal?.aborted) {
|
|
80
|
+
throw new Error('Callback server aborted before it started');
|
|
81
|
+
}
|
|
82
|
+
const app = (0, express_1.default)();
|
|
83
|
+
const server = http.createServer(app);
|
|
84
|
+
const sockets = new Set();
|
|
85
|
+
server.on('connection', (socket) => {
|
|
86
|
+
sockets.add(socket);
|
|
87
|
+
socket.on('close', () => sockets.delete(socket));
|
|
88
|
+
});
|
|
89
|
+
let resultSettled = false;
|
|
90
|
+
let resolveResult;
|
|
91
|
+
let rejectResult;
|
|
92
|
+
const resultPromise = new Promise((res, rej) => {
|
|
93
|
+
resolveResult = res;
|
|
94
|
+
rejectResult = rej;
|
|
95
|
+
});
|
|
96
|
+
// Marked handled at creation: a body may create this promise and walk away,
|
|
97
|
+
// and rejecting it at scope end would otherwise raise unhandledRejection.
|
|
98
|
+
void resultPromise.catch(() => undefined);
|
|
99
|
+
let scopeSettled = false;
|
|
100
|
+
let resolveScope;
|
|
101
|
+
let rejectScope;
|
|
102
|
+
const scopePromise = new Promise((res, rej) => {
|
|
103
|
+
resolveScope = res;
|
|
104
|
+
rejectScope = rej;
|
|
105
|
+
});
|
|
106
|
+
let timer = null;
|
|
107
|
+
let alive = false;
|
|
108
|
+
const settleResult = (outcome) => {
|
|
109
|
+
if (resultSettled)
|
|
110
|
+
return;
|
|
111
|
+
resultSettled = true;
|
|
112
|
+
if ('value' in outcome)
|
|
113
|
+
resolveResult(outcome.value);
|
|
114
|
+
else
|
|
115
|
+
rejectResult(outcome.error);
|
|
116
|
+
};
|
|
117
|
+
/** The one place a scope ends. Everything after the first call is a no-op. */
|
|
118
|
+
const endScope = (outcome) => {
|
|
119
|
+
if (scopeSettled)
|
|
120
|
+
return;
|
|
121
|
+
scopeSettled = true;
|
|
122
|
+
alive = false;
|
|
123
|
+
if (timer) {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
timer = null;
|
|
126
|
+
}
|
|
127
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
128
|
+
settleResult({
|
|
129
|
+
error: new Error('Callback server closed before a result arrived'),
|
|
130
|
+
});
|
|
131
|
+
void shutdown().then(() => {
|
|
132
|
+
if ('value' in outcome)
|
|
133
|
+
resolveScope(outcome.value);
|
|
134
|
+
else
|
|
135
|
+
rejectScope(outcome.error);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
function onAbort() {
|
|
139
|
+
endScope({ error: new Error('Callback server aborted') });
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Stop accepting, end idle connections, wait for `close` under a grace, then
|
|
143
|
+
* destroy whatever is left. `closeAllConnections()` is deliberately not used
|
|
144
|
+
* in the first step: it destroys active connections too, and the one carrying
|
|
145
|
+
* the success page is active.
|
|
146
|
+
*/
|
|
147
|
+
function shutdown() {
|
|
148
|
+
return new Promise((done) => {
|
|
149
|
+
let finished = false;
|
|
150
|
+
const finish = () => {
|
|
151
|
+
if (finished)
|
|
152
|
+
return;
|
|
153
|
+
finished = true;
|
|
154
|
+
done();
|
|
155
|
+
};
|
|
156
|
+
// `close()` alone, first. From Node 19 it ends idle connections itself,
|
|
157
|
+
// and it does so gracefully — the client still reads what was written.
|
|
158
|
+
// `closeIdleConnections()` destroys instead, which cuts the success page
|
|
159
|
+
// off mid-read: `finish` on the response means "handed to the OS", not
|
|
160
|
+
// "read by the client".
|
|
161
|
+
server.close(() => finish());
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
if (finished)
|
|
164
|
+
return;
|
|
165
|
+
// Grace expired — on Node 18.x `close()` does not end idle connections,
|
|
166
|
+
// and an active one may simply be stuck. Force it, bounded.
|
|
167
|
+
server.closeIdleConnections?.();
|
|
168
|
+
server.closeAllConnections?.();
|
|
169
|
+
for (const socket of sockets)
|
|
170
|
+
socket.destroy();
|
|
171
|
+
finish();
|
|
172
|
+
}, SHUTDOWN_GRACE_MS).unref?.();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Settle only once the response has actually flushed, so shutdown cannot cut
|
|
177
|
+
* it off.
|
|
178
|
+
*
|
|
179
|
+
* The check is `writableFinished`, not `writableEnded`: the latter is true as
|
|
180
|
+
* soon as `end()` has been called and says nothing about the data having
|
|
181
|
+
* left. Measured on Node 25 with a paused client — an 800-byte body reports
|
|
182
|
+
* both flags true at once, but a 20 MB body reports `writableEnded` true and
|
|
183
|
+
* `writableFinished` false, with `finish` arriving 456 ms later. Keying off
|
|
184
|
+
* `writableEnded` therefore made this deferral a no-op on the very path it
|
|
185
|
+
* exists for.
|
|
186
|
+
*/
|
|
187
|
+
const afterFlush = (res, then) => {
|
|
188
|
+
if (!res || res.writableFinished) {
|
|
189
|
+
then();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
res.once('finish', then);
|
|
193
|
+
res.once('close', then);
|
|
194
|
+
};
|
|
195
|
+
const settle = {
|
|
196
|
+
ok(value, res) {
|
|
197
|
+
afterFlush(res, () => settleResult({ value }));
|
|
198
|
+
},
|
|
199
|
+
err(error, res) {
|
|
200
|
+
afterFlush(res, () => {
|
|
201
|
+
settleResult({ error });
|
|
202
|
+
endScope({ error });
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
routes(app, settle);
|
|
207
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
208
|
+
const handle = {
|
|
209
|
+
port: options.port,
|
|
210
|
+
redirectUri: `http://localhost:${options.port}/callback`,
|
|
211
|
+
waitForResult: () => alive
|
|
212
|
+
? resultPromise
|
|
213
|
+
: Promise.reject(new Error('Callback server scope has ended')),
|
|
214
|
+
// Silent no-op once the scope has ended: this is called fire-and-forget
|
|
215
|
+
// from a browser launcher's .catch(), and a late rejection must not become
|
|
216
|
+
// a fresh unhandled rejection.
|
|
217
|
+
fail: (error) => {
|
|
218
|
+
if (!alive)
|
|
219
|
+
return;
|
|
220
|
+
settleResult({ error });
|
|
221
|
+
endScope({ error });
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
server.once('error', (error) => {
|
|
225
|
+
endScope({ error });
|
|
226
|
+
});
|
|
227
|
+
server.listen(options.port, () => {
|
|
228
|
+
if (scopeSettled) {
|
|
229
|
+
// Aborted while binding.
|
|
230
|
+
void shutdown();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
alive = true;
|
|
234
|
+
timer = setTimeout(() => {
|
|
235
|
+
endScope({
|
|
236
|
+
error: new Error(`Authentication timeout after ${options.timeoutMs / 1000} seconds. Please try again.`),
|
|
237
|
+
});
|
|
238
|
+
}, options.timeoutMs);
|
|
239
|
+
void use(handle).then((value) => endScope({ value }), (error) => endScope({ error }));
|
|
240
|
+
});
|
|
241
|
+
return await scopePromise;
|
|
242
|
+
}
|
|
243
|
+
const successHtml = `<!DOCTYPE html>
|
|
244
|
+
<html lang="en"><head><meta charset="UTF-8">
|
|
245
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
246
|
+
<title>SAP BTP Authentication</title>
|
|
247
|
+
<style>body{font-family:'Segoe UI',Tahoma,sans-serif;text-align:center;padding:50px 20px;background:linear-gradient(135deg,#0070f3,#00d4ff);color:#fff;min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center}.container{background:rgba(255,255,255,.1);border-radius:20px;padding:40px;max-width:500px}.success-icon{font-size:4rem;margin-bottom:20px;color:#4ade80}h1{font-weight:300}</style>
|
|
248
|
+
</head><body><div class="container"><div class="success-icon">✓</div>
|
|
249
|
+
<h1>Authentication Successful!</h1>
|
|
250
|
+
<p>You have successfully authenticated with SAP BTP. You can close this window.</p>
|
|
251
|
+
</div></body></html>`;
|
|
252
|
+
const errorHtml = (message) => `<!DOCTYPE html>
|
|
253
|
+
<html lang="en"><head><meta charset="UTF-8">
|
|
254
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
255
|
+
<title>Authentication Error</title>
|
|
256
|
+
<style>body{font-family:'Segoe UI',Tahoma,sans-serif;text-align:center;padding:50px 20px;background:linear-gradient(135deg,#dc2626,#ef4444);color:#fff;min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center}.container{background:rgba(255,255,255,.1);border-radius:20px;padding:40px;max-width:500px}.error-icon{font-size:4rem;margin-bottom:20px;color:#fbbf24}h1{font-weight:300}</style>
|
|
257
|
+
</head><body><div class="container"><div class="error-icon">✗</div>
|
|
258
|
+
<h1>Authentication Failed</h1>
|
|
259
|
+
<p>${message}</p>
|
|
260
|
+
<p>Please check your service key configuration and try again.</p>
|
|
261
|
+
</div></body></html>`;
|
|
262
|
+
// Manual paste form (GET /). Used when the automatic localhost callback cannot
|
|
263
|
+
// reach this server (browser on another machine). Accepts a bare code or a full
|
|
264
|
+
// redirected URL; re-renders with a message on a bad paste.
|
|
265
|
+
const pasteFormHtml = (message) => `<!DOCTYPE html>
|
|
266
|
+
<html lang="en"><head><meta charset="UTF-8">
|
|
267
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
268
|
+
<title>SAP BTP Authentication — paste code</title>
|
|
269
|
+
<style>body{font-family:'Segoe UI',Tahoma,sans-serif;text-align:center;padding:50px 20px;background:linear-gradient(135deg,#0070f3,#00d4ff);color:#fff;min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center}.container{background:rgba(255,255,255,.1);border-radius:20px;padding:40px;max-width:560px;width:100%}h1{font-weight:300}input{width:100%;padding:12px;border-radius:8px;border:none;font-size:1rem;box-sizing:border-box;margin:14px 0}button{padding:12px 24px;border-radius:8px;border:none;background:#fff;color:#0070f3;font-size:1rem;cursor:pointer}.msg{color:#fde68a;margin-bottom:10px}</style>
|
|
270
|
+
</head><body><div class="container">
|
|
271
|
+
<h1>Paste authorization code</h1>
|
|
272
|
+
${message ? `<p class="msg">${message}</p>` : ''}
|
|
273
|
+
<p>After signing in, copy the <code>code</code> from your browser's address bar
|
|
274
|
+
(or paste the whole redirected URL) and submit it here.</p>
|
|
275
|
+
<form action="/submit" method="get">
|
|
276
|
+
<input name="input" autofocus placeholder="code=... or http://localhost/callback?code=..." />
|
|
277
|
+
<button type="submit">Submit</button>
|
|
278
|
+
</form></div></body></html>`;
|
|
279
|
+
/**
|
|
280
|
+
* Callback server for the UAA authorization-code flow.
|
|
281
|
+
*
|
|
282
|
+
* Delivers the authorization code. The paste form and `/submit` are kept for
|
|
283
|
+
* the case where the browser runs on another machine.
|
|
284
|
+
*/
|
|
285
|
+
const withBrowserCallbackServer = (options, use) => runCallbackScope(options, (app, settle) => {
|
|
286
|
+
app.get('/callback', (req, res) => {
|
|
287
|
+
const { error, error_description, error_uri } = req.query;
|
|
288
|
+
if (error) {
|
|
289
|
+
const message = error_description
|
|
290
|
+
? `${String(error)}: ${String(error_description)}`
|
|
291
|
+
: String(error);
|
|
292
|
+
res.status(400).send(errorHtml(message));
|
|
293
|
+
settle.err(new Error(`OAuth2 authentication failed: ${message}` +
|
|
294
|
+
(error_uri ? ` (${String(error_uri)})` : '')), res);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const { code } = req.query;
|
|
298
|
+
if (!code || typeof code !== 'string') {
|
|
299
|
+
res.status(400).send('Error: Authorization code missing');
|
|
300
|
+
settle.err(new Error('Authorization code missing'), res);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
res.send(successHtml);
|
|
304
|
+
settle.ok(code, res);
|
|
305
|
+
});
|
|
306
|
+
app.get('/', (_req, res) => {
|
|
307
|
+
res.send(pasteFormHtml());
|
|
308
|
+
});
|
|
309
|
+
app.get('/submit', (req, res) => {
|
|
310
|
+
const raw = req.query.input ?? req.query.code;
|
|
311
|
+
const code = typeof raw === 'string' ? (0, browserAuth_1.extractCode)(raw) : null;
|
|
312
|
+
if (!code) {
|
|
313
|
+
res
|
|
314
|
+
.status(400)
|
|
315
|
+
.send(pasteFormHtml('Could not read an authorization code from that input. Try again.'));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
res.send(successHtml);
|
|
319
|
+
settle.ok(code, res);
|
|
320
|
+
});
|
|
321
|
+
}, use);
|
|
322
|
+
exports.withBrowserCallbackServer = withBrowserCallbackServer;
|
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
* OIDC browser authorization code flow (capture code)
|
|
3
3
|
*/
|
|
4
4
|
import type { ILogger } from '@mcp-abap-adt/interfaces';
|
|
5
|
-
export
|
|
5
|
+
export interface OidcCallbackResult {
|
|
6
6
|
code: string;
|
|
7
7
|
state?: string;
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* OIDC browser login.
|
|
11
|
+
*
|
|
12
|
+
* The callback socket belongs to the scope: it is released when the scope ends,
|
|
13
|
+
* whatever ends it. Before this, the flow had no timeout at all — an abandoned
|
|
14
|
+
* login never settled and the port was held for the life of the process.
|
|
15
|
+
*/
|
|
16
|
+
export declare function startOidcBrowserAuth(authorizationUrl: string, browser: string, logger?: ILogger, port?: number, timeoutMs?: number): Promise<OidcCallbackResult>;
|
|
9
17
|
//# sourceMappingURL=oidcBrowserAuth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oidcBrowserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/oidcBrowserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"oidcBrowserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/oidcBrowserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAIV,OAAO,EACR,MAAM,0BAA0B,CAAC;AAqElC,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+BD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,EACnB,SAAS,GAAE,MAAkB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CAyB7B"}
|
|
@@ -35,14 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
return result;
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
-
};
|
|
41
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
39
|
exports.startOidcBrowserAuth = startOidcBrowserAuth;
|
|
43
|
-
const http = __importStar(require("node:http"));
|
|
44
40
|
const net = __importStar(require("node:net"));
|
|
45
|
-
const
|
|
41
|
+
const callbackServer_1 = require("./callbackServer");
|
|
46
42
|
const BROWSER_MAP = {
|
|
47
43
|
chrome: 'chrome',
|
|
48
44
|
edge: 'msedge',
|
|
@@ -103,42 +99,46 @@ async function openBrowserUrl(authorizationUrl, browser, logger) {
|
|
|
103
99
|
logger?.info('[OIDC] Open URL manually', { authorizationUrl });
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
|
-
|
|
102
|
+
const withOidcCallbackServer = (options, use) => (0, callbackServer_1.runCallbackScope)(options, (app, settle) => {
|
|
103
|
+
app.get('/callback', (req, res) => {
|
|
104
|
+
const code = req.query.code;
|
|
105
|
+
const state = req.query.state;
|
|
106
|
+
if (!code || typeof code !== 'string') {
|
|
107
|
+
res.status(400).send('Missing authorization code');
|
|
108
|
+
settle.err(new Error('Missing authorization code'), res);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
res
|
|
112
|
+
.status(200)
|
|
113
|
+
.send('Authentication complete. You can close this window.');
|
|
114
|
+
settle.ok({ code, state: typeof state === 'string' ? state : undefined }, res);
|
|
115
|
+
});
|
|
116
|
+
}, use);
|
|
117
|
+
/**
|
|
118
|
+
* OIDC browser login.
|
|
119
|
+
*
|
|
120
|
+
* The callback socket belongs to the scope: it is released when the scope ends,
|
|
121
|
+
* whatever ends it. Before this, the flow had no timeout at all — an abandoned
|
|
122
|
+
* login never settled and the port was held for the life of the process.
|
|
123
|
+
*/
|
|
124
|
+
async function startOidcBrowserAuth(authorizationUrl, browser, logger, port = 3001, timeoutMs = 30 * 1000) {
|
|
125
|
+
// Pre-check kept for its message: AuthBroker matches /already in use/i to
|
|
126
|
+
// distinguish a busy port from other failures.
|
|
107
127
|
const portAvailable = await isPortAvailable(port);
|
|
108
128
|
if (!portAvailable) {
|
|
109
129
|
throw new Error(`Port ${port} is already in use. Please specify a different port or free the port.`);
|
|
110
130
|
}
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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);
|
|
131
|
+
return await withOidcCallbackServer({ port, timeoutMs }, async (server) => {
|
|
132
|
+
logger?.info('[OIDC] Callback server listening', { port: server.port });
|
|
133
|
+
const waiting = server.waitForResult();
|
|
134
|
+
// Not awaited: a launcher that hangs must not delay the timeout or the
|
|
135
|
+
// release of the port.
|
|
136
|
+
void openBrowserUrl(authorizationUrl, browser, logger).catch((error) => {
|
|
137
|
+
logger?.warn('[OIDC] Failed to open browser', {
|
|
138
|
+
error: error instanceof Error ? error.message : String(error),
|
|
139
|
+
});
|
|
140
|
+
logger?.info('[OIDC] Open URL manually', { authorizationUrl });
|
|
142
141
|
});
|
|
142
|
+
return await waiting;
|
|
143
143
|
});
|
|
144
144
|
}
|
package/dist/auth/saml2Auth.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface Saml2AuthConfig {
|
|
|
10
10
|
authorizationUrl?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function buildSamlAuthorizationUrl(config: Saml2AuthConfig): string;
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* SAML browser login.
|
|
15
|
+
*
|
|
16
|
+
* The callback socket belongs to the scope: it is released when the scope ends,
|
|
17
|
+
* whatever ends it. Before this, the flow had no timeout at all — an abandoned
|
|
18
|
+
* login never settled and the port was held for the life of the process.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startSamlBrowserAuth(config: Saml2AuthConfig, browser: string, logger?: ILogger, port?: number, timeoutMs?: number): Promise<string>;
|
|
14
21
|
export declare function parseSamlNotOnOrAfter(samlResponse: string): number | undefined;
|
|
15
22
|
//# sourceMappingURL=saml2Auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"saml2Auth.d.ts","sourceRoot":"","sources":["../../src/auth/saml2Auth.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"saml2Auth.d.ts","sourceRoot":"","sources":["../../src/auth/saml2Auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAIV,OAAO,EACR,MAAM,0BAA0B,CAAC;AAIlC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AA8CD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAazE;AA8ED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,EACnB,SAAS,GAAE,MAAkB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA2BjB;AAED,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,SAAS,CAYpB"}
|
package/dist/auth/saml2Auth.js
CHANGED
|
@@ -43,10 +43,10 @@ exports.buildSamlAuthorizationUrl = buildSamlAuthorizationUrl;
|
|
|
43
43
|
exports.startSamlBrowserAuth = startSamlBrowserAuth;
|
|
44
44
|
exports.parseSamlNotOnOrAfter = parseSamlNotOnOrAfter;
|
|
45
45
|
const node_crypto_1 = require("node:crypto");
|
|
46
|
-
const http = __importStar(require("node:http"));
|
|
47
46
|
const net = __importStar(require("node:net"));
|
|
48
47
|
const node_zlib_1 = require("node:zlib");
|
|
49
48
|
const express_1 = __importDefault(require("express"));
|
|
49
|
+
const callbackServer_1 = require("./callbackServer");
|
|
50
50
|
const BROWSER_MAP = {
|
|
51
51
|
chrome: 'chrome',
|
|
52
52
|
edge: 'msedge',
|
|
@@ -140,53 +140,53 @@ async function openBrowserUrl(authorizationUrl, browser, logger) {
|
|
|
140
140
|
logger?.info('[SAML] Open URL manually', { authorizationUrl });
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
const withSamlCallbackServer = (options, use) => (0, callbackServer_1.runCallbackScope)(options, (app, settle) => {
|
|
144
|
+
app.use(express_1.default.urlencoded({ extended: false, limit: '5mb' }));
|
|
145
|
+
const handle = (samlResponse, res) => {
|
|
146
|
+
res
|
|
147
|
+
.status(200)
|
|
148
|
+
.send('SAML authentication complete. You can close this window.');
|
|
149
|
+
if (typeof samlResponse === 'string' && samlResponse) {
|
|
150
|
+
settle.ok(samlResponse, res);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
settle.err(new Error('Missing SAMLResponse'), res);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
app.post('/callback', (req, res) => {
|
|
157
|
+
handle(req.body?.SAMLResponse, res);
|
|
158
|
+
});
|
|
159
|
+
app.get('/callback', (req, res) => {
|
|
160
|
+
handle(req.query.SAMLResponse, res);
|
|
161
|
+
});
|
|
162
|
+
}, use);
|
|
163
|
+
/**
|
|
164
|
+
* SAML browser login.
|
|
165
|
+
*
|
|
166
|
+
* The callback socket belongs to the scope: it is released when the scope ends,
|
|
167
|
+
* whatever ends it. Before this, the flow had no timeout at all — an abandoned
|
|
168
|
+
* login never settled and the port was held for the life of the process.
|
|
169
|
+
*/
|
|
170
|
+
async function startSamlBrowserAuth(config, browser, logger, port = 3001, timeoutMs = 30 * 1000) {
|
|
171
|
+
// Pre-check kept for its message: AuthBroker matches /already in use/i to
|
|
172
|
+
// distinguish a busy port from other failures.
|
|
144
173
|
const portAvailable = await isPortAvailable(port);
|
|
145
174
|
if (!portAvailable) {
|
|
146
175
|
throw new Error(`Port ${port} is already in use. Please specify a different port or free the port.`);
|
|
147
176
|
}
|
|
148
177
|
const authorizationUrl = buildSamlAuthorizationUrl(config);
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return;
|
|
160
|
-
resolved = true;
|
|
161
|
-
server.close();
|
|
162
|
-
};
|
|
163
|
-
const handleResponse = (samlResponse) => {
|
|
164
|
-
if (!samlResponse) {
|
|
165
|
-
cleanup();
|
|
166
|
-
reject(new Error('Missing SAMLResponse'));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
cleanup();
|
|
170
|
-
resolve(samlResponse);
|
|
171
|
-
};
|
|
172
|
-
app.post('/callback', (req, res) => {
|
|
173
|
-
const samlResponse = req.body?.SAMLResponse;
|
|
174
|
-
res
|
|
175
|
-
.status(200)
|
|
176
|
-
.send('SAML authentication complete. You can close this window.');
|
|
177
|
-
handleResponse(typeof samlResponse === 'string' ? samlResponse : undefined);
|
|
178
|
-
});
|
|
179
|
-
app.get('/callback', (req, res) => {
|
|
180
|
-
const samlResponse = req.query.SAMLResponse;
|
|
181
|
-
res
|
|
182
|
-
.status(200)
|
|
183
|
-
.send('SAML authentication complete. You can close this window.');
|
|
184
|
-
handleResponse(typeof samlResponse === 'string' ? samlResponse : undefined);
|
|
185
|
-
});
|
|
186
|
-
server.listen(PORT, async () => {
|
|
187
|
-
logger?.info('[SAML] Callback server listening', { port: PORT });
|
|
188
|
-
await openBrowserUrl(authorizationUrl, browser, logger);
|
|
178
|
+
return await withSamlCallbackServer({ port, timeoutMs }, async (server) => {
|
|
179
|
+
logger?.info('[SAML] Callback server listening', { port: server.port });
|
|
180
|
+
const waiting = server.waitForResult();
|
|
181
|
+
// Not awaited: a launcher that hangs must not delay the timeout or the
|
|
182
|
+
// release of the port.
|
|
183
|
+
void openBrowserUrl(authorizationUrl, browser, logger).catch((error) => {
|
|
184
|
+
logger?.warn('[SAML] Failed to open browser', {
|
|
185
|
+
error: error instanceof Error ? error.message : String(error),
|
|
186
|
+
});
|
|
187
|
+
logger?.info('[SAML] Open URL manually', { authorizationUrl });
|
|
189
188
|
});
|
|
189
|
+
return await waiting;
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
function parseSamlNotOnOrAfter(samlResponse) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthorizationCodeProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AuthorizationCodeProvider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAEV,OAAO,EACP,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"AuthorizationCodeProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AuthorizationCodeProvider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAEV,OAAO,EACP,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAKxD,MAAM,WAAW,+BAA+B;IAE9C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IAGrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,iBAAiB;IAC9D,OAAO,CAAC,MAAM,CAAkC;gBAEpC,MAAM,EAAE,+BAA+B;IA8D7C,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAIxC,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;cAoErC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CA0CxD"}
|