@syncello/auth 2.5.1 → 3.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/README.md +1 -6
- package/dist/index.cjs +546 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1455 -112
- package/dist/index.d.ts +1455 -112
- package/dist/index.js +545 -69
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -507,16 +507,16 @@ function createDeviceToken() {
|
|
|
507
507
|
};
|
|
508
508
|
}
|
|
509
509
|
async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
|
|
510
|
-
const { eq:
|
|
510
|
+
const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
|
|
511
511
|
const [device] = await db.select().from(userTrustedDevicesTable).where(
|
|
512
512
|
and14(
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
eq33(userTrustedDevicesTable.userId, userId),
|
|
514
|
+
eq33(userTrustedDevicesTable.tokenHash, tokenHash),
|
|
515
515
|
gt6(userTrustedDevicesTable.expiresAt, Date.now())
|
|
516
516
|
)
|
|
517
517
|
).limit(1);
|
|
518
518
|
if (device) {
|
|
519
|
-
await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(
|
|
519
|
+
await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
|
|
520
520
|
return true;
|
|
521
521
|
}
|
|
522
522
|
return false;
|
|
@@ -1438,7 +1438,7 @@ var requireVerifiedEmail = createMiddleware4(
|
|
|
1438
1438
|
);
|
|
1439
1439
|
|
|
1440
1440
|
// src/routes/index.ts
|
|
1441
|
-
import { OpenAPIHono as
|
|
1441
|
+
import { OpenAPIHono as OpenAPIHono3 } from "@hono/zod-openapi";
|
|
1442
1442
|
|
|
1443
1443
|
// src/routes/signup.ts
|
|
1444
1444
|
import { createRoute } from "@hono/zod-openapi";
|
|
@@ -2181,13 +2181,92 @@ var ResendAdapter = class {
|
|
|
2181
2181
|
}
|
|
2182
2182
|
};
|
|
2183
2183
|
|
|
2184
|
+
// src/lib/email/adapters/cloudflare-adapter.ts
|
|
2185
|
+
var CloudflareEmailAdapter = class {
|
|
2186
|
+
constructor(binding, options) {
|
|
2187
|
+
this.binding = binding;
|
|
2188
|
+
this.options = options;
|
|
2189
|
+
}
|
|
2190
|
+
binding;
|
|
2191
|
+
options;
|
|
2192
|
+
providerName = "cloudflare";
|
|
2193
|
+
async send(options) {
|
|
2194
|
+
if (this.options.dryRun) {
|
|
2195
|
+
logger_default.info("Cloudflare: Dry-run, email not sent", {
|
|
2196
|
+
provider: this.providerName,
|
|
2197
|
+
to: options.to,
|
|
2198
|
+
from: options.from,
|
|
2199
|
+
subject: options.subject
|
|
2200
|
+
});
|
|
2201
|
+
return { success: true, emailId: "dry-run" };
|
|
2202
|
+
}
|
|
2203
|
+
try {
|
|
2204
|
+
logger_default.info("Cloudflare: Sending email", {
|
|
2205
|
+
provider: this.providerName,
|
|
2206
|
+
to: options.to,
|
|
2207
|
+
subject: options.subject
|
|
2208
|
+
});
|
|
2209
|
+
const result = await this.binding.send({
|
|
2210
|
+
to: options.to,
|
|
2211
|
+
from: options.from,
|
|
2212
|
+
subject: options.subject,
|
|
2213
|
+
html: options.html,
|
|
2214
|
+
text: options.text,
|
|
2215
|
+
headers: options.tags ? Object.fromEntries(
|
|
2216
|
+
Object.entries(options.tags).map(([name, value]) => [`X-Tag-${name}`, value])
|
|
2217
|
+
) : void 0
|
|
2218
|
+
});
|
|
2219
|
+
logger_default.info("Cloudflare: Email sent successfully", {
|
|
2220
|
+
provider: this.providerName,
|
|
2221
|
+
emailId: result.messageId,
|
|
2222
|
+
to: options.to
|
|
2223
|
+
});
|
|
2224
|
+
return { success: true, emailId: result.messageId };
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2227
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
2228
|
+
const errorText = code ? `${code}: ${message}` : message;
|
|
2229
|
+
if (code === "E_RECIPIENT_SUPPRESSED") {
|
|
2230
|
+
logger_default.warn("Cloudflare: Recipient is on the suppression list", {
|
|
2231
|
+
provider: this.providerName,
|
|
2232
|
+
to: options.to
|
|
2233
|
+
});
|
|
2234
|
+
} else {
|
|
2235
|
+
logger_default.error("Cloudflare send error", {
|
|
2236
|
+
provider: this.providerName,
|
|
2237
|
+
error: errorText,
|
|
2238
|
+
to: options.to,
|
|
2239
|
+
subject: options.subject
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
return { success: false, error: errorText };
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
|
|
2184
2247
|
// src/lib/email/adapters/factory.ts
|
|
2185
2248
|
function createEmailAdapter(env) {
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2249
|
+
const provider = env.EMAIL_PROVIDER ?? "resend";
|
|
2250
|
+
logger_default.info("Creating email adapter", { provider });
|
|
2251
|
+
switch (provider) {
|
|
2252
|
+
case "resend":
|
|
2253
|
+
if (!env.RESEND_API_KEY) {
|
|
2254
|
+
throw new Error("RESEND_API_KEY is required");
|
|
2255
|
+
}
|
|
2256
|
+
return new ResendAdapter(env.RESEND_API_KEY);
|
|
2257
|
+
case "cloudflare": {
|
|
2258
|
+
if (!env.EMAIL) {
|
|
2259
|
+
throw new Error(
|
|
2260
|
+
'EMAIL send_email binding is required when EMAIL_PROVIDER=cloudflare - add [[send_email]] name = "EMAIL" to wrangler.toml'
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
const environment = env.ENVIRONMENT?.toLowerCase();
|
|
2264
|
+
const dryRun = environment !== "production" && environment !== "staging";
|
|
2265
|
+
return new CloudflareEmailAdapter(env.EMAIL, { dryRun });
|
|
2266
|
+
}
|
|
2267
|
+
default:
|
|
2268
|
+
throw new Error(`Unknown EMAIL_PROVIDER: ${String(provider)}`);
|
|
2189
2269
|
}
|
|
2190
|
-
return new ResendAdapter(env.RESEND_API_KEY);
|
|
2191
2270
|
}
|
|
2192
2271
|
|
|
2193
2272
|
// src/lib/email/email-service.ts
|
|
@@ -2195,6 +2274,9 @@ var EmailService = class {
|
|
|
2195
2274
|
adapter;
|
|
2196
2275
|
env;
|
|
2197
2276
|
get fromAddress() {
|
|
2277
|
+
if (this.env.EMAIL_FROM) {
|
|
2278
|
+
return this.env.EMAIL_FROM;
|
|
2279
|
+
}
|
|
2198
2280
|
const appName = this.env.APP_NAME ?? "Your App";
|
|
2199
2281
|
const appUrl = this.env.APP_URL ?? "";
|
|
2200
2282
|
let domain = "example.com";
|
|
@@ -2816,19 +2898,19 @@ var signupRequestSchema = z.object({
|
|
|
2816
2898
|
var loginRequestSchema = z.object({
|
|
2817
2899
|
email: z.string().email().openapi({ example: "user@example.com" }),
|
|
2818
2900
|
password: z.string().min(1).openapi({ example: "SecurePass123!" }),
|
|
2819
|
-
turnstileToken: z.string().
|
|
2901
|
+
turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
2820
2902
|
}).openapi("LoginRequest");
|
|
2821
2903
|
var verifyEmailRequestSchema = z.object({
|
|
2822
2904
|
token: z.string().min(1).openapi({ example: "verification-token-abc123" })
|
|
2823
2905
|
}).openapi("VerifyEmailRequest");
|
|
2824
2906
|
var forgotPasswordRequestSchema = z.object({
|
|
2825
2907
|
email: z.string().email().openapi({ example: "user@example.com" }),
|
|
2826
|
-
turnstileToken: z.string().
|
|
2908
|
+
turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
2827
2909
|
}).openapi("ForgotPasswordRequest");
|
|
2828
2910
|
var resetPasswordRequestSchema = z.object({
|
|
2829
2911
|
token: z.string().min(1).openapi({ example: "reset-token-xyz789" }),
|
|
2830
2912
|
password: z.string().min(8).openapi({ example: "NewSecurePass456!" }),
|
|
2831
|
-
turnstileToken: z.string().
|
|
2913
|
+
turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
2832
2914
|
}).openapi("ResetPasswordRequest");
|
|
2833
2915
|
var changePasswordRequestSchema = z.object({
|
|
2834
2916
|
currentPassword: z.string().min(1).openapi({ example: "OldPassword123!" }),
|
|
@@ -3051,7 +3133,8 @@ var signupHandler = async (c) => {
|
|
|
3051
3133
|
verificationUrl,
|
|
3052
3134
|
firstName: newUser.name || void 0
|
|
3053
3135
|
},
|
|
3054
|
-
database
|
|
3136
|
+
database,
|
|
3137
|
+
schema.users
|
|
3055
3138
|
);
|
|
3056
3139
|
const fingerprint = await generateFingerprint(c.req.raw);
|
|
3057
3140
|
const ipAddress = getClientIp(c.req.raw);
|
|
@@ -3525,7 +3608,7 @@ var forgotPasswordRoute = createRoute6({
|
|
|
3525
3608
|
});
|
|
3526
3609
|
var forgotPasswordHandler = async (c) => {
|
|
3527
3610
|
const { email, turnstileToken } = c.req.valid("json");
|
|
3528
|
-
const database = c
|
|
3611
|
+
const { db: database, schema } = getAuthContext(c);
|
|
3529
3612
|
const env = c.env;
|
|
3530
3613
|
const turnstileValid = await verifyTurnstileToken(
|
|
3531
3614
|
turnstileToken,
|
|
@@ -3536,7 +3619,11 @@ var forgotPasswordHandler = async (c) => {
|
|
|
3536
3619
|
if (!turnstileValid) {
|
|
3537
3620
|
return problems.badRequest(c, "Invalid captcha");
|
|
3538
3621
|
}
|
|
3539
|
-
const resetResult = await createPasswordResetToken(
|
|
3622
|
+
const resetResult = await createPasswordResetToken(
|
|
3623
|
+
database,
|
|
3624
|
+
{ users: schema.users, passwordResetTokens: schema.passwordResetTokens },
|
|
3625
|
+
email
|
|
3626
|
+
);
|
|
3540
3627
|
if (!resetResult) {
|
|
3541
3628
|
return c.json({ success: true });
|
|
3542
3629
|
}
|
|
@@ -3548,7 +3635,8 @@ var forgotPasswordHandler = async (c) => {
|
|
|
3548
3635
|
token: resetResult.token,
|
|
3549
3636
|
resetUrl
|
|
3550
3637
|
},
|
|
3551
|
-
database
|
|
3638
|
+
database,
|
|
3639
|
+
schema.users
|
|
3552
3640
|
);
|
|
3553
3641
|
logSecurityEvent("password_reset_requested", "medium", { userId: resetResult.userId });
|
|
3554
3642
|
return c.json({ success: true });
|
|
@@ -3690,7 +3778,7 @@ var changePasswordHandler = async (c) => {
|
|
|
3690
3778
|
const userId = c.get("userId");
|
|
3691
3779
|
if (!userId) return problems.unauthorized(c, "Not authenticated");
|
|
3692
3780
|
const { currentPassword, newPassword } = c.req.valid("json");
|
|
3693
|
-
const database = c
|
|
3781
|
+
const { db: database, schema } = getAuthContext(c);
|
|
3694
3782
|
const env = c.env;
|
|
3695
3783
|
const passwordValidation = validatePassword(newPassword);
|
|
3696
3784
|
if (!passwordValidation.valid) {
|
|
@@ -3707,7 +3795,8 @@ var changePasswordHandler = async (c) => {
|
|
|
3707
3795
|
newPassword,
|
|
3708
3796
|
currentSessionId,
|
|
3709
3797
|
pepper: env.PASSWORD_PEPPER_V1,
|
|
3710
|
-
db: database
|
|
3798
|
+
db: database,
|
|
3799
|
+
tables: { users: schema.users, sessions: schema.sessions }
|
|
3711
3800
|
});
|
|
3712
3801
|
if (!result.success) {
|
|
3713
3802
|
return problems.badRequest(c, result.error || "Failed to change password");
|
|
@@ -3786,7 +3875,7 @@ var changeEmailHandler = async (c) => {
|
|
|
3786
3875
|
const userId = c.get("userId");
|
|
3787
3876
|
if (!userId) return problems.unauthorized(c, "Not authenticated");
|
|
3788
3877
|
const { password, newEmail } = c.req.valid("json");
|
|
3789
|
-
const database = c
|
|
3878
|
+
const { db: database, schema } = getAuthContext(c);
|
|
3790
3879
|
const env = c.env;
|
|
3791
3880
|
const appUrl = env.APP_URL || "http://localhost:5173";
|
|
3792
3881
|
const result = await requestEmailChange({
|
|
@@ -3794,7 +3883,8 @@ var changeEmailHandler = async (c) => {
|
|
|
3794
3883
|
password,
|
|
3795
3884
|
newEmail,
|
|
3796
3885
|
pepper: env.PASSWORD_PEPPER_V1,
|
|
3797
|
-
db: database
|
|
3886
|
+
db: database,
|
|
3887
|
+
tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
|
|
3798
3888
|
});
|
|
3799
3889
|
if (!result.success) {
|
|
3800
3890
|
if (result.error === "Incorrect password") {
|
|
@@ -3806,7 +3896,8 @@ var changeEmailHandler = async (c) => {
|
|
|
3806
3896
|
const confirmUrl = `${appUrl}/confirm-email-change?token=${result.confirmToken}`;
|
|
3807
3897
|
const confirmResult = await emailService.sendEmailChangeConfirmation(
|
|
3808
3898
|
{ newEmail, confirmUrl },
|
|
3809
|
-
database
|
|
3899
|
+
database,
|
|
3900
|
+
schema.users
|
|
3810
3901
|
);
|
|
3811
3902
|
if (!confirmResult.success) {
|
|
3812
3903
|
return problems.badRequest(
|
|
@@ -3817,7 +3908,8 @@ var changeEmailHandler = async (c) => {
|
|
|
3817
3908
|
const cancelUrl = `${appUrl}/cancel-email-change?token=${result.cancelToken}`;
|
|
3818
3909
|
await emailService.sendEmailChangeNotification(
|
|
3819
3910
|
{ oldEmail: result.oldEmail, newEmail, cancelUrl },
|
|
3820
|
-
database
|
|
3911
|
+
database,
|
|
3912
|
+
schema.users
|
|
3821
3913
|
);
|
|
3822
3914
|
logSecurityEvent("email_change_requested", "medium", { userId });
|
|
3823
3915
|
return c.json({ success: true });
|
|
@@ -3858,8 +3950,12 @@ var confirmEmailChangeRoute = createRoute11({
|
|
|
3858
3950
|
});
|
|
3859
3951
|
var confirmEmailChangeHandler = async (c) => {
|
|
3860
3952
|
const { token } = c.req.valid("json");
|
|
3861
|
-
const database = c
|
|
3862
|
-
const result = await confirmEmailChange({
|
|
3953
|
+
const { db: database, schema } = getAuthContext(c);
|
|
3954
|
+
const result = await confirmEmailChange({
|
|
3955
|
+
token,
|
|
3956
|
+
db: database,
|
|
3957
|
+
tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
|
|
3958
|
+
});
|
|
3863
3959
|
if (!result.success) {
|
|
3864
3960
|
return problems.badRequest(c, result.error || "Invalid or expired token");
|
|
3865
3961
|
}
|
|
@@ -3900,8 +3996,12 @@ var cancelEmailChangeRoute = createRoute12({
|
|
|
3900
3996
|
});
|
|
3901
3997
|
var cancelEmailChangeHandler = async (c) => {
|
|
3902
3998
|
const { token } = c.req.valid("json");
|
|
3903
|
-
const database = c
|
|
3904
|
-
const result = await cancelEmailChange({
|
|
3999
|
+
const { db: database, schema } = getAuthContext(c);
|
|
4000
|
+
const result = await cancelEmailChange({
|
|
4001
|
+
token,
|
|
4002
|
+
db: database,
|
|
4003
|
+
tables: { emailChangeTokens: schema.emailChangeTokens }
|
|
4004
|
+
});
|
|
3905
4005
|
if (!result.success) {
|
|
3906
4006
|
return problems.badRequest(c, result.error || "Invalid or expired token");
|
|
3907
4007
|
}
|
|
@@ -4040,7 +4140,7 @@ var refreshHandler = async (c) => {
|
|
|
4040
4140
|
if (!session) {
|
|
4041
4141
|
return problems.unauthorized(c, "Invalid or expired refresh token");
|
|
4042
4142
|
}
|
|
4043
|
-
await deleteSession(db, session.id);
|
|
4143
|
+
await deleteSession(db, { sessions: schema.sessions }, session.id);
|
|
4044
4144
|
const newSessionId = await createSession(
|
|
4045
4145
|
db,
|
|
4046
4146
|
{ sessions: schema.sessions },
|
|
@@ -4131,7 +4231,8 @@ var resendVerificationHandler = async (c) => {
|
|
|
4131
4231
|
verificationUrl,
|
|
4132
4232
|
firstName: user.name || void 0
|
|
4133
4233
|
},
|
|
4134
|
-
db
|
|
4234
|
+
db,
|
|
4235
|
+
schema.users
|
|
4135
4236
|
);
|
|
4136
4237
|
logger_default.info("Verification email resent", { userId: user.id, email: user.email });
|
|
4137
4238
|
return c.json({
|
|
@@ -4457,7 +4558,8 @@ var totpVerifyHandler = async (c) => {
|
|
|
4457
4558
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4458
4559
|
await emailService.send2faEnabledEmail(
|
|
4459
4560
|
{ email: user.email, firstName, method: "totp" },
|
|
4460
|
-
db
|
|
4561
|
+
db,
|
|
4562
|
+
schema.users
|
|
4461
4563
|
);
|
|
4462
4564
|
}
|
|
4463
4565
|
logSecurityEvent("2fa_totp_enrolled", "high", { userId });
|
|
@@ -4514,12 +4616,12 @@ var totpDisableHandler = async (c) => {
|
|
|
4514
4616
|
if (remaining.length === 0) {
|
|
4515
4617
|
await db.delete(schema.userBackupCodes).where(eq22(schema.userBackupCodes.userId, userId));
|
|
4516
4618
|
await db.delete(schema.userTrustedDevices).where(eq22(schema.userTrustedDevices.userId, userId));
|
|
4517
|
-
await invalidateAllUserSessions(db, userId);
|
|
4619
|
+
await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
|
|
4518
4620
|
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq22(schema.users.id, userId)).limit(1);
|
|
4519
4621
|
if (user) {
|
|
4520
4622
|
const emailService = new EmailService(env);
|
|
4521
4623
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4522
|
-
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
|
|
4624
|
+
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
|
|
4523
4625
|
}
|
|
4524
4626
|
logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "totp" });
|
|
4525
4627
|
return c.json({ success: true, sessionInvalidated: true });
|
|
@@ -4575,7 +4677,7 @@ var emailSetupHandler = async (c) => {
|
|
|
4575
4677
|
await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
|
|
4576
4678
|
const emailService = new EmailService(env);
|
|
4577
4679
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4578
|
-
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
|
|
4680
|
+
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
|
|
4579
4681
|
logSecurityEvent("2fa_email_setup_initiated", "low", { userId });
|
|
4580
4682
|
return c.json({ success: true });
|
|
4581
4683
|
};
|
|
@@ -4656,7 +4758,8 @@ var emailVerifyHandler = async (c) => {
|
|
|
4656
4758
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4657
4759
|
await emailService.send2faEnabledEmail(
|
|
4658
4760
|
{ email: user.email, firstName, method: "email" },
|
|
4659
|
-
db
|
|
4761
|
+
db,
|
|
4762
|
+
schema.users
|
|
4660
4763
|
);
|
|
4661
4764
|
}
|
|
4662
4765
|
logSecurityEvent("2fa_email_enrolled", "high", { userId });
|
|
@@ -4710,7 +4813,8 @@ var emailSendCodeHandler = async (c) => {
|
|
|
4710
4813
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4711
4814
|
const emailResult = await emailService.send2faCodeEmail(
|
|
4712
4815
|
{ email: user.email, firstName, code },
|
|
4713
|
-
db
|
|
4816
|
+
db,
|
|
4817
|
+
schema.users
|
|
4714
4818
|
);
|
|
4715
4819
|
if (!emailResult.success) {
|
|
4716
4820
|
return problems.badRequest(
|
|
@@ -4772,12 +4876,12 @@ var emailDisableHandler = async (c) => {
|
|
|
4772
4876
|
if (remaining.length === 0) {
|
|
4773
4877
|
await db.delete(schema.userBackupCodes).where(eq26(schema.userBackupCodes.userId, userId));
|
|
4774
4878
|
await db.delete(schema.userTrustedDevices).where(eq26(schema.userTrustedDevices.userId, userId));
|
|
4775
|
-
await invalidateAllUserSessions(db, userId);
|
|
4879
|
+
await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
|
|
4776
4880
|
const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq26(schema.users.id, userId)).limit(1);
|
|
4777
4881
|
if (user) {
|
|
4778
4882
|
const emailService = new EmailService(env);
|
|
4779
4883
|
const firstName = user.name?.split(" ")[0] || null;
|
|
4780
|
-
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
|
|
4884
|
+
await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
|
|
4781
4885
|
}
|
|
4782
4886
|
logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "email" });
|
|
4783
4887
|
return c.json({ success: true, sessionInvalidated: true });
|
|
@@ -5098,7 +5202,7 @@ var challengeResendHandler = async (c) => {
|
|
|
5098
5202
|
});
|
|
5099
5203
|
const emailService = new EmailService(c.env);
|
|
5100
5204
|
const firstName = user.name?.split(" ")[0] || null;
|
|
5101
|
-
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
|
|
5205
|
+
await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
|
|
5102
5206
|
logger_default.info("Email 2FA code resent", { userId: payload.userId });
|
|
5103
5207
|
return c.json({ success: true });
|
|
5104
5208
|
};
|
|
@@ -5107,11 +5211,8 @@ var challengeResendHandler = async (c) => {
|
|
|
5107
5211
|
var twoFa = new OpenAPIHono();
|
|
5108
5212
|
twoFa.use("*", csrf);
|
|
5109
5213
|
twoFa.use("/status", requireAuth);
|
|
5110
|
-
twoFa.openapi(statusRoute, statusHandler);
|
|
5111
5214
|
twoFa.use("/totp/setup", requireAuth);
|
|
5112
|
-
twoFa.openapi(totpSetupRoute, totpSetupHandler);
|
|
5113
5215
|
twoFa.use("/totp/verify", requireAuth);
|
|
5114
|
-
twoFa.openapi(totpVerifyRoute, totpVerifyHandler);
|
|
5115
5216
|
twoFa.use(
|
|
5116
5217
|
"/totp/disable",
|
|
5117
5218
|
requireAuth,
|
|
@@ -5123,11 +5224,8 @@ twoFa.use(
|
|
|
5123
5224
|
// 5 attempts per 15 minutes
|
|
5124
5225
|
})
|
|
5125
5226
|
);
|
|
5126
|
-
twoFa.openapi(totpDisableRoute, totpDisableHandler);
|
|
5127
5227
|
twoFa.use("/email/setup", requireAuth);
|
|
5128
|
-
twoFa.openapi(emailSetupRoute, emailSetupHandler);
|
|
5129
5228
|
twoFa.use("/email/verify", requireAuth);
|
|
5130
|
-
twoFa.openapi(emailVerifyRoute, emailVerifyHandler);
|
|
5131
5229
|
twoFa.use(
|
|
5132
5230
|
"/email/send-code",
|
|
5133
5231
|
requireAuth,
|
|
@@ -5139,7 +5237,6 @@ twoFa.use(
|
|
|
5139
5237
|
// 3 per 5 minutes
|
|
5140
5238
|
})
|
|
5141
5239
|
);
|
|
5142
|
-
twoFa.openapi(emailSendCodeRoute, emailSendCodeHandler);
|
|
5143
5240
|
twoFa.use(
|
|
5144
5241
|
"/email/disable",
|
|
5145
5242
|
requireAuth,
|
|
@@ -5151,11 +5248,8 @@ twoFa.use(
|
|
|
5151
5248
|
// 5 attempts per 15 minutes
|
|
5152
5249
|
})
|
|
5153
5250
|
);
|
|
5154
|
-
twoFa.openapi(emailDisableRoute, emailDisableHandler);
|
|
5155
5251
|
twoFa.use("/trusted-devices", requireAuth);
|
|
5156
|
-
twoFa.openapi(trustedDevicesGetRoute, trustedDevicesGetHandler);
|
|
5157
5252
|
twoFa.use("/trusted-devices/:id", requireAuth);
|
|
5158
|
-
twoFa.openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler);
|
|
5159
5253
|
twoFa.use(
|
|
5160
5254
|
"/backup-codes/regenerate",
|
|
5161
5255
|
requireAuth,
|
|
@@ -5167,7 +5261,6 @@ twoFa.use(
|
|
|
5167
5261
|
// 3 per hour per user
|
|
5168
5262
|
})
|
|
5169
5263
|
);
|
|
5170
|
-
twoFa.openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler);
|
|
5171
5264
|
twoFa.use(
|
|
5172
5265
|
"/challenge",
|
|
5173
5266
|
rateLimit({
|
|
@@ -5178,7 +5271,6 @@ twoFa.use(
|
|
|
5178
5271
|
// 10 per 5 min per IP
|
|
5179
5272
|
})
|
|
5180
5273
|
);
|
|
5181
|
-
twoFa.openapi(challengeRoute, challengeHandler);
|
|
5182
5274
|
twoFa.use(
|
|
5183
5275
|
"/challenge/resend",
|
|
5184
5276
|
rateLimit({
|
|
@@ -5189,40 +5281,423 @@ twoFa.use(
|
|
|
5189
5281
|
// 3 per 5 min per IP
|
|
5190
5282
|
})
|
|
5191
5283
|
);
|
|
5192
|
-
twoFa.openapi(challengeResendRoute, challengeResendHandler);
|
|
5193
|
-
var fa_default =
|
|
5284
|
+
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);
|
|
5285
|
+
var fa_default = twoFaRoutes;
|
|
5286
|
+
|
|
5287
|
+
// src/routes/oauth/index.ts
|
|
5288
|
+
import { OpenAPIHono as OpenAPIHono2 } from "@hono/zod-openapi";
|
|
5289
|
+
|
|
5290
|
+
// src/routes/oauth/authorize.ts
|
|
5291
|
+
import { createRoute as createRoute28 } from "@hono/zod-openapi";
|
|
5292
|
+
|
|
5293
|
+
// src/routes/oauth/providers.ts
|
|
5294
|
+
var PROVIDERS = {
|
|
5295
|
+
google: {
|
|
5296
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
5297
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
5298
|
+
userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
5299
|
+
scopes: ["email"],
|
|
5300
|
+
emailExtractor: (data) => data.email || null,
|
|
5301
|
+
userIdExtractor: (data) => data.id || null
|
|
5302
|
+
},
|
|
5303
|
+
microsoft: {
|
|
5304
|
+
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
5305
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
5306
|
+
userinfoUrl: "https://graph.microsoft.com/v1.0/me",
|
|
5307
|
+
scopes: ["openid", "email", "User.Read"],
|
|
5308
|
+
emailExtractor: (data) => data.mail || data.userPrincipalName || null,
|
|
5309
|
+
userIdExtractor: (data) => data.id || null
|
|
5310
|
+
}
|
|
5311
|
+
};
|
|
5312
|
+
function getProviderCredentials(provider, env) {
|
|
5313
|
+
if (provider === "google" && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
|
5314
|
+
return { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET };
|
|
5315
|
+
}
|
|
5316
|
+
if (provider === "microsoft" && env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) {
|
|
5317
|
+
return { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET };
|
|
5318
|
+
}
|
|
5319
|
+
return null;
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
// src/routes/oauth/schemas.ts
|
|
5323
|
+
import { z as z10 } from "@hono/zod-openapi";
|
|
5324
|
+
var oauthProviderParamSchema = z10.object({
|
|
5325
|
+
provider: z10.enum(["google", "microsoft"]).openapi({ example: "google" })
|
|
5326
|
+
});
|
|
5327
|
+
var oauthAuthorizeQuerySchema = z10.object({
|
|
5328
|
+
redirect: z10.string().optional().openapi({ example: "/dashboard" }),
|
|
5329
|
+
invitationToken: z10.string().optional().openapi({ example: "token123" }),
|
|
5330
|
+
code_challenge: z10.string().min(43).max(128).optional().openapi({ example: "E9Mrozoa2owUednMY..." }),
|
|
5331
|
+
code_challenge_method: z10.enum(["S256"]).optional().openapi({ example: "S256" }),
|
|
5332
|
+
redirect_uri: z10.string().url().optional().openapi({ example: "myapp://oauth-callback" })
|
|
5333
|
+
}).openapi("OAuthAuthorizeQuery");
|
|
5334
|
+
var oauthCallbackQuerySchema = z10.object({
|
|
5335
|
+
code: z10.string().optional(),
|
|
5336
|
+
state: z10.string().optional(),
|
|
5337
|
+
error: z10.string().optional()
|
|
5338
|
+
});
|
|
5339
|
+
var oauthCompleteRequestSchema = z10.object({
|
|
5340
|
+
code: z10.string().uuid("Invalid code format").openapi({ example: "a1b2c3d4-..." })
|
|
5341
|
+
}).openapi("OAuthCompleteRequest");
|
|
5342
|
+
var oauthCompleteResponseSchema = z10.object({
|
|
5343
|
+
message: z10.string().openapi({ example: "Login successful" }),
|
|
5344
|
+
redirect: z10.string().openapi({ example: "/" }),
|
|
5345
|
+
user: z10.object({
|
|
5346
|
+
id: z10.string().uuid(),
|
|
5347
|
+
email: z10.string().email(),
|
|
5348
|
+
emailVerified: z10.boolean()
|
|
5349
|
+
})
|
|
5350
|
+
}).openapi("OAuthCompleteResponse");
|
|
5351
|
+
var errorResponseSchema4 = z10.object({
|
|
5352
|
+
type: z10.string().url(),
|
|
5353
|
+
title: z10.string(),
|
|
5354
|
+
status: z10.number().int(),
|
|
5355
|
+
detail: z10.string().optional()
|
|
5356
|
+
}).openapi("ErrorResponse");
|
|
5357
|
+
|
|
5358
|
+
// src/routes/oauth/authorize.ts
|
|
5359
|
+
var authorizeRoute = createRoute28({
|
|
5360
|
+
method: "get",
|
|
5361
|
+
path: "/{provider}/authorize",
|
|
5362
|
+
tags: ["OAuth"],
|
|
5363
|
+
summary: "Start OAuth login flow",
|
|
5364
|
+
description: "Generates a CSRF state token and redirects the user to the OAuth provider.",
|
|
5365
|
+
request: {
|
|
5366
|
+
params: oauthProviderParamSchema,
|
|
5367
|
+
query: oauthAuthorizeQuerySchema
|
|
5368
|
+
},
|
|
5369
|
+
responses: {
|
|
5370
|
+
302: { description: "Redirect to OAuth provider or error page" }
|
|
5371
|
+
}
|
|
5372
|
+
});
|
|
5373
|
+
var authorizeHandler = async (c) => {
|
|
5374
|
+
const { provider } = c.req.valid("param");
|
|
5375
|
+
const {
|
|
5376
|
+
redirect: loginRedirect,
|
|
5377
|
+
invitationToken,
|
|
5378
|
+
code_challenge,
|
|
5379
|
+
code_challenge_method,
|
|
5380
|
+
redirect_uri: mobileRedirectUri
|
|
5381
|
+
} = c.req.valid("query");
|
|
5382
|
+
const providerConfig = PROVIDERS[provider];
|
|
5383
|
+
if (!providerConfig) {
|
|
5384
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_provider`);
|
|
5385
|
+
}
|
|
5386
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5387
|
+
if (!credentials) {
|
|
5388
|
+
logError(new Error(`OAuth credentials not configured for ${provider}`), {
|
|
5389
|
+
context: "oauth_authorize",
|
|
5390
|
+
provider
|
|
5391
|
+
});
|
|
5392
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_not_configured`);
|
|
5393
|
+
}
|
|
5394
|
+
if (code_challenge && !code_challenge_method || !code_challenge && code_challenge_method) {
|
|
5395
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_pkce`);
|
|
5396
|
+
}
|
|
5397
|
+
const state = crypto.randomUUID();
|
|
5398
|
+
await c.env.OAUTH_STATES.put(
|
|
5399
|
+
`oauth:state:${state}`,
|
|
5400
|
+
JSON.stringify({
|
|
5401
|
+
provider,
|
|
5402
|
+
invitationToken: invitationToken || null,
|
|
5403
|
+
redirect: loginRedirect || null,
|
|
5404
|
+
codeChallenge: code_challenge || null,
|
|
5405
|
+
codeChallengeMethod: code_challenge_method || null,
|
|
5406
|
+
mobileRedirectUri: mobileRedirectUri || null
|
|
5407
|
+
}),
|
|
5408
|
+
{ expirationTtl: 600 }
|
|
5409
|
+
// 10 minutes
|
|
5410
|
+
);
|
|
5411
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5412
|
+
const authUrl = new URL(providerConfig.authorizeUrl);
|
|
5413
|
+
authUrl.searchParams.set("client_id", credentials.clientId);
|
|
5414
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
5415
|
+
authUrl.searchParams.set("response_type", "code");
|
|
5416
|
+
if (providerConfig.scopes.length > 0) {
|
|
5417
|
+
authUrl.searchParams.set("scope", providerConfig.scopes.join(" "));
|
|
5418
|
+
}
|
|
5419
|
+
authUrl.searchParams.set("state", state);
|
|
5420
|
+
if (provider === "google") {
|
|
5421
|
+
authUrl.searchParams.set("prompt", "select_account");
|
|
5422
|
+
}
|
|
5423
|
+
logger_default.info("OAuth authorize initiated", { provider, hasPkce: !!code_challenge });
|
|
5424
|
+
return c.redirect(authUrl.toString());
|
|
5425
|
+
};
|
|
5426
|
+
|
|
5427
|
+
// src/routes/oauth/callback.ts
|
|
5428
|
+
import { createRoute as createRoute29 } from "@hono/zod-openapi";
|
|
5429
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
5430
|
+
var callbackRoute = createRoute29({
|
|
5431
|
+
method: "get",
|
|
5432
|
+
path: "/{provider}/callback",
|
|
5433
|
+
tags: ["OAuth"],
|
|
5434
|
+
summary: "OAuth provider callback",
|
|
5435
|
+
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.",
|
|
5436
|
+
request: {
|
|
5437
|
+
params: oauthProviderParamSchema,
|
|
5438
|
+
query: oauthCallbackQuerySchema
|
|
5439
|
+
},
|
|
5440
|
+
responses: {
|
|
5441
|
+
302: { description: "Redirect to app with login code or signup token" }
|
|
5442
|
+
}
|
|
5443
|
+
});
|
|
5444
|
+
var callbackHandler = async (c) => {
|
|
5445
|
+
const { provider } = c.req.valid("param");
|
|
5446
|
+
const { code, state, error } = c.req.valid("query");
|
|
5447
|
+
const appUrl = c.env.APP_URL;
|
|
5448
|
+
if (error) {
|
|
5449
|
+
logger_default.info("OAuth provider returned error", { provider, error });
|
|
5450
|
+
const errorType = error === "access_denied" || error === "consent_required" ? "oauth_cancelled" : "oauth_failed";
|
|
5451
|
+
return c.redirect(`${appUrl}/login?error=${errorType}`);
|
|
5452
|
+
}
|
|
5453
|
+
if (!code || !state) {
|
|
5454
|
+
logger_default.warn("Missing code or state in OAuth callback", { provider });
|
|
5455
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5456
|
+
}
|
|
5457
|
+
const storedState = await c.env.OAUTH_STATES.get(`oauth:state:${state}`);
|
|
5458
|
+
if (!storedState) {
|
|
5459
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5460
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5461
|
+
}
|
|
5462
|
+
let stateData;
|
|
5463
|
+
try {
|
|
5464
|
+
stateData = JSON.parse(storedState);
|
|
5465
|
+
} catch (err) {
|
|
5466
|
+
logError(err, { context: "oauth_state_parse", provider });
|
|
5467
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5468
|
+
}
|
|
5469
|
+
if (stateData.provider !== provider) {
|
|
5470
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5471
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5472
|
+
}
|
|
5473
|
+
await c.env.OAUTH_STATES.delete(`oauth:state:${state}`);
|
|
5474
|
+
const {
|
|
5475
|
+
provider: _stateProvider,
|
|
5476
|
+
invitationToken,
|
|
5477
|
+
redirect: loginRedirect,
|
|
5478
|
+
codeChallenge,
|
|
5479
|
+
codeChallengeMethod,
|
|
5480
|
+
mobileRedirectUri
|
|
5481
|
+
} = stateData;
|
|
5482
|
+
const codeVerifier = c.req.query("code_verifier");
|
|
5483
|
+
if (codeChallenge && codeChallengeMethod === "S256") {
|
|
5484
|
+
if (!codeVerifier) {
|
|
5485
|
+
logger_default.warn("OAuth PKCE: missing code_verifier", { provider });
|
|
5486
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_missing`);
|
|
5487
|
+
}
|
|
5488
|
+
const pkceValid = await verifyCodeChallenge(codeVerifier, codeChallenge);
|
|
5489
|
+
if (!pkceValid) {
|
|
5490
|
+
logger_default.warn("OAuth PKCE: invalid code_verifier", { provider });
|
|
5491
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_invalid`);
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
const providerConfig = PROVIDERS[provider];
|
|
5495
|
+
if (!providerConfig) {
|
|
5496
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5497
|
+
}
|
|
5498
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5499
|
+
if (!credentials) {
|
|
5500
|
+
return c.redirect(`${appUrl}/login?error=oauth_not_configured`);
|
|
5501
|
+
}
|
|
5502
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5503
|
+
try {
|
|
5504
|
+
const tokenResponse = await fetch(providerConfig.tokenUrl, {
|
|
5505
|
+
method: "POST",
|
|
5506
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
5507
|
+
body: new URLSearchParams({
|
|
5508
|
+
grant_type: "authorization_code",
|
|
5509
|
+
code,
|
|
5510
|
+
redirect_uri: redirectUri,
|
|
5511
|
+
client_id: credentials.clientId,
|
|
5512
|
+
client_secret: credentials.clientSecret
|
|
5513
|
+
}),
|
|
5514
|
+
signal: AbortSignal.timeout(1e4)
|
|
5515
|
+
});
|
|
5516
|
+
if (!tokenResponse.ok) {
|
|
5517
|
+
const body = await tokenResponse.text();
|
|
5518
|
+
logError(new Error(`OAuth token exchange failed: ${tokenResponse.status} ${body}`), {
|
|
5519
|
+
context: "oauth_token_exchange",
|
|
5520
|
+
provider
|
|
5521
|
+
});
|
|
5522
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5523
|
+
}
|
|
5524
|
+
const tokenData = await tokenResponse.json();
|
|
5525
|
+
if (!tokenData.access_token) {
|
|
5526
|
+
logError(new Error("No access_token in OAuth response"), {
|
|
5527
|
+
context: "oauth_token_exchange",
|
|
5528
|
+
provider
|
|
5529
|
+
});
|
|
5530
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5531
|
+
}
|
|
5532
|
+
const userinfoResponse = await fetch(providerConfig.userinfoUrl, {
|
|
5533
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
|
5534
|
+
signal: AbortSignal.timeout(1e4)
|
|
5535
|
+
});
|
|
5536
|
+
if (!userinfoResponse.ok) {
|
|
5537
|
+
logError(new Error(`OAuth userinfo failed: ${userinfoResponse.status}`), {
|
|
5538
|
+
context: "oauth_userinfo",
|
|
5539
|
+
provider
|
|
5540
|
+
});
|
|
5541
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5542
|
+
}
|
|
5543
|
+
const userInfo = await userinfoResponse.json();
|
|
5544
|
+
const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
|
|
5545
|
+
const providerUserId = providerConfig.userIdExtractor(userInfo);
|
|
5546
|
+
if (!email) {
|
|
5547
|
+
logger_default.warn("No email returned from OAuth provider", { provider });
|
|
5548
|
+
return c.redirect(`${appUrl}/login?error=oauth_no_email`);
|
|
5549
|
+
}
|
|
5550
|
+
if (!providerUserId) {
|
|
5551
|
+
logger_default.warn("No user ID returned from OAuth provider", { provider });
|
|
5552
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5553
|
+
}
|
|
5554
|
+
const { db, schema } = getAuthContext(c);
|
|
5555
|
+
const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where(eq31(schema.users.email, email)).limit(1);
|
|
5556
|
+
if (existingUser) {
|
|
5557
|
+
const loginCode = crypto.randomUUID();
|
|
5558
|
+
await c.env.OAUTH_STATES.put(
|
|
5559
|
+
`oauth:login:${loginCode}`,
|
|
5560
|
+
JSON.stringify({
|
|
5561
|
+
userId: existingUser.id,
|
|
5562
|
+
email: existingUser.email,
|
|
5563
|
+
provider,
|
|
5564
|
+
providerUserId,
|
|
5565
|
+
redirect: loginRedirect,
|
|
5566
|
+
invitationToken
|
|
5567
|
+
}),
|
|
5568
|
+
{ expirationTtl: 60 }
|
|
5569
|
+
);
|
|
5570
|
+
logSecurityEvent("oauth_login_initiated", "low", {
|
|
5571
|
+
userId: existingUser.id,
|
|
5572
|
+
email: existingUser.email,
|
|
5573
|
+
provider
|
|
5574
|
+
});
|
|
5575
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5576
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5577
|
+
mobileUrl.searchParams.set("code", loginCode);
|
|
5578
|
+
mobileUrl.searchParams.set("action", "login");
|
|
5579
|
+
return c.redirect(mobileUrl.toString());
|
|
5580
|
+
}
|
|
5581
|
+
return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
|
|
5582
|
+
} else {
|
|
5583
|
+
const signupToken = crypto.randomUUID();
|
|
5584
|
+
await c.env.OAUTH_STATES.put(
|
|
5585
|
+
`oauth:signup:${signupToken}`,
|
|
5586
|
+
JSON.stringify({ email, provider, providerUserId, invitationToken }),
|
|
5587
|
+
{ expirationTtl: 600 }
|
|
5588
|
+
);
|
|
5589
|
+
logger_default.info("OAuth signup initiated", { provider, email });
|
|
5590
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5591
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5592
|
+
mobileUrl.searchParams.set("oauth_token", signupToken);
|
|
5593
|
+
mobileUrl.searchParams.set("email", email);
|
|
5594
|
+
if (invitationToken) {
|
|
5595
|
+
mobileUrl.searchParams.set("invitation", invitationToken);
|
|
5596
|
+
}
|
|
5597
|
+
return c.redirect(mobileUrl.toString());
|
|
5598
|
+
}
|
|
5599
|
+
let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
|
|
5600
|
+
if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
|
|
5601
|
+
return c.redirect(signupUrl);
|
|
5602
|
+
}
|
|
5603
|
+
} catch (err) {
|
|
5604
|
+
logError(err, { context: "oauth_callback", provider });
|
|
5605
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5606
|
+
}
|
|
5607
|
+
};
|
|
5608
|
+
|
|
5609
|
+
// src/routes/oauth/complete.ts
|
|
5610
|
+
import { createRoute as createRoute30 } from "@hono/zod-openapi";
|
|
5611
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
5612
|
+
var completeRoute = createRoute30({
|
|
5613
|
+
method: "post",
|
|
5614
|
+
path: "/complete",
|
|
5615
|
+
tags: ["OAuth"],
|
|
5616
|
+
summary: "Complete OAuth login",
|
|
5617
|
+
description: "Exchanges a one-time login code (from the OAuth callback) for a session cookie. Called by the /auth/callback frontend page.",
|
|
5618
|
+
request: {
|
|
5619
|
+
body: {
|
|
5620
|
+
content: { "application/json": { schema: oauthCompleteRequestSchema } },
|
|
5621
|
+
required: true
|
|
5622
|
+
}
|
|
5623
|
+
},
|
|
5624
|
+
responses: {
|
|
5625
|
+
200: {
|
|
5626
|
+
description: "Session created successfully",
|
|
5627
|
+
content: { "application/json": { schema: oauthCompleteResponseSchema } }
|
|
5628
|
+
},
|
|
5629
|
+
401: {
|
|
5630
|
+
description: "Invalid or expired login code",
|
|
5631
|
+
content: { "application/json": { schema: errorResponseSchema4 } }
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
});
|
|
5635
|
+
var completeMiddleware = [
|
|
5636
|
+
rateLimit({
|
|
5637
|
+
identifier: (c) => c.req.header("cf-connecting-ip") || "unknown",
|
|
5638
|
+
action: "oauth_complete",
|
|
5639
|
+
maxAttempts: 10,
|
|
5640
|
+
windowMs: 9e5
|
|
5641
|
+
// 15 minutes
|
|
5642
|
+
})
|
|
5643
|
+
];
|
|
5644
|
+
var completeHandler = async (c) => {
|
|
5645
|
+
const { code } = c.req.valid("json");
|
|
5646
|
+
const stored = await c.env.OAUTH_STATES.get(`oauth:login:${code}`);
|
|
5647
|
+
if (!stored) {
|
|
5648
|
+
return problems.unauthorized(c, "Invalid or expired login code");
|
|
5649
|
+
}
|
|
5650
|
+
await c.env.OAUTH_STATES.delete(`oauth:login:${code}`);
|
|
5651
|
+
const {
|
|
5652
|
+
userId,
|
|
5653
|
+
email,
|
|
5654
|
+
provider,
|
|
5655
|
+
providerUserId,
|
|
5656
|
+
redirect: storedRedirect
|
|
5657
|
+
} = JSON.parse(stored);
|
|
5658
|
+
const { db, schema } = getAuthContext(c);
|
|
5659
|
+
await db.update(schema.users).set({ emailVerified: true }).where(eq32(schema.users.id, userId));
|
|
5660
|
+
await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
|
|
5661
|
+
const fingerprint = await generateFingerprint(c.req.raw);
|
|
5662
|
+
const ipAddress = getClientIp(c.req.raw);
|
|
5663
|
+
const sessionId = await createSession(db, { sessions: schema.sessions }, userId, fingerprint, ipAddress);
|
|
5664
|
+
const redirect = storedRedirect?.startsWith("/") && !storedRedirect.startsWith("//") ? storedRedirect : "/";
|
|
5665
|
+
logger_default.info("OAuth login completed", { userId, provider, sessionId: sessionId.slice(0, 8) });
|
|
5666
|
+
logSecurityEvent("oauth_login_success", "low", {
|
|
5667
|
+
userId,
|
|
5668
|
+
email,
|
|
5669
|
+
provider,
|
|
5670
|
+
ip: c.req.header("cf-connecting-ip")
|
|
5671
|
+
});
|
|
5672
|
+
return c.json(
|
|
5673
|
+
{ message: "Login successful", redirect, user: { id: userId, email, emailVerified: true } },
|
|
5674
|
+
200,
|
|
5675
|
+
{ "Set-Cookie": setSessionCookie(sessionId, c.env) }
|
|
5676
|
+
);
|
|
5677
|
+
};
|
|
5678
|
+
|
|
5679
|
+
// src/routes/oauth/index.ts
|
|
5680
|
+
var oauth = new OpenAPIHono2();
|
|
5681
|
+
oauth.use("/complete", ...completeMiddleware);
|
|
5682
|
+
var oauthRoutes = oauth.openapi(authorizeRoute, authorizeHandler).openapi(callbackRoute, callbackHandler).openapi(completeRoute, completeHandler);
|
|
5683
|
+
var oauth_default = oauthRoutes;
|
|
5194
5684
|
|
|
5195
5685
|
// src/routes/index.ts
|
|
5196
|
-
var auth = new
|
|
5686
|
+
var auth = new OpenAPIHono3();
|
|
5197
5687
|
auth.use("*", csrf);
|
|
5198
5688
|
auth.use("/signup", ...signupMiddleware);
|
|
5199
|
-
auth.openapi(signupRoute, signupHandler);
|
|
5200
5689
|
auth.use("/login", ...loginMiddleware);
|
|
5201
|
-
auth.openapi(loginRoute, loginHandler);
|
|
5202
|
-
auth.openapi(logoutRoute, logoutHandler);
|
|
5203
5690
|
auth.use("/me", ...meMiddleware);
|
|
5204
|
-
auth.openapi(meRoute, meHandler);
|
|
5205
|
-
auth.openapi(verifyEmailRoute, verifyEmailHandler);
|
|
5206
5691
|
auth.use("/forgot-password", ...forgotPasswordMiddleware);
|
|
5207
|
-
auth.openapi(forgotPasswordRoute, forgotPasswordHandler);
|
|
5208
5692
|
auth.use("/reset-password", ...resetPasswordMiddleware);
|
|
5209
|
-
auth.openapi(resetPasswordRoute, resetPasswordHandler);
|
|
5210
5693
|
auth.use("/change-password", ...changePasswordMiddleware);
|
|
5211
|
-
auth.openapi(changePasswordRoute, changePasswordHandler);
|
|
5212
5694
|
auth.use("/heartbeat", ...heartbeatMiddleware);
|
|
5213
|
-
auth.openapi(heartbeatRoute, heartbeatHandler);
|
|
5214
5695
|
auth.use("/change-email", ...changeEmailMiddleware);
|
|
5215
|
-
auth.openapi(changeEmailRoute, changeEmailHandler);
|
|
5216
|
-
auth.openapi(confirmEmailChangeRoute, confirmEmailChangeHandler);
|
|
5217
|
-
auth.openapi(cancelEmailChangeRoute, cancelEmailChangeHandler);
|
|
5218
5696
|
auth.use("/account", ...deleteAccountMiddleware);
|
|
5219
|
-
auth.openapi(deleteAccountRoute, deleteAccountHandler);
|
|
5220
5697
|
auth.use("/refresh", ...refreshMiddleware);
|
|
5221
|
-
auth.openapi(refreshRoute, refreshHandler);
|
|
5222
5698
|
auth.use("/resend-verification", ...resendVerificationMiddleware);
|
|
5223
|
-
auth.openapi(resendVerificationRoute, resendVerificationHandler);
|
|
5224
|
-
|
|
5225
|
-
var routes_default = auth;
|
|
5699
|
+
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);
|
|
5700
|
+
var routes_default = authRoutes;
|
|
5226
5701
|
|
|
5227
5702
|
// src/lib/email/webhook-verifier.ts
|
|
5228
5703
|
import { Webhook } from "svix";
|
|
@@ -5250,6 +5725,7 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
5250
5725
|
export {
|
|
5251
5726
|
AUTH_DEFAULTS,
|
|
5252
5727
|
CHALLENGE_TTL_MS,
|
|
5728
|
+
CloudflareEmailAdapter,
|
|
5253
5729
|
EmailService,
|
|
5254
5730
|
MAX_CHALLENGE_ATTEMPTS,
|
|
5255
5731
|
ProblemTypes,
|