@_mustachio/openauth 0.6.1 → 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.
- package/dist/esm/issuer.js +95 -1
- package/dist/types/issuer.d.ts +46 -1
- package/dist/types/issuer.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/issuer.ts +177 -2
package/dist/esm/issuer.js
CHANGED
|
@@ -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);
|
package/dist/types/issuer.d.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.
|
|
@@ -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":"AAAA
|
|
1
|
+
{"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../../src/issuer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
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)
|