@_mustachio/openauth 0.6.0 → 0.7.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.
@@ -19,6 +19,10 @@ import { DynamoStorage } from "./storage/dynamo.js";
19
19
  import { MemoryStorage } from "./storage/memory.js";
20
20
  import { cors } from "hono/cors";
21
21
  import { logger } from "hono/logger";
22
+ var TENANT_ID_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
23
+ function isValidTenantId(tenantId) {
24
+ return TENANT_ID_REGEX.test(tenantId);
25
+ }
22
26
  var aws = awsHandle;
23
27
  function issuer(input) {
24
28
  const error = input.error ?? function(err) {
@@ -64,9 +68,16 @@ function issuer(input) {
64
68
  const encryptionKey = lazy(() => allEncryption().then((all) => all[0]));
65
69
  const auth = {
66
70
  async success(ctx, properties, successOpts) {
71
+ const authorization = await getAuthorization(ctx);
72
+ const tenantIdFromPath = ctx.get("tenantId");
73
+ if (authorization.tenantId !== tenantIdFromPath) {
74
+ const url = new URL(authorization.redirect_uri);
75
+ url.searchParams.set("error", "invalid_request");
76
+ url.searchParams.set("error_description", "Tenant ID mismatch");
77
+ return ctx.redirect(url.toString());
78
+ }
67
79
  return await input.success({
68
80
  async subject(type, properties2, subjectOpts) {
69
- const authorization = await getAuthorization(ctx);
70
81
  const subject = subjectOpts?.subject ? subjectOpts.subject : await resolveSubject(type, properties2);
71
82
  await successOpts?.invalidate?.(await resolveSubject(type, properties2));
72
83
  if (authorization.response_type === "token") {
@@ -113,6 +124,7 @@ function issuer(input) {
113
124
  }
114
125
  }, {
115
126
  provider: ctx.get("provider"),
127
+ tenantId: authorization.tenantId,
116
128
  ...properties
117
129
  }, ctx.req.raw);
118
130
  },
@@ -478,6 +490,63 @@ function issuer(input) {
478
490
  value.type
479
491
  ])), c.req.raw));
480
492
  });
493
+ app.get("/tenant/:tenantId/authorize", async (c) => {
494
+ const tenantId = c.req.param("tenantId");
495
+ if (!isValidTenantId(tenantId)) {
496
+ return c.json({ error: "invalid_tenant_id" }, 400);
497
+ }
498
+ c.set("tenantId", tenantId);
499
+ const provider = c.req.query("provider");
500
+ const response_type = c.req.query("response_type");
501
+ const redirect_uri = c.req.query("redirect_uri");
502
+ const state = c.req.query("state");
503
+ const client_id = c.req.query("client_id");
504
+ const audience = c.req.query("audience");
505
+ const code_challenge = c.req.query("code_challenge");
506
+ const code_challenge_method = c.req.query("code_challenge_method");
507
+ const authorization = {
508
+ response_type,
509
+ redirect_uri,
510
+ state,
511
+ client_id,
512
+ audience,
513
+ pkce: code_challenge && code_challenge_method ? {
514
+ challenge: code_challenge,
515
+ method: code_challenge_method
516
+ } : undefined,
517
+ tenantId
518
+ };
519
+ if (!redirect_uri) {
520
+ return c.text("Missing redirect_uri", { status: 400 });
521
+ }
522
+ if (!response_type) {
523
+ throw new MissingParameterError("response_type");
524
+ }
525
+ if (!client_id) {
526
+ throw new MissingParameterError("client_id");
527
+ }
528
+ if (input.start) {
529
+ await input.start(c.req.raw);
530
+ }
531
+ if (!await allow()({
532
+ clientID: client_id,
533
+ redirectURI: redirect_uri,
534
+ audience
535
+ }, c.req.raw))
536
+ throw new UnauthorizedClientError(client_id, redirect_uri);
537
+ await auth.set(c, "authorization", 60 * 60 * 24, authorization);
538
+ c.set("authorization", authorization);
539
+ if (provider)
540
+ return c.redirect(`/tenant/${tenantId}/${provider}/authorize`);
541
+ const resolvedProviders = await getProviders(c);
542
+ const providerNames = Object.keys(resolvedProviders);
543
+ if (providerNames.length === 1)
544
+ return c.redirect(`/tenant/${tenantId}/${providerNames[0]}/authorize`);
545
+ return auth.forward(c, await select()(Object.fromEntries(Object.entries(resolvedProviders).map(([key, value]) => [
546
+ key,
547
+ value.type
548
+ ])), c.req.raw));
549
+ });
481
550
  app.get("/userinfo", async (c) => {
482
551
  const header = c.req.header("Authorization");
483
552
  if (!header) {
@@ -512,6 +581,31 @@ function issuer(input) {
512
581
  });
513
582
  });
514
583
  if (typeof input.providers === "function") {
584
+ app.all("/tenant/:tenantId/:provider_name/*", async (c, next) => {
585
+ const tenantId = c.req.param("tenantId");
586
+ if (!isValidTenantId(tenantId)) {
587
+ return c.json({ error: "invalid_tenant_id" }, 400);
588
+ }
589
+ c.set("tenantId", tenantId);
590
+ const name = c.req.param("provider_name");
591
+ const providers = await getProviders(c);
592
+ const value = providers[name];
593
+ if (!value)
594
+ return next();
595
+ const route = new Hono;
596
+ route.use(async (c2, next2) => {
597
+ c2.set("provider", name);
598
+ c2.set("tenantId", tenantId);
599
+ await next2();
600
+ });
601
+ value.init(route, {
602
+ name,
603
+ ...auth
604
+ });
605
+ const sub = new Hono;
606
+ sub.route(`/tenant/${tenantId}/${name}`, route);
607
+ return sub.fetch(c.req.raw);
608
+ });
515
609
  app.all("/:provider_name/*", async (c, next) => {
516
610
  const name = c.req.param("provider_name");
517
611
  const providers = await getProviders(c);
@@ -2,8 +2,7 @@
2
2
  function M2MProvider(config) {
3
3
  return {
4
4
  type: "m2m",
5
- init() {
6
- },
5
+ init() {},
7
6
  async client(input) {
8
7
  const result = await config.verify(input.clientID, input.clientSecret, input.params);
9
8
  if (!result)
@@ -91,6 +91,24 @@
91
91
  * })
92
92
  * ```
93
93
  *
94
+ * #### Multi-tenant routes
95
+ *
96
+ * For environments where subdomains or headers can't identify tenants (e.g., Lambda URLs),
97
+ * use explicit tenant routes: `/tenant/:tenantId/authorize` instead of `/authorize`.
98
+ *
99
+ * ```ts title="issuer.ts"
100
+ * const app = issuer({
101
+ * providers: async (ctx) => {
102
+ * const tenantId = ctx.get("tenantId") as string
103
+ * const config = await db.tenants.findUnique({ where: { id: tenantId } })
104
+ * return { github: GithubProvider({ clientID: config.githubClientId, ... }) }
105
+ * },
106
+ * success: async (ctx, value) => {
107
+ * return ctx.subject("user", { userID: value.email, tenantId: value.tenantId })
108
+ * }
109
+ * })
110
+ * ```
111
+ *
94
112
  * #### Deploy
95
113
  *
96
114
  * Since `issuer` is a Hono app, you can deploy it anywhere Hono supports.
@@ -174,6 +192,7 @@ export interface AuthorizationState {
174
192
  challenge: string;
175
193
  method: "S256";
176
194
  };
195
+ tenantId?: string;
177
196
  }
178
197
  /**
179
198
  * @internal
@@ -263,6 +282,25 @@ export interface IssuerInput<Providers extends Record<string, Provider<any>>, Su
263
282
  * }
264
283
  * }
265
284
  * ```
285
+ *
286
+ * For multi-tenant applications, you can provide a function that returns providers dynamically.
287
+ * When using tenant routes (`/tenant/:tenantId/authorize`), the tenant ID is available via
288
+ * `ctx.get("tenantId")`:
289
+ *
290
+ * ```ts
291
+ * {
292
+ * providers: async (ctx) => {
293
+ * const tenantId = ctx.get("tenantId") as string
294
+ * const config = await db.tenants.findUnique({ where: { id: tenantId } })
295
+ * return {
296
+ * github: GithubProvider({
297
+ * clientID: config.githubClientId,
298
+ * clientSecret: config.githubClientSecret,
299
+ * })
300
+ * }
301
+ * }
302
+ * }
303
+ * ```
266
304
  */
267
305
  providers: Providers | ((ctx: Context) => Promise<Providers>);
268
306
  /**
@@ -363,6 +401,9 @@ export interface IssuerInput<Providers extends Record<string, Provider<any>>, Su
363
401
  *
364
402
  * This is called after the user has been redirected back to your app after the OAuth flow.
365
403
  *
404
+ * The `value` parameter includes `provider` (the provider name) and any provider-specific
405
+ * data. When using tenant routes (`/tenant/:tenantId/authorize`), it also includes `tenantId`.
406
+ *
366
407
  * @example
367
408
  * ```ts
368
409
  * {
@@ -377,7 +418,9 @@ export interface IssuerInput<Providers extends Record<string, Provider<any>>, Su
377
418
  * userID = ... // lookup user or create them
378
419
  * }
379
420
  * return ctx.subject("user", {
380
- * userID
421
+ * userID,
422
+ * // For multi-tenant, store tenantId in subject for refresh access
423
+ * tenantId: value.tenantId,
381
424
  * })
382
425
  * },
383
426
  * // ...
@@ -456,10 +499,12 @@ export interface IssuerInput<Providers extends Record<string, Provider<any>>, Su
456
499
  export declare function issuer<Providers extends Record<string, Provider<any>>, Subjects extends SubjectSchema, Result = {
457
500
  [key in keyof Providers]: Prettify<{
458
501
  provider: key;
502
+ tenantId?: string;
459
503
  } & (Providers[key] extends Provider<infer T> ? T : {})>;
460
504
  }[keyof Providers]>(input: IssuerInput<Providers, Subjects, Result>): import("hono/hono-base").HonoBase<{
461
505
  Variables: {
462
506
  authorization: AuthorizationState;
507
+ tenantId: string;
463
508
  };
464
509
  }, import("hono/types").BlankSchema, "/", "*">;
465
510
  //# sourceMappingURL=issuer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../../src/issuer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../../src/issuer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@_mustachio/openauth",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/the-human-mustachio/openauth.git"
8
+ },
5
9
  "type": "module",
6
10
  "scripts": {
7
11
  "build": "bun run script/build.ts",
package/src/issuer.ts CHANGED
@@ -91,6 +91,24 @@
91
91
  * })
92
92
  * ```
93
93
  *
94
+ * #### Multi-tenant routes
95
+ *
96
+ * For environments where subdomains or headers can't identify tenants (e.g., Lambda URLs),
97
+ * use explicit tenant routes: `/tenant/:tenantId/authorize` instead of `/authorize`.
98
+ *
99
+ * ```ts title="issuer.ts"
100
+ * const app = issuer({
101
+ * providers: async (ctx) => {
102
+ * const tenantId = ctx.get("tenantId") as string
103
+ * const config = await db.tenants.findUnique({ where: { id: tenantId } })
104
+ * return { github: GithubProvider({ clientID: config.githubClientId, ... }) }
105
+ * },
106
+ * success: async (ctx, value) => {
107
+ * return ctx.subject("user", { userID: value.email, tenantId: value.tenantId })
108
+ * }
109
+ * })
110
+ * ```
111
+ *
94
112
  * #### Deploy
95
113
  *
96
114
  * Since `issuer` is a Hono app, you can deploy it anywhere Hono supports.
@@ -182,6 +200,7 @@ export interface AuthorizationState {
182
200
  challenge: string
183
201
  method: "S256"
184
202
  }
203
+ tenantId?: string
185
204
  }
186
205
 
187
206
  /**
@@ -209,6 +228,13 @@ import { MemoryStorage } from "./storage/memory.js"
209
228
  import { cors } from "hono/cors"
210
229
  import { logger } from "hono/logger"
211
230
 
231
+ // Tenant ID validation: alphanumeric, underscore, hyphen, 1-64 chars
232
+ const TENANT_ID_REGEX = /^[a-zA-Z0-9_-]{1,64}$/
233
+
234
+ function isValidTenantId(tenantId: string): boolean {
235
+ return TENANT_ID_REGEX.test(tenantId)
236
+ }
237
+
212
238
  /** @internal */
213
239
  export const aws = awsHandle
214
240
 
@@ -285,6 +311,25 @@ export interface IssuerInput<
285
311
  * }
286
312
  * }
287
313
  * ```
314
+ *
315
+ * For multi-tenant applications, you can provide a function that returns providers dynamically.
316
+ * When using tenant routes (`/tenant/:tenantId/authorize`), the tenant ID is available via
317
+ * `ctx.get("tenantId")`:
318
+ *
319
+ * ```ts
320
+ * {
321
+ * providers: async (ctx) => {
322
+ * const tenantId = ctx.get("tenantId") as string
323
+ * const config = await db.tenants.findUnique({ where: { id: tenantId } })
324
+ * return {
325
+ * github: GithubProvider({
326
+ * clientID: config.githubClientId,
327
+ * clientSecret: config.githubClientSecret,
328
+ * })
329
+ * }
330
+ * }
331
+ * }
332
+ * ```
288
333
  */
289
334
  providers: Providers | ((ctx: Context) => Promise<Providers>)
290
335
  /**
@@ -385,6 +430,9 @@ export interface IssuerInput<
385
430
  *
386
431
  * This is called after the user has been redirected back to your app after the OAuth flow.
387
432
  *
433
+ * The `value` parameter includes `provider` (the provider name) and any provider-specific
434
+ * data. When using tenant routes (`/tenant/:tenantId/authorize`), it also includes `tenantId`.
435
+ *
388
436
  * @example
389
437
  * ```ts
390
438
  * {
@@ -399,7 +447,9 @@ export interface IssuerInput<
399
447
  * userID = ... // lookup user or create them
400
448
  * }
401
449
  * return ctx.subject("user", {
402
- * userID
450
+ * userID,
451
+ * // For multi-tenant, store tenantId in subject for refresh access
452
+ * tenantId: value.tenantId,
403
453
  * })
404
454
  * },
405
455
  * // ...
@@ -491,6 +541,7 @@ export function issuer<
491
541
  [key in keyof Providers]: Prettify<
492
542
  {
493
543
  provider: key
544
+ tenantId?: string
494
545
  } & (Providers[key] extends Provider<infer T> ? T : {})
495
546
  >
496
547
  }[keyof Providers],
@@ -556,10 +607,20 @@ export function issuer<
556
607
 
557
608
  const auth: Omit<ProviderOptions<any>, "name"> = {
558
609
  async success(ctx: Context, properties: any, successOpts) {
610
+ const authorization = await getAuthorization(ctx)
611
+ const tenantIdFromPath = ctx.get("tenantId") as string | undefined
612
+
613
+ // Security: Verify tenantId matches what was stored at authorize time
614
+ if (authorization.tenantId !== tenantIdFromPath) {
615
+ const url = new URL(authorization.redirect_uri)
616
+ url.searchParams.set("error", "invalid_request")
617
+ url.searchParams.set("error_description", "Tenant ID mismatch")
618
+ return ctx.redirect(url.toString())
619
+ }
620
+
559
621
  return await input.success(
560
622
  {
561
623
  async subject(type, properties, subjectOpts) {
562
- const authorization = await getAuthorization(ctx)
563
624
  const subject = subjectOpts?.subject
564
625
  ? subjectOpts.subject
565
626
  : await resolveSubject(type, properties)
@@ -619,6 +680,7 @@ export function issuer<
619
680
  },
620
681
  {
621
682
  provider: ctx.get("provider"),
683
+ tenantId: authorization.tenantId,
622
684
  ...properties,
623
685
  },
624
686
  ctx.req.raw,
@@ -772,6 +834,7 @@ export function issuer<
772
834
  const app = new Hono<{
773
835
  Variables: {
774
836
  authorization: AuthorizationState
837
+ tenantId: string
775
838
  }
776
839
  }>().use(logger())
777
840
 
@@ -1188,6 +1251,87 @@ export function issuer<
1188
1251
  )
1189
1252
  })
1190
1253
 
1254
+ app.get("/tenant/:tenantId/authorize", async (c) => {
1255
+ const tenantId = c.req.param("tenantId")
1256
+
1257
+ // Validate tenantId to prevent path traversal/injection attacks
1258
+ if (!isValidTenantId(tenantId)) {
1259
+ return c.json({ error: "invalid_tenant_id" }, 400)
1260
+ }
1261
+
1262
+ c.set("tenantId", tenantId)
1263
+ const provider = c.req.query("provider")
1264
+ const response_type = c.req.query("response_type")
1265
+ const redirect_uri = c.req.query("redirect_uri")
1266
+ const state = c.req.query("state")
1267
+ const client_id = c.req.query("client_id")
1268
+ const audience = c.req.query("audience")
1269
+ const code_challenge = c.req.query("code_challenge")
1270
+ const code_challenge_method = c.req.query("code_challenge_method")
1271
+ const authorization: AuthorizationState = {
1272
+ response_type,
1273
+ redirect_uri,
1274
+ state,
1275
+ client_id,
1276
+ audience,
1277
+ pkce:
1278
+ code_challenge && code_challenge_method
1279
+ ? {
1280
+ challenge: code_challenge,
1281
+ method: code_challenge_method,
1282
+ }
1283
+ : undefined,
1284
+ tenantId,
1285
+ } as AuthorizationState
1286
+
1287
+ if (!redirect_uri) {
1288
+ return c.text("Missing redirect_uri", { status: 400 })
1289
+ }
1290
+
1291
+ if (!response_type) {
1292
+ throw new MissingParameterError("response_type")
1293
+ }
1294
+
1295
+ if (!client_id) {
1296
+ throw new MissingParameterError("client_id")
1297
+ }
1298
+
1299
+ if (input.start) {
1300
+ await input.start(c.req.raw)
1301
+ }
1302
+
1303
+ if (
1304
+ !(await allow()(
1305
+ {
1306
+ clientID: client_id,
1307
+ redirectURI: redirect_uri,
1308
+ audience,
1309
+ },
1310
+ c.req.raw,
1311
+ ))
1312
+ )
1313
+ throw new UnauthorizedClientError(client_id, redirect_uri)
1314
+ await auth.set(c, "authorization", 60 * 60 * 24, authorization)
1315
+ c.set("authorization", authorization)
1316
+ if (provider) return c.redirect(`/tenant/${tenantId}/${provider}/authorize`)
1317
+ const resolvedProviders = await getProviders(c)
1318
+ const providerNames = Object.keys(resolvedProviders)
1319
+ if (providerNames.length === 1)
1320
+ return c.redirect(`/tenant/${tenantId}/${providerNames[0]}/authorize`)
1321
+ return auth.forward(
1322
+ c,
1323
+ await select()(
1324
+ Object.fromEntries(
1325
+ Object.entries(resolvedProviders).map(([key, value]) => [
1326
+ key,
1327
+ value.type,
1328
+ ]),
1329
+ ),
1330
+ c.req.raw,
1331
+ ),
1332
+ )
1333
+ })
1334
+
1191
1335
  app.get("/userinfo", async (c) => {
1192
1336
  const header = c.req.header("Authorization")
1193
1337
 
@@ -1246,6 +1390,37 @@ export function issuer<
1246
1390
  })
1247
1391
 
1248
1392
  if (typeof input.providers === "function") {
1393
+ // Tenant-specific provider routes (must be before generic /:provider_name/*)
1394
+ app.all("/tenant/:tenantId/:provider_name/*", async (c, next) => {
1395
+ const tenantId = c.req.param("tenantId")
1396
+
1397
+ // Validate tenantId to prevent path traversal/injection attacks
1398
+ if (!isValidTenantId(tenantId)) {
1399
+ return c.json({ error: "invalid_tenant_id" }, 400)
1400
+ }
1401
+
1402
+ c.set("tenantId", tenantId)
1403
+ const name = c.req.param("provider_name")
1404
+ const providers = await getProviders(c)
1405
+ const value = providers[name]
1406
+ if (!value) return next()
1407
+
1408
+ const route = new Hono<any>()
1409
+ route.use(async (c, next) => {
1410
+ c.set("provider", name)
1411
+ c.set("tenantId", tenantId)
1412
+ await next()
1413
+ })
1414
+ value.init(route, {
1415
+ name,
1416
+ ...auth,
1417
+ })
1418
+ const sub = new Hono()
1419
+ sub.route(`/tenant/${tenantId}/${name}`, route)
1420
+ return sub.fetch(c.req.raw)
1421
+ })
1422
+
1423
+ // Generic provider routes (for non-tenant flows)
1249
1424
  app.all("/:provider_name/*", async (c, next) => {
1250
1425
  const name = c.req.param("provider_name")
1251
1426
  const providers = await getProviders(c)