@leanmcp/auth 0.3.1 → 0.4.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.
@@ -0,0 +1,536 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/proxy/index.ts
22
+ var proxy_exports = {};
23
+ __export(proxy_exports, {
24
+ OAuthProxy: () => OAuthProxy,
25
+ azureProvider: () => azureProvider,
26
+ customProvider: () => customProvider,
27
+ discordProvider: () => discordProvider,
28
+ githubProvider: () => githubProvider,
29
+ gitlabProvider: () => gitlabProvider,
30
+ googleProvider: () => googleProvider,
31
+ slackProvider: () => slackProvider
32
+ });
33
+ module.exports = __toCommonJS(proxy_exports);
34
+
35
+ // src/proxy/providers.ts
36
+ function googleProvider(options) {
37
+ return {
38
+ id: "google",
39
+ name: "Google",
40
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
41
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
42
+ userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo",
43
+ clientId: options.clientId,
44
+ clientSecret: options.clientSecret,
45
+ scopes: options.scopes ?? [
46
+ "openid",
47
+ "email",
48
+ "profile"
49
+ ],
50
+ tokenEndpointAuthMethod: "client_secret_post",
51
+ supportsPkce: true,
52
+ authorizationParams: {
53
+ access_type: "offline",
54
+ prompt: "consent"
55
+ }
56
+ };
57
+ }
58
+ __name(googleProvider, "googleProvider");
59
+ function githubProvider(options) {
60
+ return {
61
+ id: "github",
62
+ name: "GitHub",
63
+ authorizationEndpoint: "https://github.com/login/oauth/authorize",
64
+ tokenEndpoint: "https://github.com/login/oauth/access_token",
65
+ userInfoEndpoint: "https://api.github.com/user",
66
+ clientId: options.clientId,
67
+ clientSecret: options.clientSecret,
68
+ scopes: options.scopes ?? [
69
+ "read:user",
70
+ "user:email"
71
+ ],
72
+ tokenEndpointAuthMethod: "client_secret_post",
73
+ supportsPkce: false
74
+ };
75
+ }
76
+ __name(githubProvider, "githubProvider");
77
+ function azureProvider(options) {
78
+ const tenantId = options.tenantId ?? "common";
79
+ return {
80
+ id: "azure",
81
+ name: "Microsoft",
82
+ authorizationEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
83
+ tokenEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
84
+ userInfoEndpoint: "https://graph.microsoft.com/oidc/userinfo",
85
+ clientId: options.clientId,
86
+ clientSecret: options.clientSecret,
87
+ scopes: options.scopes ?? [
88
+ "openid",
89
+ "email",
90
+ "profile",
91
+ "offline_access"
92
+ ],
93
+ tokenEndpointAuthMethod: "client_secret_post",
94
+ supportsPkce: true
95
+ };
96
+ }
97
+ __name(azureProvider, "azureProvider");
98
+ function gitlabProvider(options) {
99
+ const baseUrl = options.baseUrl ?? "https://gitlab.com";
100
+ return {
101
+ id: "gitlab",
102
+ name: "GitLab",
103
+ authorizationEndpoint: `${baseUrl}/oauth/authorize`,
104
+ tokenEndpoint: `${baseUrl}/oauth/token`,
105
+ userInfoEndpoint: `${baseUrl}/api/v4/user`,
106
+ clientId: options.clientId,
107
+ clientSecret: options.clientSecret,
108
+ scopes: options.scopes ?? [
109
+ "openid",
110
+ "read_user",
111
+ "email"
112
+ ],
113
+ tokenEndpointAuthMethod: "client_secret_post",
114
+ supportsPkce: true
115
+ };
116
+ }
117
+ __name(gitlabProvider, "gitlabProvider");
118
+ function slackProvider(options) {
119
+ return {
120
+ id: "slack",
121
+ name: "Slack",
122
+ authorizationEndpoint: "https://slack.com/oauth/v2/authorize",
123
+ tokenEndpoint: "https://slack.com/api/oauth.v2.access",
124
+ userInfoEndpoint: "https://slack.com/api/users.identity",
125
+ clientId: options.clientId,
126
+ clientSecret: options.clientSecret,
127
+ scopes: options.scopes ?? [
128
+ "openid",
129
+ "email",
130
+ "profile"
131
+ ],
132
+ tokenEndpointAuthMethod: "client_secret_post",
133
+ supportsPkce: false
134
+ };
135
+ }
136
+ __name(slackProvider, "slackProvider");
137
+ function discordProvider(options) {
138
+ return {
139
+ id: "discord",
140
+ name: "Discord",
141
+ authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
142
+ tokenEndpoint: "https://discord.com/api/oauth2/token",
143
+ userInfoEndpoint: "https://discord.com/api/users/@me",
144
+ clientId: options.clientId,
145
+ clientSecret: options.clientSecret,
146
+ scopes: options.scopes ?? [
147
+ "identify",
148
+ "email"
149
+ ],
150
+ tokenEndpointAuthMethod: "client_secret_post",
151
+ supportsPkce: true
152
+ };
153
+ }
154
+ __name(discordProvider, "discordProvider");
155
+ function customProvider(config) {
156
+ return {
157
+ tokenEndpointAuthMethod: "client_secret_post",
158
+ supportsPkce: false,
159
+ ...config
160
+ };
161
+ }
162
+ __name(customProvider, "customProvider");
163
+
164
+ // src/proxy/oauth-proxy.ts
165
+ var import_crypto2 = require("crypto");
166
+
167
+ // src/client/pkce.ts
168
+ var import_crypto = require("crypto");
169
+ function generateCodeVerifier(length = 64) {
170
+ if (length < 43 || length > 128) {
171
+ throw new Error("PKCE code verifier must be between 43-128 characters");
172
+ }
173
+ const bytesNeeded = Math.ceil(length * 0.75);
174
+ const randomBuffer = (0, import_crypto.randomBytes)(bytesNeeded);
175
+ return randomBuffer.toString("base64url").slice(0, length);
176
+ }
177
+ __name(generateCodeVerifier, "generateCodeVerifier");
178
+ function generateCodeChallenge(verifier) {
179
+ return (0, import_crypto.createHash)("sha256").update(verifier, "utf8").digest("base64url");
180
+ }
181
+ __name(generateCodeChallenge, "generateCodeChallenge");
182
+ function generatePKCE(verifierLength = 64) {
183
+ const verifier = generateCodeVerifier(verifierLength);
184
+ const challenge = generateCodeChallenge(verifier);
185
+ return {
186
+ verifier,
187
+ challenge,
188
+ method: "S256"
189
+ };
190
+ }
191
+ __name(generatePKCE, "generatePKCE");
192
+
193
+ // src/proxy/oauth-proxy.ts
194
+ var defaultTokenMapper = /* @__PURE__ */ __name(async (externalTokens, userInfo) => {
195
+ return {
196
+ access_token: externalTokens.access_token,
197
+ token_type: externalTokens.token_type,
198
+ expires_in: externalTokens.expires_in,
199
+ refresh_token: externalTokens.refresh_token,
200
+ user_id: userInfo.sub
201
+ };
202
+ }, "defaultTokenMapper");
203
+ var pendingRequests = /* @__PURE__ */ new Map();
204
+ var REQUEST_TTL_MS = 10 * 60 * 1e3;
205
+ function cleanupExpiredRequests() {
206
+ const now = Date.now();
207
+ for (const [key, request] of pendingRequests.entries()) {
208
+ if (now - request.createdAt > REQUEST_TTL_MS) {
209
+ pendingRequests.delete(key);
210
+ }
211
+ }
212
+ }
213
+ __name(cleanupExpiredRequests, "cleanupExpiredRequests");
214
+ setInterval(cleanupExpiredRequests, 60 * 1e3);
215
+ var OAuthProxy = class {
216
+ static {
217
+ __name(this, "OAuthProxy");
218
+ }
219
+ config;
220
+ providersMap;
221
+ constructor(config) {
222
+ this.config = {
223
+ authorizePath: "/authorize",
224
+ tokenPath: "/token",
225
+ callbackPath: "/callback",
226
+ tokenMapper: defaultTokenMapper,
227
+ forwardPkce: true,
228
+ ...config
229
+ };
230
+ this.providersMap = /* @__PURE__ */ new Map();
231
+ for (const provider of config.providers) {
232
+ this.providersMap.set(provider.id, provider);
233
+ }
234
+ }
235
+ /**
236
+ * Get configured providers
237
+ */
238
+ getProviders() {
239
+ return this.config.providers;
240
+ }
241
+ /**
242
+ * Get a provider by ID
243
+ */
244
+ getProvider(id) {
245
+ return this.providersMap.get(id);
246
+ }
247
+ /**
248
+ * Generate state parameter with signature
249
+ */
250
+ generateState() {
251
+ const nonce = (0, import_crypto2.randomUUID)();
252
+ const signature = (0, import_crypto2.createHmac)("sha256", this.config.sessionSecret).update(nonce).digest("hex").substring(0, 8);
253
+ return `${nonce}.${signature}`;
254
+ }
255
+ /**
256
+ * Verify state parameter signature
257
+ */
258
+ verifyState(state) {
259
+ const [nonce, signature] = state.split(".");
260
+ if (!nonce || !signature) return false;
261
+ const expectedSignature = (0, import_crypto2.createHmac)("sha256", this.config.sessionSecret).update(nonce).digest("hex").substring(0, 8);
262
+ return signature === expectedSignature;
263
+ }
264
+ /**
265
+ * Handle authorization request
266
+ *
267
+ * Redirects the user to the external provider's authorization page.
268
+ *
269
+ * @param params - Request parameters
270
+ * @returns URL to redirect the user to
271
+ */
272
+ handleAuthorize(params) {
273
+ const provider = this.providersMap.get(params.provider);
274
+ if (!provider) {
275
+ throw new Error(`Unknown provider: ${params.provider}`);
276
+ }
277
+ const proxyState = this.generateState();
278
+ let codeVerifier;
279
+ let codeChallenge;
280
+ if (provider.supportsPkce && this.config.forwardPkce) {
281
+ const pkce = generatePKCE();
282
+ codeVerifier = pkce.verifier;
283
+ codeChallenge = pkce.challenge;
284
+ }
285
+ const pendingRequest = {
286
+ providerId: params.provider,
287
+ clientRedirectUri: params.redirect_uri,
288
+ clientState: params.state,
289
+ codeVerifier,
290
+ proxyState,
291
+ createdAt: Date.now()
292
+ };
293
+ pendingRequests.set(proxyState, pendingRequest);
294
+ const callbackUrl = `${this.config.baseUrl}${this.config.callbackPath}`;
295
+ const authUrl = new URL(provider.authorizationEndpoint);
296
+ authUrl.searchParams.set("client_id", provider.clientId);
297
+ authUrl.searchParams.set("redirect_uri", callbackUrl);
298
+ authUrl.searchParams.set("response_type", "code");
299
+ authUrl.searchParams.set("state", proxyState);
300
+ const scopes = params.scope?.split(" ") ?? provider.scopes;
301
+ authUrl.searchParams.set("scope", scopes.join(" "));
302
+ if (codeChallenge) {
303
+ authUrl.searchParams.set("code_challenge", codeChallenge);
304
+ authUrl.searchParams.set("code_challenge_method", "S256");
305
+ }
306
+ if (provider.authorizationParams) {
307
+ for (const [key, value] of Object.entries(provider.authorizationParams)) {
308
+ authUrl.searchParams.set(key, value);
309
+ }
310
+ }
311
+ return authUrl.toString();
312
+ }
313
+ /**
314
+ * Handle callback from external provider
315
+ *
316
+ * Exchanges the authorization code for tokens and maps them.
317
+ *
318
+ * @param params - Callback query parameters
319
+ * @returns Result with redirect URI and tokens
320
+ */
321
+ async handleCallback(params) {
322
+ const { code, state, error, error_description } = params;
323
+ if (error) {
324
+ const pendingRequest2 = state ? pendingRequests.get(state) : void 0;
325
+ pendingRequests.delete(state ?? "");
326
+ const redirectUri2 = new URL(pendingRequest2?.clientRedirectUri ?? "/");
327
+ redirectUri2.searchParams.set("error", error);
328
+ if (error_description) {
329
+ redirectUri2.searchParams.set("error_description", error_description);
330
+ }
331
+ if (pendingRequest2?.clientState) {
332
+ redirectUri2.searchParams.set("state", pendingRequest2.clientState);
333
+ }
334
+ return {
335
+ redirectUri: redirectUri2.toString(),
336
+ error
337
+ };
338
+ }
339
+ if (!state || !this.verifyState(state)) {
340
+ throw new Error("Invalid or missing state parameter");
341
+ }
342
+ const pendingRequest = pendingRequests.get(state);
343
+ if (!pendingRequest) {
344
+ throw new Error("State not found - authorization request expired");
345
+ }
346
+ pendingRequests.delete(state);
347
+ if (!code) {
348
+ throw new Error("Missing authorization code");
349
+ }
350
+ const provider = this.providersMap.get(pendingRequest.providerId);
351
+ if (!provider) {
352
+ throw new Error(`Provider not found: ${pendingRequest.providerId}`);
353
+ }
354
+ const callbackUrl = `${this.config.baseUrl}${this.config.callbackPath}`;
355
+ const externalTokens = await this.exchangeCodeForTokens(provider, code, callbackUrl, pendingRequest.codeVerifier);
356
+ let userInfo = {
357
+ sub: "unknown"
358
+ };
359
+ if (provider.userInfoEndpoint) {
360
+ userInfo = await this.fetchUserInfo(provider, externalTokens.access_token);
361
+ }
362
+ const mappedTokens = await this.config.tokenMapper(externalTokens, userInfo, provider);
363
+ const redirectUri = new URL(pendingRequest.clientRedirectUri);
364
+ const internalCode = this.generateInternalCode(mappedTokens);
365
+ redirectUri.searchParams.set("code", internalCode);
366
+ if (pendingRequest.clientState) {
367
+ redirectUri.searchParams.set("state", pendingRequest.clientState);
368
+ }
369
+ return {
370
+ redirectUri: redirectUri.toString(),
371
+ tokens: mappedTokens
372
+ };
373
+ }
374
+ /**
375
+ * Exchange authorization code for tokens with external provider
376
+ */
377
+ async exchangeCodeForTokens(provider, code, redirectUri, codeVerifier) {
378
+ const tokenPayload = {
379
+ grant_type: "authorization_code",
380
+ code,
381
+ redirect_uri: redirectUri,
382
+ client_id: provider.clientId
383
+ };
384
+ if (provider.tokenEndpointAuthMethod === "client_secret_post") {
385
+ tokenPayload.client_secret = provider.clientSecret;
386
+ }
387
+ if (codeVerifier) {
388
+ tokenPayload.code_verifier = codeVerifier;
389
+ }
390
+ if (provider.tokenParams) {
391
+ for (const [key, value] of Object.entries(provider.tokenParams)) {
392
+ tokenPayload[key] = value;
393
+ }
394
+ }
395
+ const headers = {
396
+ "Content-Type": "application/x-www-form-urlencoded",
397
+ "Accept": "application/json"
398
+ };
399
+ if (provider.tokenEndpointAuthMethod === "client_secret_basic") {
400
+ const credentials = Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString("base64");
401
+ headers["Authorization"] = `Basic ${credentials}`;
402
+ }
403
+ const response = await fetch(provider.tokenEndpoint, {
404
+ method: "POST",
405
+ headers,
406
+ body: new URLSearchParams(tokenPayload)
407
+ });
408
+ if (!response.ok) {
409
+ const errorText = await response.text();
410
+ throw new Error(`Token exchange failed: ${response.status} - ${errorText}`);
411
+ }
412
+ return response.json();
413
+ }
414
+ /**
415
+ * Fetch user info from external provider
416
+ */
417
+ async fetchUserInfo(provider, accessToken) {
418
+ if (!provider.userInfoEndpoint) {
419
+ return {
420
+ sub: "unknown"
421
+ };
422
+ }
423
+ const response = await fetch(provider.userInfoEndpoint, {
424
+ headers: {
425
+ "Authorization": `Bearer ${accessToken}`,
426
+ "Accept": "application/json"
427
+ }
428
+ });
429
+ if (!response.ok) {
430
+ console.warn(`Failed to fetch user info: ${response.status}`);
431
+ return {
432
+ sub: "unknown"
433
+ };
434
+ }
435
+ const data = await response.json();
436
+ return {
437
+ sub: data.sub ?? data.id ?? data.user_id ?? "unknown",
438
+ email: data.email,
439
+ email_verified: data.email_verified ?? data.verified_email,
440
+ name: data.name ?? data.login ?? data.display_name,
441
+ picture: data.picture ?? data.avatar_url ?? data.avatar,
442
+ raw: data
443
+ };
444
+ }
445
+ /**
446
+ * Generate an internal authorization code for the mapped tokens
447
+ * This code can be exchanged via the token endpoint
448
+ */
449
+ generateInternalCode(tokens) {
450
+ const code = (0, import_crypto2.randomUUID)();
451
+ const signature = (0, import_crypto2.createHmac)("sha256", this.config.sessionSecret).update(code).update(JSON.stringify(tokens)).digest("hex").substring(0, 16);
452
+ const fullCode = `${code}.${signature}`;
453
+ pendingRequests.set(fullCode, {
454
+ providerId: "_internal",
455
+ clientRedirectUri: "",
456
+ proxyState: fullCode,
457
+ createdAt: Date.now(),
458
+ // @ts-expect-error - storing tokens in pending request
459
+ _tokens: tokens
460
+ });
461
+ setTimeout(() => pendingRequests.delete(fullCode), 5 * 60 * 1e3);
462
+ return fullCode;
463
+ }
464
+ /**
465
+ * Handle token request (exchange internal code for tokens)
466
+ */
467
+ async handleToken(params) {
468
+ const { grant_type, code, refresh_token } = params;
469
+ if (grant_type === "authorization_code") {
470
+ if (!code) {
471
+ throw new Error("Missing code parameter");
472
+ }
473
+ const pending = pendingRequests.get(code);
474
+ if (!pending || !pending._tokens) {
475
+ throw new Error("Invalid or expired code");
476
+ }
477
+ pendingRequests.delete(code);
478
+ return pending._tokens;
479
+ }
480
+ if (grant_type === "refresh_token") {
481
+ if (!refresh_token) {
482
+ throw new Error("Missing refresh_token parameter");
483
+ }
484
+ throw new Error("Refresh token grant not implemented - use external provider directly");
485
+ }
486
+ throw new Error(`Unsupported grant_type: ${grant_type}`);
487
+ }
488
+ /**
489
+ * Express/Connect middleware factory
490
+ */
491
+ createMiddleware() {
492
+ return {
493
+ authorize: /* @__PURE__ */ __name((req, res) => {
494
+ try {
495
+ const url = this.handleAuthorize(req.query);
496
+ res.redirect(url);
497
+ } catch (error) {
498
+ res.status(400).json({
499
+ error: error.message
500
+ });
501
+ }
502
+ }, "authorize"),
503
+ callback: /* @__PURE__ */ __name(async (req, res) => {
504
+ try {
505
+ const result = await this.handleCallback(req.query);
506
+ res.redirect(result.redirectUri);
507
+ } catch (error) {
508
+ res.status(400).json({
509
+ error: error.message
510
+ });
511
+ }
512
+ }, "callback"),
513
+ token: /* @__PURE__ */ __name(async (req, res) => {
514
+ try {
515
+ const tokens = await this.handleToken(req.body);
516
+ res.json(tokens);
517
+ } catch (error) {
518
+ res.status(400).json({
519
+ error: error.message
520
+ });
521
+ }
522
+ }, "token")
523
+ };
524
+ }
525
+ };
526
+ // Annotate the CommonJS export names for ESM import in node:
527
+ 0 && (module.exports = {
528
+ OAuthProxy,
529
+ azureProvider,
530
+ customProvider,
531
+ discordProvider,
532
+ githubProvider,
533
+ gitlabProvider,
534
+ googleProvider,
535
+ slackProvider
536
+ });