@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/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";
|
|
@@ -2816,19 +2816,19 @@ var signupRequestSchema = z.object({
|
|
|
2816
2816
|
var loginRequestSchema = z.object({
|
|
2817
2817
|
email: z.string().email().openapi({ example: "user@example.com" }),
|
|
2818
2818
|
password: z.string().min(1).openapi({ example: "SecurePass123!" }),
|
|
2819
|
-
turnstileToken: z.string().
|
|
2819
|
+
turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
2820
2820
|
}).openapi("LoginRequest");
|
|
2821
2821
|
var verifyEmailRequestSchema = z.object({
|
|
2822
2822
|
token: z.string().min(1).openapi({ example: "verification-token-abc123" })
|
|
2823
2823
|
}).openapi("VerifyEmailRequest");
|
|
2824
2824
|
var forgotPasswordRequestSchema = z.object({
|
|
2825
2825
|
email: z.string().email().openapi({ example: "user@example.com" }),
|
|
2826
|
-
turnstileToken: z.string().
|
|
2826
|
+
turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
2827
2827
|
}).openapi("ForgotPasswordRequest");
|
|
2828
2828
|
var resetPasswordRequestSchema = z.object({
|
|
2829
2829
|
token: z.string().min(1).openapi({ example: "reset-token-xyz789" }),
|
|
2830
2830
|
password: z.string().min(8).openapi({ example: "NewSecurePass456!" }),
|
|
2831
|
-
turnstileToken: z.string().
|
|
2831
|
+
turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
|
|
2832
2832
|
}).openapi("ResetPasswordRequest");
|
|
2833
2833
|
var changePasswordRequestSchema = z.object({
|
|
2834
2834
|
currentPassword: z.string().min(1).openapi({ example: "OldPassword123!" }),
|
|
@@ -5107,11 +5107,8 @@ var challengeResendHandler = async (c) => {
|
|
|
5107
5107
|
var twoFa = new OpenAPIHono();
|
|
5108
5108
|
twoFa.use("*", csrf);
|
|
5109
5109
|
twoFa.use("/status", requireAuth);
|
|
5110
|
-
twoFa.openapi(statusRoute, statusHandler);
|
|
5111
5110
|
twoFa.use("/totp/setup", requireAuth);
|
|
5112
|
-
twoFa.openapi(totpSetupRoute, totpSetupHandler);
|
|
5113
5111
|
twoFa.use("/totp/verify", requireAuth);
|
|
5114
|
-
twoFa.openapi(totpVerifyRoute, totpVerifyHandler);
|
|
5115
5112
|
twoFa.use(
|
|
5116
5113
|
"/totp/disable",
|
|
5117
5114
|
requireAuth,
|
|
@@ -5123,11 +5120,8 @@ twoFa.use(
|
|
|
5123
5120
|
// 5 attempts per 15 minutes
|
|
5124
5121
|
})
|
|
5125
5122
|
);
|
|
5126
|
-
twoFa.openapi(totpDisableRoute, totpDisableHandler);
|
|
5127
5123
|
twoFa.use("/email/setup", requireAuth);
|
|
5128
|
-
twoFa.openapi(emailSetupRoute, emailSetupHandler);
|
|
5129
5124
|
twoFa.use("/email/verify", requireAuth);
|
|
5130
|
-
twoFa.openapi(emailVerifyRoute, emailVerifyHandler);
|
|
5131
5125
|
twoFa.use(
|
|
5132
5126
|
"/email/send-code",
|
|
5133
5127
|
requireAuth,
|
|
@@ -5139,7 +5133,6 @@ twoFa.use(
|
|
|
5139
5133
|
// 3 per 5 minutes
|
|
5140
5134
|
})
|
|
5141
5135
|
);
|
|
5142
|
-
twoFa.openapi(emailSendCodeRoute, emailSendCodeHandler);
|
|
5143
5136
|
twoFa.use(
|
|
5144
5137
|
"/email/disable",
|
|
5145
5138
|
requireAuth,
|
|
@@ -5151,11 +5144,8 @@ twoFa.use(
|
|
|
5151
5144
|
// 5 attempts per 15 minutes
|
|
5152
5145
|
})
|
|
5153
5146
|
);
|
|
5154
|
-
twoFa.openapi(emailDisableRoute, emailDisableHandler);
|
|
5155
5147
|
twoFa.use("/trusted-devices", requireAuth);
|
|
5156
|
-
twoFa.openapi(trustedDevicesGetRoute, trustedDevicesGetHandler);
|
|
5157
5148
|
twoFa.use("/trusted-devices/:id", requireAuth);
|
|
5158
|
-
twoFa.openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler);
|
|
5159
5149
|
twoFa.use(
|
|
5160
5150
|
"/backup-codes/regenerate",
|
|
5161
5151
|
requireAuth,
|
|
@@ -5167,7 +5157,6 @@ twoFa.use(
|
|
|
5167
5157
|
// 3 per hour per user
|
|
5168
5158
|
})
|
|
5169
5159
|
);
|
|
5170
|
-
twoFa.openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler);
|
|
5171
5160
|
twoFa.use(
|
|
5172
5161
|
"/challenge",
|
|
5173
5162
|
rateLimit({
|
|
@@ -5178,7 +5167,6 @@ twoFa.use(
|
|
|
5178
5167
|
// 10 per 5 min per IP
|
|
5179
5168
|
})
|
|
5180
5169
|
);
|
|
5181
|
-
twoFa.openapi(challengeRoute, challengeHandler);
|
|
5182
5170
|
twoFa.use(
|
|
5183
5171
|
"/challenge/resend",
|
|
5184
5172
|
rateLimit({
|
|
@@ -5189,40 +5177,423 @@ twoFa.use(
|
|
|
5189
5177
|
// 3 per 5 min per IP
|
|
5190
5178
|
})
|
|
5191
5179
|
);
|
|
5192
|
-
twoFa.openapi(challengeResendRoute, challengeResendHandler);
|
|
5193
|
-
var fa_default =
|
|
5180
|
+
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);
|
|
5181
|
+
var fa_default = twoFaRoutes;
|
|
5182
|
+
|
|
5183
|
+
// src/routes/oauth/index.ts
|
|
5184
|
+
import { OpenAPIHono as OpenAPIHono2 } from "@hono/zod-openapi";
|
|
5185
|
+
|
|
5186
|
+
// src/routes/oauth/authorize.ts
|
|
5187
|
+
import { createRoute as createRoute28 } from "@hono/zod-openapi";
|
|
5188
|
+
|
|
5189
|
+
// src/routes/oauth/providers.ts
|
|
5190
|
+
var PROVIDERS = {
|
|
5191
|
+
google: {
|
|
5192
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
5193
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
5194
|
+
userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
5195
|
+
scopes: ["email"],
|
|
5196
|
+
emailExtractor: (data) => data.email || null,
|
|
5197
|
+
userIdExtractor: (data) => data.id || null
|
|
5198
|
+
},
|
|
5199
|
+
microsoft: {
|
|
5200
|
+
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
5201
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
5202
|
+
userinfoUrl: "https://graph.microsoft.com/v1.0/me",
|
|
5203
|
+
scopes: ["openid", "email", "User.Read"],
|
|
5204
|
+
emailExtractor: (data) => data.mail || data.userPrincipalName || null,
|
|
5205
|
+
userIdExtractor: (data) => data.id || null
|
|
5206
|
+
}
|
|
5207
|
+
};
|
|
5208
|
+
function getProviderCredentials(provider, env) {
|
|
5209
|
+
if (provider === "google" && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
|
5210
|
+
return { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET };
|
|
5211
|
+
}
|
|
5212
|
+
if (provider === "microsoft" && env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) {
|
|
5213
|
+
return { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET };
|
|
5214
|
+
}
|
|
5215
|
+
return null;
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5218
|
+
// src/routes/oauth/schemas.ts
|
|
5219
|
+
import { z as z10 } from "@hono/zod-openapi";
|
|
5220
|
+
var oauthProviderParamSchema = z10.object({
|
|
5221
|
+
provider: z10.enum(["google", "microsoft"]).openapi({ example: "google" })
|
|
5222
|
+
});
|
|
5223
|
+
var oauthAuthorizeQuerySchema = z10.object({
|
|
5224
|
+
redirect: z10.string().optional().openapi({ example: "/dashboard" }),
|
|
5225
|
+
invitationToken: z10.string().optional().openapi({ example: "token123" }),
|
|
5226
|
+
code_challenge: z10.string().min(43).max(128).optional().openapi({ example: "E9Mrozoa2owUednMY..." }),
|
|
5227
|
+
code_challenge_method: z10.enum(["S256"]).optional().openapi({ example: "S256" }),
|
|
5228
|
+
redirect_uri: z10.string().url().optional().openapi({ example: "myapp://oauth-callback" })
|
|
5229
|
+
}).openapi("OAuthAuthorizeQuery");
|
|
5230
|
+
var oauthCallbackQuerySchema = z10.object({
|
|
5231
|
+
code: z10.string().optional(),
|
|
5232
|
+
state: z10.string().optional(),
|
|
5233
|
+
error: z10.string().optional()
|
|
5234
|
+
});
|
|
5235
|
+
var oauthCompleteRequestSchema = z10.object({
|
|
5236
|
+
code: z10.string().uuid("Invalid code format").openapi({ example: "a1b2c3d4-..." })
|
|
5237
|
+
}).openapi("OAuthCompleteRequest");
|
|
5238
|
+
var oauthCompleteResponseSchema = z10.object({
|
|
5239
|
+
message: z10.string().openapi({ example: "Login successful" }),
|
|
5240
|
+
redirect: z10.string().openapi({ example: "/" }),
|
|
5241
|
+
user: z10.object({
|
|
5242
|
+
id: z10.string().uuid(),
|
|
5243
|
+
email: z10.string().email(),
|
|
5244
|
+
emailVerified: z10.boolean()
|
|
5245
|
+
})
|
|
5246
|
+
}).openapi("OAuthCompleteResponse");
|
|
5247
|
+
var errorResponseSchema4 = z10.object({
|
|
5248
|
+
type: z10.string().url(),
|
|
5249
|
+
title: z10.string(),
|
|
5250
|
+
status: z10.number().int(),
|
|
5251
|
+
detail: z10.string().optional()
|
|
5252
|
+
}).openapi("ErrorResponse");
|
|
5253
|
+
|
|
5254
|
+
// src/routes/oauth/authorize.ts
|
|
5255
|
+
var authorizeRoute = createRoute28({
|
|
5256
|
+
method: "get",
|
|
5257
|
+
path: "/{provider}/authorize",
|
|
5258
|
+
tags: ["OAuth"],
|
|
5259
|
+
summary: "Start OAuth login flow",
|
|
5260
|
+
description: "Generates a CSRF state token and redirects the user to the OAuth provider.",
|
|
5261
|
+
request: {
|
|
5262
|
+
params: oauthProviderParamSchema,
|
|
5263
|
+
query: oauthAuthorizeQuerySchema
|
|
5264
|
+
},
|
|
5265
|
+
responses: {
|
|
5266
|
+
302: { description: "Redirect to OAuth provider or error page" }
|
|
5267
|
+
}
|
|
5268
|
+
});
|
|
5269
|
+
var authorizeHandler = async (c) => {
|
|
5270
|
+
const { provider } = c.req.valid("param");
|
|
5271
|
+
const {
|
|
5272
|
+
redirect: loginRedirect,
|
|
5273
|
+
invitationToken,
|
|
5274
|
+
code_challenge,
|
|
5275
|
+
code_challenge_method,
|
|
5276
|
+
redirect_uri: mobileRedirectUri
|
|
5277
|
+
} = c.req.valid("query");
|
|
5278
|
+
const providerConfig = PROVIDERS[provider];
|
|
5279
|
+
if (!providerConfig) {
|
|
5280
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_provider`);
|
|
5281
|
+
}
|
|
5282
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5283
|
+
if (!credentials) {
|
|
5284
|
+
logError(new Error(`OAuth credentials not configured for ${provider}`), {
|
|
5285
|
+
context: "oauth_authorize",
|
|
5286
|
+
provider
|
|
5287
|
+
});
|
|
5288
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_not_configured`);
|
|
5289
|
+
}
|
|
5290
|
+
if (code_challenge && !code_challenge_method || !code_challenge && code_challenge_method) {
|
|
5291
|
+
return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_pkce`);
|
|
5292
|
+
}
|
|
5293
|
+
const state = crypto.randomUUID();
|
|
5294
|
+
await c.env.OAUTH_STATES.put(
|
|
5295
|
+
`oauth:state:${state}`,
|
|
5296
|
+
JSON.stringify({
|
|
5297
|
+
provider,
|
|
5298
|
+
invitationToken: invitationToken || null,
|
|
5299
|
+
redirect: loginRedirect || null,
|
|
5300
|
+
codeChallenge: code_challenge || null,
|
|
5301
|
+
codeChallengeMethod: code_challenge_method || null,
|
|
5302
|
+
mobileRedirectUri: mobileRedirectUri || null
|
|
5303
|
+
}),
|
|
5304
|
+
{ expirationTtl: 600 }
|
|
5305
|
+
// 10 minutes
|
|
5306
|
+
);
|
|
5307
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5308
|
+
const authUrl = new URL(providerConfig.authorizeUrl);
|
|
5309
|
+
authUrl.searchParams.set("client_id", credentials.clientId);
|
|
5310
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
5311
|
+
authUrl.searchParams.set("response_type", "code");
|
|
5312
|
+
if (providerConfig.scopes.length > 0) {
|
|
5313
|
+
authUrl.searchParams.set("scope", providerConfig.scopes.join(" "));
|
|
5314
|
+
}
|
|
5315
|
+
authUrl.searchParams.set("state", state);
|
|
5316
|
+
if (provider === "google") {
|
|
5317
|
+
authUrl.searchParams.set("prompt", "select_account");
|
|
5318
|
+
}
|
|
5319
|
+
logger_default.info("OAuth authorize initiated", { provider, hasPkce: !!code_challenge });
|
|
5320
|
+
return c.redirect(authUrl.toString());
|
|
5321
|
+
};
|
|
5322
|
+
|
|
5323
|
+
// src/routes/oauth/callback.ts
|
|
5324
|
+
import { createRoute as createRoute29 } from "@hono/zod-openapi";
|
|
5325
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
5326
|
+
var callbackRoute = createRoute29({
|
|
5327
|
+
method: "get",
|
|
5328
|
+
path: "/{provider}/callback",
|
|
5329
|
+
tags: ["OAuth"],
|
|
5330
|
+
summary: "OAuth provider callback",
|
|
5331
|
+
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.",
|
|
5332
|
+
request: {
|
|
5333
|
+
params: oauthProviderParamSchema,
|
|
5334
|
+
query: oauthCallbackQuerySchema
|
|
5335
|
+
},
|
|
5336
|
+
responses: {
|
|
5337
|
+
302: { description: "Redirect to app with login code or signup token" }
|
|
5338
|
+
}
|
|
5339
|
+
});
|
|
5340
|
+
var callbackHandler = async (c) => {
|
|
5341
|
+
const { provider } = c.req.valid("param");
|
|
5342
|
+
const { code, state, error } = c.req.valid("query");
|
|
5343
|
+
const appUrl = c.env.APP_URL;
|
|
5344
|
+
if (error) {
|
|
5345
|
+
logger_default.info("OAuth provider returned error", { provider, error });
|
|
5346
|
+
const errorType = error === "access_denied" || error === "consent_required" ? "oauth_cancelled" : "oauth_failed";
|
|
5347
|
+
return c.redirect(`${appUrl}/login?error=${errorType}`);
|
|
5348
|
+
}
|
|
5349
|
+
if (!code || !state) {
|
|
5350
|
+
logger_default.warn("Missing code or state in OAuth callback", { provider });
|
|
5351
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5352
|
+
}
|
|
5353
|
+
const storedState = await c.env.OAUTH_STATES.get(`oauth:state:${state}`);
|
|
5354
|
+
if (!storedState) {
|
|
5355
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5356
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5357
|
+
}
|
|
5358
|
+
let stateData;
|
|
5359
|
+
try {
|
|
5360
|
+
stateData = JSON.parse(storedState);
|
|
5361
|
+
} catch (err) {
|
|
5362
|
+
logError(err, { context: "oauth_state_parse", provider });
|
|
5363
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5364
|
+
}
|
|
5365
|
+
if (stateData.provider !== provider) {
|
|
5366
|
+
logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
|
|
5367
|
+
return c.redirect(`${appUrl}/login?error=oauth_expired`);
|
|
5368
|
+
}
|
|
5369
|
+
await c.env.OAUTH_STATES.delete(`oauth:state:${state}`);
|
|
5370
|
+
const {
|
|
5371
|
+
provider: _stateProvider,
|
|
5372
|
+
invitationToken,
|
|
5373
|
+
redirect: loginRedirect,
|
|
5374
|
+
codeChallenge,
|
|
5375
|
+
codeChallengeMethod,
|
|
5376
|
+
mobileRedirectUri
|
|
5377
|
+
} = stateData;
|
|
5378
|
+
const codeVerifier = c.req.query("code_verifier");
|
|
5379
|
+
if (codeChallenge && codeChallengeMethod === "S256") {
|
|
5380
|
+
if (!codeVerifier) {
|
|
5381
|
+
logger_default.warn("OAuth PKCE: missing code_verifier", { provider });
|
|
5382
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_missing`);
|
|
5383
|
+
}
|
|
5384
|
+
const pkceValid = await verifyCodeChallenge(codeVerifier, codeChallenge);
|
|
5385
|
+
if (!pkceValid) {
|
|
5386
|
+
logger_default.warn("OAuth PKCE: invalid code_verifier", { provider });
|
|
5387
|
+
return c.redirect(`${appUrl}/login?error=oauth_pkce_invalid`);
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
const providerConfig = PROVIDERS[provider];
|
|
5391
|
+
if (!providerConfig) {
|
|
5392
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5393
|
+
}
|
|
5394
|
+
const credentials = getProviderCredentials(provider, c.env);
|
|
5395
|
+
if (!credentials) {
|
|
5396
|
+
return c.redirect(`${appUrl}/login?error=oauth_not_configured`);
|
|
5397
|
+
}
|
|
5398
|
+
const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
|
|
5399
|
+
try {
|
|
5400
|
+
const tokenResponse = await fetch(providerConfig.tokenUrl, {
|
|
5401
|
+
method: "POST",
|
|
5402
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
5403
|
+
body: new URLSearchParams({
|
|
5404
|
+
grant_type: "authorization_code",
|
|
5405
|
+
code,
|
|
5406
|
+
redirect_uri: redirectUri,
|
|
5407
|
+
client_id: credentials.clientId,
|
|
5408
|
+
client_secret: credentials.clientSecret
|
|
5409
|
+
}),
|
|
5410
|
+
signal: AbortSignal.timeout(1e4)
|
|
5411
|
+
});
|
|
5412
|
+
if (!tokenResponse.ok) {
|
|
5413
|
+
const body = await tokenResponse.text();
|
|
5414
|
+
logError(new Error(`OAuth token exchange failed: ${tokenResponse.status} ${body}`), {
|
|
5415
|
+
context: "oauth_token_exchange",
|
|
5416
|
+
provider
|
|
5417
|
+
});
|
|
5418
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5419
|
+
}
|
|
5420
|
+
const tokenData = await tokenResponse.json();
|
|
5421
|
+
if (!tokenData.access_token) {
|
|
5422
|
+
logError(new Error("No access_token in OAuth response"), {
|
|
5423
|
+
context: "oauth_token_exchange",
|
|
5424
|
+
provider
|
|
5425
|
+
});
|
|
5426
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5427
|
+
}
|
|
5428
|
+
const userinfoResponse = await fetch(providerConfig.userinfoUrl, {
|
|
5429
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
|
5430
|
+
signal: AbortSignal.timeout(1e4)
|
|
5431
|
+
});
|
|
5432
|
+
if (!userinfoResponse.ok) {
|
|
5433
|
+
logError(new Error(`OAuth userinfo failed: ${userinfoResponse.status}`), {
|
|
5434
|
+
context: "oauth_userinfo",
|
|
5435
|
+
provider
|
|
5436
|
+
});
|
|
5437
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5438
|
+
}
|
|
5439
|
+
const userInfo = await userinfoResponse.json();
|
|
5440
|
+
const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
|
|
5441
|
+
const providerUserId = providerConfig.userIdExtractor(userInfo);
|
|
5442
|
+
if (!email) {
|
|
5443
|
+
logger_default.warn("No email returned from OAuth provider", { provider });
|
|
5444
|
+
return c.redirect(`${appUrl}/login?error=oauth_no_email`);
|
|
5445
|
+
}
|
|
5446
|
+
if (!providerUserId) {
|
|
5447
|
+
logger_default.warn("No user ID returned from OAuth provider", { provider });
|
|
5448
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5449
|
+
}
|
|
5450
|
+
const { db, schema } = getAuthContext(c);
|
|
5451
|
+
const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where(eq31(schema.users.email, email)).limit(1);
|
|
5452
|
+
if (existingUser) {
|
|
5453
|
+
const loginCode = crypto.randomUUID();
|
|
5454
|
+
await c.env.OAUTH_STATES.put(
|
|
5455
|
+
`oauth:login:${loginCode}`,
|
|
5456
|
+
JSON.stringify({
|
|
5457
|
+
userId: existingUser.id,
|
|
5458
|
+
email: existingUser.email,
|
|
5459
|
+
provider,
|
|
5460
|
+
providerUserId,
|
|
5461
|
+
redirect: loginRedirect,
|
|
5462
|
+
invitationToken
|
|
5463
|
+
}),
|
|
5464
|
+
{ expirationTtl: 60 }
|
|
5465
|
+
);
|
|
5466
|
+
logSecurityEvent("oauth_login_initiated", "low", {
|
|
5467
|
+
userId: existingUser.id,
|
|
5468
|
+
email: existingUser.email,
|
|
5469
|
+
provider
|
|
5470
|
+
});
|
|
5471
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5472
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5473
|
+
mobileUrl.searchParams.set("code", loginCode);
|
|
5474
|
+
mobileUrl.searchParams.set("action", "login");
|
|
5475
|
+
return c.redirect(mobileUrl.toString());
|
|
5476
|
+
}
|
|
5477
|
+
return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
|
|
5478
|
+
} else {
|
|
5479
|
+
const signupToken = crypto.randomUUID();
|
|
5480
|
+
await c.env.OAUTH_STATES.put(
|
|
5481
|
+
`oauth:signup:${signupToken}`,
|
|
5482
|
+
JSON.stringify({ email, provider, providerUserId, invitationToken }),
|
|
5483
|
+
{ expirationTtl: 600 }
|
|
5484
|
+
);
|
|
5485
|
+
logger_default.info("OAuth signup initiated", { provider, email });
|
|
5486
|
+
if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
|
|
5487
|
+
const mobileUrl = new URL(mobileRedirectUri);
|
|
5488
|
+
mobileUrl.searchParams.set("oauth_token", signupToken);
|
|
5489
|
+
mobileUrl.searchParams.set("email", email);
|
|
5490
|
+
if (invitationToken) {
|
|
5491
|
+
mobileUrl.searchParams.set("invitation", invitationToken);
|
|
5492
|
+
}
|
|
5493
|
+
return c.redirect(mobileUrl.toString());
|
|
5494
|
+
}
|
|
5495
|
+
let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
|
|
5496
|
+
if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
|
|
5497
|
+
return c.redirect(signupUrl);
|
|
5498
|
+
}
|
|
5499
|
+
} catch (err) {
|
|
5500
|
+
logError(err, { context: "oauth_callback", provider });
|
|
5501
|
+
return c.redirect(`${appUrl}/login?error=oauth_failed`);
|
|
5502
|
+
}
|
|
5503
|
+
};
|
|
5504
|
+
|
|
5505
|
+
// src/routes/oauth/complete.ts
|
|
5506
|
+
import { createRoute as createRoute30 } from "@hono/zod-openapi";
|
|
5507
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
5508
|
+
var completeRoute = createRoute30({
|
|
5509
|
+
method: "post",
|
|
5510
|
+
path: "/complete",
|
|
5511
|
+
tags: ["OAuth"],
|
|
5512
|
+
summary: "Complete OAuth login",
|
|
5513
|
+
description: "Exchanges a one-time login code (from the OAuth callback) for a session cookie. Called by the /auth/callback frontend page.",
|
|
5514
|
+
request: {
|
|
5515
|
+
body: {
|
|
5516
|
+
content: { "application/json": { schema: oauthCompleteRequestSchema } },
|
|
5517
|
+
required: true
|
|
5518
|
+
}
|
|
5519
|
+
},
|
|
5520
|
+
responses: {
|
|
5521
|
+
200: {
|
|
5522
|
+
description: "Session created successfully",
|
|
5523
|
+
content: { "application/json": { schema: oauthCompleteResponseSchema } }
|
|
5524
|
+
},
|
|
5525
|
+
401: {
|
|
5526
|
+
description: "Invalid or expired login code",
|
|
5527
|
+
content: { "application/json": { schema: errorResponseSchema4 } }
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
});
|
|
5531
|
+
var completeMiddleware = [
|
|
5532
|
+
rateLimit({
|
|
5533
|
+
identifier: (c) => c.req.header("cf-connecting-ip") || "unknown",
|
|
5534
|
+
action: "oauth_complete",
|
|
5535
|
+
maxAttempts: 10,
|
|
5536
|
+
windowMs: 9e5
|
|
5537
|
+
// 15 minutes
|
|
5538
|
+
})
|
|
5539
|
+
];
|
|
5540
|
+
var completeHandler = async (c) => {
|
|
5541
|
+
const { code } = c.req.valid("json");
|
|
5542
|
+
const stored = await c.env.OAUTH_STATES.get(`oauth:login:${code}`);
|
|
5543
|
+
if (!stored) {
|
|
5544
|
+
return problems.unauthorized(c, "Invalid or expired login code");
|
|
5545
|
+
}
|
|
5546
|
+
await c.env.OAUTH_STATES.delete(`oauth:login:${code}`);
|
|
5547
|
+
const {
|
|
5548
|
+
userId,
|
|
5549
|
+
email,
|
|
5550
|
+
provider,
|
|
5551
|
+
providerUserId,
|
|
5552
|
+
redirect: storedRedirect
|
|
5553
|
+
} = JSON.parse(stored);
|
|
5554
|
+
const { db, schema } = getAuthContext(c);
|
|
5555
|
+
await db.update(schema.users).set({ emailVerified: true }).where(eq32(schema.users.id, userId));
|
|
5556
|
+
await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
|
|
5557
|
+
const fingerprint = await generateFingerprint(c.req.raw);
|
|
5558
|
+
const ipAddress = getClientIp(c.req.raw);
|
|
5559
|
+
const sessionId = await createSession(db, { sessions: schema.sessions }, userId, fingerprint, ipAddress);
|
|
5560
|
+
const redirect = storedRedirect?.startsWith("/") && !storedRedirect.startsWith("//") ? storedRedirect : "/";
|
|
5561
|
+
logger_default.info("OAuth login completed", { userId, provider, sessionId: sessionId.slice(0, 8) });
|
|
5562
|
+
logSecurityEvent("oauth_login_success", "low", {
|
|
5563
|
+
userId,
|
|
5564
|
+
email,
|
|
5565
|
+
provider,
|
|
5566
|
+
ip: c.req.header("cf-connecting-ip")
|
|
5567
|
+
});
|
|
5568
|
+
return c.json(
|
|
5569
|
+
{ message: "Login successful", redirect, user: { id: userId, email, emailVerified: true } },
|
|
5570
|
+
200,
|
|
5571
|
+
{ "Set-Cookie": setSessionCookie(sessionId, c.env) }
|
|
5572
|
+
);
|
|
5573
|
+
};
|
|
5574
|
+
|
|
5575
|
+
// src/routes/oauth/index.ts
|
|
5576
|
+
var oauth = new OpenAPIHono2();
|
|
5577
|
+
oauth.use("/complete", ...completeMiddleware);
|
|
5578
|
+
var oauthRoutes = oauth.openapi(authorizeRoute, authorizeHandler).openapi(callbackRoute, callbackHandler).openapi(completeRoute, completeHandler);
|
|
5579
|
+
var oauth_default = oauthRoutes;
|
|
5194
5580
|
|
|
5195
5581
|
// src/routes/index.ts
|
|
5196
|
-
var auth = new
|
|
5582
|
+
var auth = new OpenAPIHono3();
|
|
5197
5583
|
auth.use("*", csrf);
|
|
5198
5584
|
auth.use("/signup", ...signupMiddleware);
|
|
5199
|
-
auth.openapi(signupRoute, signupHandler);
|
|
5200
5585
|
auth.use("/login", ...loginMiddleware);
|
|
5201
|
-
auth.openapi(loginRoute, loginHandler);
|
|
5202
|
-
auth.openapi(logoutRoute, logoutHandler);
|
|
5203
5586
|
auth.use("/me", ...meMiddleware);
|
|
5204
|
-
auth.openapi(meRoute, meHandler);
|
|
5205
|
-
auth.openapi(verifyEmailRoute, verifyEmailHandler);
|
|
5206
5587
|
auth.use("/forgot-password", ...forgotPasswordMiddleware);
|
|
5207
|
-
auth.openapi(forgotPasswordRoute, forgotPasswordHandler);
|
|
5208
5588
|
auth.use("/reset-password", ...resetPasswordMiddleware);
|
|
5209
|
-
auth.openapi(resetPasswordRoute, resetPasswordHandler);
|
|
5210
5589
|
auth.use("/change-password", ...changePasswordMiddleware);
|
|
5211
|
-
auth.openapi(changePasswordRoute, changePasswordHandler);
|
|
5212
5590
|
auth.use("/heartbeat", ...heartbeatMiddleware);
|
|
5213
|
-
auth.openapi(heartbeatRoute, heartbeatHandler);
|
|
5214
5591
|
auth.use("/change-email", ...changeEmailMiddleware);
|
|
5215
|
-
auth.openapi(changeEmailRoute, changeEmailHandler);
|
|
5216
|
-
auth.openapi(confirmEmailChangeRoute, confirmEmailChangeHandler);
|
|
5217
|
-
auth.openapi(cancelEmailChangeRoute, cancelEmailChangeHandler);
|
|
5218
5592
|
auth.use("/account", ...deleteAccountMiddleware);
|
|
5219
|
-
auth.openapi(deleteAccountRoute, deleteAccountHandler);
|
|
5220
5593
|
auth.use("/refresh", ...refreshMiddleware);
|
|
5221
|
-
auth.openapi(refreshRoute, refreshHandler);
|
|
5222
5594
|
auth.use("/resend-verification", ...resendVerificationMiddleware);
|
|
5223
|
-
auth.openapi(resendVerificationRoute, resendVerificationHandler);
|
|
5224
|
-
|
|
5225
|
-
var routes_default = auth;
|
|
5595
|
+
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);
|
|
5596
|
+
var routes_default = authRoutes;
|
|
5226
5597
|
|
|
5227
5598
|
// src/lib/email/webhook-verifier.ts
|
|
5228
5599
|
import { Webhook } from "svix";
|