@syncello/auth 2.5.1 → 3.1.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/README.md +1 -6
- package/dist/index.cjs +411 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1348 -3
- package/dist/index.d.ts +1348 -3
- package/dist/index.js +411 -40
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -18,12 +18,7 @@ Authentication library for Hono + Drizzle + Cloudflare Workers.
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install @syncello/auth
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Configure `.npmrc`:
|
|
25
|
-
```
|
|
26
|
-
@syncello:registry=https://npm.pkg.github.com
|
|
21
|
+
npm install @syncello/auth
|
|
27
22
|
```
|
|
28
23
|
|
|
29
24
|
## Quick Start
|
package/dist/index.cjs
CHANGED
|
@@ -898,16 +898,16 @@ function createDeviceToken() {
|
|
|
898
898
|
};
|
|
899
899
|
}
|
|
900
900
|
async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
|
|
901
|
-
const { eq:
|
|
901
|
+
const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
|
|
902
902
|
const [device] = await db.select().from(userTrustedDevicesTable).where(
|
|
903
903
|
and14(
|
|
904
|
-
|
|
905
|
-
|
|
904
|
+
eq33(userTrustedDevicesTable.userId, userId),
|
|
905
|
+
eq33(userTrustedDevicesTable.tokenHash, tokenHash),
|
|
906
906
|
gt6(userTrustedDevicesTable.expiresAt, Date.now())
|
|
907
907
|
)
|
|
908
908
|
).limit(1);
|
|
909
909
|
if (device) {
|
|
910
|
-
await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(
|
|
910
|
+
await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
|
|
911
911
|
return true;
|
|
912
912
|
}
|
|
913
913
|
return false;
|
|
@@ -1829,7 +1829,7 @@ var requireVerifiedEmail = (0, import_factory5.createMiddleware)(
|
|
|
1829
1829
|
);
|
|
1830
1830
|
|
|
1831
1831
|
// src/routes/index.ts
|
|
1832
|
-
var
|
|
1832
|
+
var import_zod_openapi42 = require("@hono/zod-openapi");
|
|
1833
1833
|
|
|
1834
1834
|
// src/routes/signup.ts
|
|
1835
1835
|
var import_zod_openapi2 = require("@hono/zod-openapi");
|
|
@@ -3207,19 +3207,19 @@ var signupRequestSchema = import_zod_openapi.z.object({
|
|
|
3207
3207
|
var loginRequestSchema = import_zod_openapi.z.object({
|
|
3208
3208
|
email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
|
|
3209
3209
|
password: import_zod_openapi.z.string().min(1).openapi({ example: "SecurePass123!" }),
|
|
3210
|
-
turnstileToken: import_zod_openapi.z.string().
|
|
3210
|
+
turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
3211
3211
|
}).openapi("LoginRequest");
|
|
3212
3212
|
var verifyEmailRequestSchema = import_zod_openapi.z.object({
|
|
3213
3213
|
token: import_zod_openapi.z.string().min(1).openapi({ example: "verification-token-abc123" })
|
|
3214
3214
|
}).openapi("VerifyEmailRequest");
|
|
3215
3215
|
var forgotPasswordRequestSchema = import_zod_openapi.z.object({
|
|
3216
3216
|
email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
|
|
3217
|
-
turnstileToken: import_zod_openapi.z.string().
|
|
3217
|
+
turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
3218
3218
|
}).openapi("ForgotPasswordRequest");
|
|
3219
3219
|
var resetPasswordRequestSchema = import_zod_openapi.z.object({
|
|
3220
3220
|
token: import_zod_openapi.z.string().min(1).openapi({ example: "reset-token-xyz789" }),
|
|
3221
3221
|
password: import_zod_openapi.z.string().min(8).openapi({ example: "NewSecurePass456!" }),
|
|
3222
|
-
turnstileToken: import_zod_openapi.z.string().
|
|
3222
|
+
turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
3223
3223
|
}).openapi("ResetPasswordRequest");
|
|
3224
3224
|
var changePasswordRequestSchema = import_zod_openapi.z.object({
|
|
3225
3225
|
currentPassword: import_zod_openapi.z.string().min(1).openapi({ example: "OldPassword123!" }),
|
|
@@ -5498,11 +5498,8 @@ var challengeResendHandler = async (c) => {
|
|
|
5498
5498
|
var twoFa = new import_zod_openapi36.OpenAPIHono();
|
|
5499
5499
|
twoFa.use("*", csrf);
|
|
5500
5500
|
twoFa.use("/status", requireAuth);
|
|
5501
|
-
twoFa.openapi(statusRoute, statusHandler);
|
|
5502
5501
|
twoFa.use("/totp/setup", requireAuth);
|
|
5503
|
-
twoFa.openapi(totpSetupRoute, totpSetupHandler);
|
|
5504
5502
|
twoFa.use("/totp/verify", requireAuth);
|
|
5505
|
-
twoFa.openapi(totpVerifyRoute, totpVerifyHandler);
|
|
5506
5503
|
twoFa.use(
|
|
5507
5504
|
"/totp/disable",
|
|
5508
5505
|
requireAuth,
|
|
@@ -5514,11 +5511,8 @@ twoFa.use(
|
|
|
5514
5511
|
// 5 attempts per 15 minutes
|
|
5515
5512
|
})
|
|
5516
5513
|
);
|
|
5517
|
-
twoFa.openapi(totpDisableRoute, totpDisableHandler);
|
|
5518
5514
|
twoFa.use("/email/setup", requireAuth);
|
|
5519
|
-
twoFa.openapi(emailSetupRoute, emailSetupHandler);
|
|
5520
5515
|
twoFa.use("/email/verify", requireAuth);
|
|
5521
|
-
twoFa.openapi(emailVerifyRoute, emailVerifyHandler);
|
|
5522
5516
|
twoFa.use(
|
|
5523
5517
|
"/email/send-code",
|
|
5524
5518
|
requireAuth,
|
|
@@ -5530,7 +5524,6 @@ twoFa.use(
|
|
|
5530
5524
|
// 3 per 5 minutes
|
|
5531
5525
|
})
|
|
5532
5526
|
);
|
|
5533
|
-
twoFa.openapi(emailSendCodeRoute, emailSendCodeHandler);
|
|
5534
5527
|
twoFa.use(
|
|
5535
5528
|
"/email/disable",
|
|
5536
5529
|
requireAuth,
|
|
@@ -5542,11 +5535,8 @@ twoFa.use(
|
|
|
5542
5535
|
// 5 attempts per 15 minutes
|
|
5543
5536
|
})
|
|
5544
5537
|
);
|
|
5545
|
-
twoFa.openapi(emailDisableRoute, emailDisableHandler);
|
|
5546
5538
|
twoFa.use("/trusted-devices", requireAuth);
|
|
5547
|
-
twoFa.openapi(trustedDevicesGetRoute, trustedDevicesGetHandler);
|
|
5548
5539
|
twoFa.use("/trusted-devices/:id", requireAuth);
|
|
5549
|
-
twoFa.openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler);
|
|
5550
5540
|
twoFa.use(
|
|
5551
5541
|
"/backup-codes/regenerate",
|
|
5552
5542
|
requireAuth,
|
|
@@ -5558,7 +5548,6 @@ twoFa.use(
|
|
|
5558
5548
|
// 3 per hour per user
|
|
5559
5549
|
})
|
|
5560
5550
|
);
|
|
5561
|
-
twoFa.openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler);
|
|
5562
5551
|
twoFa.use(
|
|
5563
5552
|
"/challenge",
|
|
5564
5553
|
rateLimit({
|
|
@@ -5569,7 +5558,6 @@ twoFa.use(
|
|
|
5569
5558
|
// 10 per 5 min per IP
|
|
5570
5559
|
})
|
|
5571
5560
|
);
|
|
5572
|
-
twoFa.openapi(challengeRoute, challengeHandler);
|
|
5573
5561
|
twoFa.use(
|
|
5574
5562
|
"/challenge/resend",
|
|
5575
5563
|
rateLimit({
|
|
@@ -5580,40 +5568,423 @@ twoFa.use(
|
|
|
5580
5568
|
// 3 per 5 min per IP
|
|
5581
5569
|
})
|
|
5582
5570
|
);
|
|
5583
|
-
twoFa.openapi(challengeResendRoute, challengeResendHandler);
|
|
5584
|
-
var fa_default =
|
|
5571
|
+
var twoFaRoutes = twoFa.openapi(statusRoute, statusHandler).openapi(totpSetupRoute, totpSetupHandler).openapi(totpVerifyRoute, totpVerifyHandler).openapi(totpDisableRoute, totpDisableHandler).openapi(emailSetupRoute, emailSetupHandler).openapi(emailVerifyRoute, emailVerifyHandler).openapi(emailSendCodeRoute, emailSendCodeHandler).openapi(emailDisableRoute, emailDisableHandler).openapi(trustedDevicesGetRoute, trustedDevicesGetHandler).openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler).openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler).openapi(challengeRoute, challengeHandler).openapi(challengeResendRoute, challengeResendHandler);
|
|
5572
|
+
var fa_default = twoFaRoutes;
|
|
5573
|
+
|
|
5574
|
+
// src/routes/oauth/index.ts
|
|
5575
|
+
var import_zod_openapi41 = require("@hono/zod-openapi");
|
|
5576
|
+
|
|
5577
|
+
// src/routes/oauth/authorize.ts
|
|
5578
|
+
var import_zod_openapi38 = require("@hono/zod-openapi");
|
|
5579
|
+
|
|
5580
|
+
// src/routes/oauth/providers.ts
|
|
5581
|
+
var PROVIDERS = {
|
|
5582
|
+
google: {
|
|
5583
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
5584
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
5585
|
+
userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
5586
|
+
scopes: ["email"],
|
|
5587
|
+
emailExtractor: (data) => data.email || null,
|
|
5588
|
+
userIdExtractor: (data) => data.id || null
|
|
5589
|
+
},
|
|
5590
|
+
microsoft: {
|
|
5591
|
+
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
5592
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
5593
|
+
userinfoUrl: "https://graph.microsoft.com/v1.0/me",
|
|
5594
|
+
scopes: ["openid", "email", "User.Read"],
|
|
5595
|
+
emailExtractor: (data) => data.mail || data.userPrincipalName || null,
|
|
5596
|
+
userIdExtractor: (data) => data.id || null
|
|
5597
|
+
}
|
|
5598
|
+
};
|
|
5599
|
+
function getProviderCredentials(provider, env) {
|
|
5600
|
+
if (provider === "google" && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
|
5601
|
+
return { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET };
|
|
5602
|
+
}
|
|
5603
|
+
if (provider === "microsoft" && env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) {
|
|
5604
|
+
return { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET };
|
|
5605
|
+
}
|
|
5606
|
+
return null;
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
// src/routes/oauth/schemas.ts
|
|
5610
|
+
var import_zod_openapi37 = require("@hono/zod-openapi");
|
|
5611
|
+
var oauthProviderParamSchema = import_zod_openapi37.z.object({
|
|
5612
|
+
provider: import_zod_openapi37.z.enum(["google", "microsoft"]).openapi({ example: "google" })
|
|
5613
|
+
});
|
|
5614
|
+
var oauthAuthorizeQuerySchema = import_zod_openapi37.z.object({
|
|
5615
|
+
redirect: import_zod_openapi37.z.string().optional().openapi({ example: "/dashboard" }),
|
|
5616
|
+
invitationToken: import_zod_openapi37.z.string().optional().openapi({ example: "token123" }),
|
|
5617
|
+
code_challenge: import_zod_openapi37.z.string().min(43).max(128).optional().openapi({ example: "E9Mrozoa2owUednMY..." }),
|
|
5618
|
+
code_challenge_method: import_zod_openapi37.z.enum(["S256"]).optional().openapi({ example: "S256" }),
|
|
5619
|
+
redirect_uri: import_zod_openapi37.z.string().url().optional().openapi({ example: "myapp://oauth-callback" })
|
|
5620
|
+
}).openapi("OAuthAuthorizeQuery");
|
|
5621
|
+
var oauthCallbackQuerySchema = import_zod_openapi37.z.object({
|
|
5622
|
+
code: import_zod_openapi37.z.string().optional(),
|
|
5623
|
+
state: import_zod_openapi37.z.string().optional(),
|
|
5624
|
+
error: import_zod_openapi37.z.string().optional()
|
|
5625
|
+
});
|
|
5626
|
+
var oauthCompleteRequestSchema = import_zod_openapi37.z.object({
|
|
5627
|
+
code: import_zod_openapi37.z.string().uuid("Invalid code format").openapi({ example: "a1b2c3d4-..." })
|
|
5628
|
+
}).openapi("OAuthCompleteRequest");
|
|
5629
|
+
var oauthCompleteResponseSchema = import_zod_openapi37.z.object({
|
|
5630
|
+
message: import_zod_openapi37.z.string().openapi({ example: "Login successful" }),
|
|
5631
|
+
redirect: import_zod_openapi37.z.string().openapi({ example: "/" }),
|
|
5632
|
+
user: import_zod_openapi37.z.object({
|
|
5633
|
+
id: import_zod_openapi37.z.string().uuid(),
|
|
5634
|
+
email: import_zod_openapi37.z.string().email(),
|
|
5635
|
+
emailVerified: import_zod_openapi37.z.boolean()
|
|
5636
|
+
})
|
|
5637
|
+
}).openapi("OAuthCompleteResponse");
|
|
5638
|
+
var errorResponseSchema4 = import_zod_openapi37.z.object({
|
|
5639
|
+
type: import_zod_openapi37.z.string().url(),
|
|
5640
|
+
title: import_zod_openapi37.z.string(),
|
|
5641
|
+
status: import_zod_openapi37.z.number().int(),
|
|
5642
|
+
detail: import_zod_openapi37.z.string().optional()
|
|
5643
|
+
}).openapi("ErrorResponse");
|
|
5644
|
+
|
|
5645
|
+
// src/routes/oauth/authorize.ts
|
|
5646
|
+
var authorizeRoute = (0, import_zod_openapi38.createRoute)({
|
|
5647
|
+
method: "get",
|
|
5648
|
+
path: "/{provider}/authorize",
|
|
5649
|
+
tags: ["OAuth"],
|
|
5650
|
+
summary: "Start OAuth login flow",
|
|
5651
|
+
description: "Generates a CSRF state token and redirects the user to the OAuth provider.",
|
|
5652
|
+
request: {
|
|
5653
|
+
params: oauthProviderParamSchema,
|
|
5654
|
+
query: oauthAuthorizeQuerySchema
|
|
5655
|
+
},
|
|
5656
|
+
responses: {
|
|
5657
|
+
302: { description: "Redirect to OAuth provider or error page" }
|
|
5658
|
+
}
|
|
5659
|
+
});
|
|
5660
|
+
var authorizeHandler = async (c) => {
|
|
5661
|
+
const { provider } = c.req.valid("param");
|
|
5662
|
+
const {
|
|
5663
|
+
redirect: loginRedirect,
|
|
5664
|
+
invitationToken,
|
|
5665
|
+
code_challenge,
|
|
5666
|
+
code_challenge_method,
|
|
5667
|
+
redirect_uri: mobileRedirectUri
|
|
5668
|
+
} = c.req.valid("query");
|
|
5669
|
+
const providerConfig = PROVIDERS[provider];
|
|
5670
|
+
if (!providerConfig) {
|
|
5671
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_provider`);
|
|
5672
|
+
}
|
|
5673
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5674
|
+
if (!credentials) {
|
|
5675
|
+
logError(new Error(`OAuth credentials not configured for ${provider}`), {
|
|
5676
|
+
context: "oauth_authorize",
|
|
5677
|
+
provider
|
|
5678
|
+
});
|
|
5679
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_not_configured`);
|
|
5680
|
+
}
|
|
5681
|
+
if (code_challenge && !code_challenge_method || !code_challenge && code_challenge_method) {
|
|
5682
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_pkce`);
|
|
5683
|
+
}
|
|
5684
|
+
const state = crypto.randomUUID();
|
|
5685
|
+
await c.env.OAUTH_STATES.put(
|
|
5686
|
+
`oauth:state:${state}`,
|
|
5687
|
+
JSON.stringify({
|
|
5688
|
+
provider,
|
|
5689
|
+
invitationToken: invitationToken || null,
|
|
5690
|
+
redirect: loginRedirect || null,
|
|
5691
|
+
codeChallenge: code_challenge || null,
|
|
5692
|
+
codeChallengeMethod: code_challenge_method || null,
|
|
5693
|
+
mobileRedirectUri: mobileRedirectUri || null
|
|
5694
|
+
}),
|
|
5695
|
+
{ expirationTtl: 600 }
|
|
5696
|
+
// 10 minutes
|
|
5697
|
+
);
|
|
5698
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5699
|
+
const authUrl = new URL(providerConfig.authorizeUrl);
|
|
5700
|
+
authUrl.searchParams.set("client_id", credentials.clientId);
|
|
5701
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
5702
|
+
authUrl.searchParams.set("response_type", "code");
|
|
5703
|
+
if (providerConfig.scopes.length > 0) {
|
|
5704
|
+
authUrl.searchParams.set("scope", providerConfig.scopes.join(" "));
|
|
5705
|
+
}
|
|
5706
|
+
authUrl.searchParams.set("state", state);
|
|
5707
|
+
if (provider === "google") {
|
|
5708
|
+
authUrl.searchParams.set("prompt", "select_account");
|
|
5709
|
+
}
|
|
5710
|
+
logger_default.info("OAuth authorize initiated", { provider, hasPkce: !!code_challenge });
|
|
5711
|
+
return c.redirect(authUrl.toString());
|
|
5712
|
+
};
|
|
5713
|
+
|
|
5714
|
+
// src/routes/oauth/callback.ts
|
|
5715
|
+
var import_zod_openapi39 = require("@hono/zod-openapi");
|
|
5716
|
+
var import_drizzle_orm32 = require("drizzle-orm");
|
|
5717
|
+
var callbackRoute = (0, import_zod_openapi39.createRoute)({
|
|
5718
|
+
method: "get",
|
|
5719
|
+
path: "/{provider}/callback",
|
|
5720
|
+
tags: ["OAuth"],
|
|
5721
|
+
summary: "OAuth provider callback",
|
|
5722
|
+
description: "Handles the redirect from an OAuth provider. Validates CSRF state, exchanges the code for a token, and either starts a login or signup flow.",
|
|
5723
|
+
request: {
|
|
5724
|
+
params: oauthProviderParamSchema,
|
|
5725
|
+
query: oauthCallbackQuerySchema
|
|
5726
|
+
},
|
|
5727
|
+
responses: {
|
|
5728
|
+
302: { description: "Redirect to app with login code or signup token" }
|
|
5729
|
+
}
|
|
5730
|
+
});
|
|
5731
|
+
var callbackHandler = async (c) => {
|
|
5732
|
+
const { provider } = c.req.valid("param");
|
|
5733
|
+
const { code, state, error } = c.req.valid("query");
|
|
5734
|
+
const appUrl = c.env.APP_URL;
|
|
5735
|
+
if (error) {
|
|
5736
|
+
logger_default.info("OAuth provider returned error", { provider, error });
|
|
5737
|
+
const errorType = error === "access_denied" || error === "consent_required" ? "oauth_cancelled" : "oauth_failed";
|
|
5738
|
+
return c.redirect(`${appUrl}/login?error=${errorType}`);
|
|
5739
|
+
}
|
|
5740
|
+
if (!code || !state) {
|
|
5741
|
+
logger_default.warn("Missing code or state in OAuth callback", { provider });
|
|
5742
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5743
|
+
}
|
|
5744
|
+
const storedState = await c.env.OAUTH_STATES.get(`oauth:state:${state}`);
|
|
5745
|
+
if (!storedState) {
|
|
5746
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5747
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5748
|
+
}
|
|
5749
|
+
let stateData;
|
|
5750
|
+
try {
|
|
5751
|
+
stateData = JSON.parse(storedState);
|
|
5752
|
+
} catch (err) {
|
|
5753
|
+
logError(err, { context: "oauth_state_parse", provider });
|
|
5754
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5755
|
+
}
|
|
5756
|
+
if (stateData.provider !== provider) {
|
|
5757
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5758
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5759
|
+
}
|
|
5760
|
+
await c.env.OAUTH_STATES.delete(`oauth:state:${state}`);
|
|
5761
|
+
const {
|
|
5762
|
+
provider: _stateProvider,
|
|
5763
|
+
invitationToken,
|
|
5764
|
+
redirect: loginRedirect,
|
|
5765
|
+
codeChallenge,
|
|
5766
|
+
codeChallengeMethod,
|
|
5767
|
+
mobileRedirectUri
|
|
5768
|
+
} = stateData;
|
|
5769
|
+
const codeVerifier = c.req.query("code_verifier");
|
|
5770
|
+
if (codeChallenge && codeChallengeMethod === "S256") {
|
|
5771
|
+
if (!codeVerifier) {
|
|
5772
|
+
logger_default.warn("OAuth PKCE: missing code_verifier", { provider });
|
|
5773
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_missing`);
|
|
5774
|
+
}
|
|
5775
|
+
const pkceValid = await verifyCodeChallenge(codeVerifier, codeChallenge);
|
|
5776
|
+
if (!pkceValid) {
|
|
5777
|
+
logger_default.warn("OAuth PKCE: invalid code_verifier", { provider });
|
|
5778
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_invalid`);
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
const providerConfig = PROVIDERS[provider];
|
|
5782
|
+
if (!providerConfig) {
|
|
5783
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5784
|
+
}
|
|
5785
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5786
|
+
if (!credentials) {
|
|
5787
|
+
return c.redirect(`${appUrl}/login?error=oauth_not_configured`);
|
|
5788
|
+
}
|
|
5789
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5790
|
+
try {
|
|
5791
|
+
const tokenResponse = await fetch(providerConfig.tokenUrl, {
|
|
5792
|
+
method: "POST",
|
|
5793
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
5794
|
+
body: new URLSearchParams({
|
|
5795
|
+
grant_type: "authorization_code",
|
|
5796
|
+
code,
|
|
5797
|
+
redirect_uri: redirectUri,
|
|
5798
|
+
client_id: credentials.clientId,
|
|
5799
|
+
client_secret: credentials.clientSecret
|
|
5800
|
+
}),
|
|
5801
|
+
signal: AbortSignal.timeout(1e4)
|
|
5802
|
+
});
|
|
5803
|
+
if (!tokenResponse.ok) {
|
|
5804
|
+
const body = await tokenResponse.text();
|
|
5805
|
+
logError(new Error(`OAuth token exchange failed: ${tokenResponse.status} ${body}`), {
|
|
5806
|
+
context: "oauth_token_exchange",
|
|
5807
|
+
provider
|
|
5808
|
+
});
|
|
5809
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5810
|
+
}
|
|
5811
|
+
const tokenData = await tokenResponse.json();
|
|
5812
|
+
if (!tokenData.access_token) {
|
|
5813
|
+
logError(new Error("No access_token in OAuth response"), {
|
|
5814
|
+
context: "oauth_token_exchange",
|
|
5815
|
+
provider
|
|
5816
|
+
});
|
|
5817
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5818
|
+
}
|
|
5819
|
+
const userinfoResponse = await fetch(providerConfig.userinfoUrl, {
|
|
5820
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
|
5821
|
+
signal: AbortSignal.timeout(1e4)
|
|
5822
|
+
});
|
|
5823
|
+
if (!userinfoResponse.ok) {
|
|
5824
|
+
logError(new Error(`OAuth userinfo failed: ${userinfoResponse.status}`), {
|
|
5825
|
+
context: "oauth_userinfo",
|
|
5826
|
+
provider
|
|
5827
|
+
});
|
|
5828
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5829
|
+
}
|
|
5830
|
+
const userInfo = await userinfoResponse.json();
|
|
5831
|
+
const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
|
|
5832
|
+
const providerUserId = providerConfig.userIdExtractor(userInfo);
|
|
5833
|
+
if (!email) {
|
|
5834
|
+
logger_default.warn("No email returned from OAuth provider", { provider });
|
|
5835
|
+
return c.redirect(`${appUrl}/login?error=oauth_no_email`);
|
|
5836
|
+
}
|
|
5837
|
+
if (!providerUserId) {
|
|
5838
|
+
logger_default.warn("No user ID returned from OAuth provider", { provider });
|
|
5839
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5840
|
+
}
|
|
5841
|
+
const { db, schema } = getAuthContext(c);
|
|
5842
|
+
const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.email, email)).limit(1);
|
|
5843
|
+
if (existingUser) {
|
|
5844
|
+
const loginCode = crypto.randomUUID();
|
|
5845
|
+
await c.env.OAUTH_STATES.put(
|
|
5846
|
+
`oauth:login:${loginCode}`,
|
|
5847
|
+
JSON.stringify({
|
|
5848
|
+
userId: existingUser.id,
|
|
5849
|
+
email: existingUser.email,
|
|
5850
|
+
provider,
|
|
5851
|
+
providerUserId,
|
|
5852
|
+
redirect: loginRedirect,
|
|
5853
|
+
invitationToken
|
|
5854
|
+
}),
|
|
5855
|
+
{ expirationTtl: 60 }
|
|
5856
|
+
);
|
|
5857
|
+
logSecurityEvent("oauth_login_initiated", "low", {
|
|
5858
|
+
userId: existingUser.id,
|
|
5859
|
+
email: existingUser.email,
|
|
5860
|
+
provider
|
|
5861
|
+
});
|
|
5862
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5863
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5864
|
+
mobileUrl.searchParams.set("code", loginCode);
|
|
5865
|
+
mobileUrl.searchParams.set("action", "login");
|
|
5866
|
+
return c.redirect(mobileUrl.toString());
|
|
5867
|
+
}
|
|
5868
|
+
return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
|
|
5869
|
+
} else {
|
|
5870
|
+
const signupToken = crypto.randomUUID();
|
|
5871
|
+
await c.env.OAUTH_STATES.put(
|
|
5872
|
+
`oauth:signup:${signupToken}`,
|
|
5873
|
+
JSON.stringify({ email, provider, providerUserId, invitationToken }),
|
|
5874
|
+
{ expirationTtl: 600 }
|
|
5875
|
+
);
|
|
5876
|
+
logger_default.info("OAuth signup initiated", { provider, email });
|
|
5877
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5878
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5879
|
+
mobileUrl.searchParams.set("oauth_token", signupToken);
|
|
5880
|
+
mobileUrl.searchParams.set("email", email);
|
|
5881
|
+
if (invitationToken) {
|
|
5882
|
+
mobileUrl.searchParams.set("invitation", invitationToken);
|
|
5883
|
+
}
|
|
5884
|
+
return c.redirect(mobileUrl.toString());
|
|
5885
|
+
}
|
|
5886
|
+
let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
|
|
5887
|
+
if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
|
|
5888
|
+
return c.redirect(signupUrl);
|
|
5889
|
+
}
|
|
5890
|
+
} catch (err) {
|
|
5891
|
+
logError(err, { context: "oauth_callback", provider });
|
|
5892
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5893
|
+
}
|
|
5894
|
+
};
|
|
5895
|
+
|
|
5896
|
+
// src/routes/oauth/complete.ts
|
|
5897
|
+
var import_zod_openapi40 = require("@hono/zod-openapi");
|
|
5898
|
+
var import_drizzle_orm33 = require("drizzle-orm");
|
|
5899
|
+
var completeRoute = (0, import_zod_openapi40.createRoute)({
|
|
5900
|
+
method: "post",
|
|
5901
|
+
path: "/complete",
|
|
5902
|
+
tags: ["OAuth"],
|
|
5903
|
+
summary: "Complete OAuth login",
|
|
5904
|
+
description: "Exchanges a one-time login code (from the OAuth callback) for a session cookie. Called by the /auth/callback frontend page.",
|
|
5905
|
+
request: {
|
|
5906
|
+
body: {
|
|
5907
|
+
content: { "application/json": { schema: oauthCompleteRequestSchema } },
|
|
5908
|
+
required: true
|
|
5909
|
+
}
|
|
5910
|
+
},
|
|
5911
|
+
responses: {
|
|
5912
|
+
200: {
|
|
5913
|
+
description: "Session created successfully",
|
|
5914
|
+
content: { "application/json": { schema: oauthCompleteResponseSchema } }
|
|
5915
|
+
},
|
|
5916
|
+
401: {
|
|
5917
|
+
description: "Invalid or expired login code",
|
|
5918
|
+
content: { "application/json": { schema: errorResponseSchema4 } }
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
});
|
|
5922
|
+
var completeMiddleware = [
|
|
5923
|
+
rateLimit({
|
|
5924
|
+
identifier: (c) => c.req.header("cf-connecting-ip") || "unknown",
|
|
5925
|
+
action: "oauth_complete",
|
|
5926
|
+
maxAttempts: 10,
|
|
5927
|
+
windowMs: 9e5
|
|
5928
|
+
// 15 minutes
|
|
5929
|
+
})
|
|
5930
|
+
];
|
|
5931
|
+
var completeHandler = async (c) => {
|
|
5932
|
+
const { code } = c.req.valid("json");
|
|
5933
|
+
const stored = await c.env.OAUTH_STATES.get(`oauth:login:${code}`);
|
|
5934
|
+
if (!stored) {
|
|
5935
|
+
return problems.unauthorized(c, "Invalid or expired login code");
|
|
5936
|
+
}
|
|
5937
|
+
await c.env.OAUTH_STATES.delete(`oauth:login:${code}`);
|
|
5938
|
+
const {
|
|
5939
|
+
userId,
|
|
5940
|
+
email,
|
|
5941
|
+
provider,
|
|
5942
|
+
providerUserId,
|
|
5943
|
+
redirect: storedRedirect
|
|
5944
|
+
} = JSON.parse(stored);
|
|
5945
|
+
const { db, schema } = getAuthContext(c);
|
|
5946
|
+
await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm33.eq)(schema.users.id, userId));
|
|
5947
|
+
await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
|
|
5948
|
+
const fingerprint = await generateFingerprint(c.req.raw);
|
|
5949
|
+
const ipAddress = getClientIp(c.req.raw);
|
|
5950
|
+
const sessionId = await createSession(db, { sessions: schema.sessions }, userId, fingerprint, ipAddress);
|
|
5951
|
+
const redirect = storedRedirect?.startsWith("/") && !storedRedirect.startsWith("//") ? storedRedirect : "/";
|
|
5952
|
+
logger_default.info("OAuth login completed", { userId, provider, sessionId: sessionId.slice(0, 8) });
|
|
5953
|
+
logSecurityEvent("oauth_login_success", "low", {
|
|
5954
|
+
userId,
|
|
5955
|
+
email,
|
|
5956
|
+
provider,
|
|
5957
|
+
ip: c.req.header("cf-connecting-ip")
|
|
5958
|
+
});
|
|
5959
|
+
return c.json(
|
|
5960
|
+
{ message: "Login successful", redirect, user: { id: userId, email, emailVerified: true } },
|
|
5961
|
+
200,
|
|
5962
|
+
{ "Set-Cookie": setSessionCookie(sessionId, c.env) }
|
|
5963
|
+
);
|
|
5964
|
+
};
|
|
5965
|
+
|
|
5966
|
+
// src/routes/oauth/index.ts
|
|
5967
|
+
var oauth = new import_zod_openapi41.OpenAPIHono();
|
|
5968
|
+
oauth.use("/complete", ...completeMiddleware);
|
|
5969
|
+
var oauthRoutes = oauth.openapi(authorizeRoute, authorizeHandler).openapi(callbackRoute, callbackHandler).openapi(completeRoute, completeHandler);
|
|
5970
|
+
var oauth_default = oauthRoutes;
|
|
5585
5971
|
|
|
5586
5972
|
// src/routes/index.ts
|
|
5587
|
-
var auth = new
|
|
5973
|
+
var auth = new import_zod_openapi42.OpenAPIHono();
|
|
5588
5974
|
auth.use("*", csrf);
|
|
5589
5975
|
auth.use("/signup", ...signupMiddleware);
|
|
5590
|
-
auth.openapi(signupRoute, signupHandler);
|
|
5591
5976
|
auth.use("/login", ...loginMiddleware);
|
|
5592
|
-
auth.openapi(loginRoute, loginHandler);
|
|
5593
|
-
auth.openapi(logoutRoute, logoutHandler);
|
|
5594
5977
|
auth.use("/me", ...meMiddleware);
|
|
5595
|
-
auth.openapi(meRoute, meHandler);
|
|
5596
|
-
auth.openapi(verifyEmailRoute, verifyEmailHandler);
|
|
5597
5978
|
auth.use("/forgot-password", ...forgotPasswordMiddleware);
|
|
5598
|
-
auth.openapi(forgotPasswordRoute, forgotPasswordHandler);
|
|
5599
5979
|
auth.use("/reset-password", ...resetPasswordMiddleware);
|
|
5600
|
-
auth.openapi(resetPasswordRoute, resetPasswordHandler);
|
|
5601
5980
|
auth.use("/change-password", ...changePasswordMiddleware);
|
|
5602
|
-
auth.openapi(changePasswordRoute, changePasswordHandler);
|
|
5603
5981
|
auth.use("/heartbeat", ...heartbeatMiddleware);
|
|
5604
|
-
auth.openapi(heartbeatRoute, heartbeatHandler);
|
|
5605
5982
|
auth.use("/change-email", ...changeEmailMiddleware);
|
|
5606
|
-
auth.openapi(changeEmailRoute, changeEmailHandler);
|
|
5607
|
-
auth.openapi(confirmEmailChangeRoute, confirmEmailChangeHandler);
|
|
5608
|
-
auth.openapi(cancelEmailChangeRoute, cancelEmailChangeHandler);
|
|
5609
5983
|
auth.use("/account", ...deleteAccountMiddleware);
|
|
5610
|
-
auth.openapi(deleteAccountRoute, deleteAccountHandler);
|
|
5611
5984
|
auth.use("/refresh", ...refreshMiddleware);
|
|
5612
|
-
auth.openapi(refreshRoute, refreshHandler);
|
|
5613
5985
|
auth.use("/resend-verification", ...resendVerificationMiddleware);
|
|
5614
|
-
auth.openapi(resendVerificationRoute, resendVerificationHandler);
|
|
5615
|
-
|
|
5616
|
-
var routes_default = auth;
|
|
5986
|
+
var authRoutes = auth.openapi(signupRoute, signupHandler).openapi(loginRoute, loginHandler).openapi(logoutRoute, logoutHandler).openapi(meRoute, meHandler).openapi(verifyEmailRoute, verifyEmailHandler).openapi(forgotPasswordRoute, forgotPasswordHandler).openapi(resetPasswordRoute, resetPasswordHandler).openapi(changePasswordRoute, changePasswordHandler).openapi(heartbeatRoute, heartbeatHandler).openapi(changeEmailRoute, changeEmailHandler).openapi(confirmEmailChangeRoute, confirmEmailChangeHandler).openapi(cancelEmailChangeRoute, cancelEmailChangeHandler).openapi(deleteAccountRoute, deleteAccountHandler).openapi(refreshRoute, refreshHandler).openapi(resendVerificationRoute, resendVerificationHandler).route("/2fa", fa_default).route("/oauth", oauth_default);
|
|
5987
|
+
var routes_default = authRoutes;
|
|
5617
5988
|
|
|
5618
5989
|
// src/lib/email/webhook-verifier.ts
|
|
5619
5990
|
var import_svix = require("svix");
|