@fonderie/auth 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +54 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +55 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -1744,7 +1744,10 @@ function authController(store, config, bus) {
|
|
|
1744
1744
|
passwordHash,
|
|
1745
1745
|
row.userId
|
|
1746
1746
|
]),
|
|
1747
|
-
tx.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [row.userId])
|
|
1747
|
+
tx.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [row.userId]),
|
|
1748
|
+
// Revoke every session: the reset exists because the account may
|
|
1749
|
+
// be compromised — a stolen session must not survive it.
|
|
1750
|
+
tx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId])
|
|
1748
1751
|
]);
|
|
1749
1752
|
});
|
|
1750
1753
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
|
|
@@ -2265,6 +2268,7 @@ function userController(store, config, bus) {
|
|
|
2265
2268
|
}
|
|
2266
2269
|
|
|
2267
2270
|
// src/controllers/oauth.controller.ts
|
|
2271
|
+
var import_node_crypto6 = require("crypto");
|
|
2268
2272
|
var import_core8 = require("@fonderie/core");
|
|
2269
2273
|
function oauthController(store, config) {
|
|
2270
2274
|
const users = new UserModel(store);
|
|
@@ -2280,18 +2284,30 @@ function oauthController(store, config) {
|
|
|
2280
2284
|
"Google OAuth not configured"
|
|
2281
2285
|
);
|
|
2282
2286
|
}
|
|
2287
|
+
const state = (0, import_node_crypto6.randomBytes)(16).toString("hex");
|
|
2283
2288
|
const params = new URLSearchParams({
|
|
2284
2289
|
client_id: google.clientId,
|
|
2285
2290
|
redirect_uri: google.redirectUri,
|
|
2286
2291
|
response_type: "code",
|
|
2287
|
-
scope: "openid email profile"
|
|
2292
|
+
scope: "openid email profile",
|
|
2293
|
+
state
|
|
2288
2294
|
});
|
|
2289
2295
|
const url = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2296
|
+
const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production" ? "; Secure" : "";
|
|
2297
|
+
return Response.json(
|
|
2298
|
+
{
|
|
2299
|
+
reason: "GOOGLE_AUTH_URL",
|
|
2300
|
+
explanation: "Redirect the user to the returned URL to begin Google OAuth.",
|
|
2301
|
+
result: { url }
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
status: 200,
|
|
2305
|
+
// SameSite=Lax (not Strict): the cookie must be sent on the
|
|
2306
|
+
// top-level cross-site redirect back from Google.
|
|
2307
|
+
headers: cookieHeaders([
|
|
2308
|
+
`oauth_state=${state}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600${secure}`
|
|
2309
|
+
])
|
|
2310
|
+
}
|
|
2295
2311
|
);
|
|
2296
2312
|
},
|
|
2297
2313
|
googleCallback: async (ctx) => {
|
|
@@ -2308,6 +2324,16 @@ function oauthController(store, config) {
|
|
|
2308
2324
|
if (!code) {
|
|
2309
2325
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
|
|
2310
2326
|
}
|
|
2327
|
+
const returnedState = url.searchParams.get("state") ?? "";
|
|
2328
|
+
const cookieHeader = ctx.request.headers.get("cookie") ?? "";
|
|
2329
|
+
const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
|
|
2330
|
+
if (!returnedState || !expectedState || !(0, import_core8.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
|
|
2331
|
+
return (0, import_core8.setApiResponse)(
|
|
2332
|
+
import_core8.HTTP.BAD_REQUEST,
|
|
2333
|
+
"GOOGLE_AUTH_FAILED",
|
|
2334
|
+
"OAuth state mismatch \u2014 restart the sign-in flow"
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2311
2337
|
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
2312
2338
|
method: "POST",
|
|
2313
2339
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
@@ -2330,9 +2356,25 @@ function oauthController(store, config) {
|
|
|
2330
2356
|
const payload = JSON.parse(
|
|
2331
2357
|
Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
|
|
2332
2358
|
);
|
|
2359
|
+
if (payload.aud !== google.clientId) {
|
|
2360
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
|
|
2361
|
+
}
|
|
2362
|
+
if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
|
|
2363
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
|
|
2364
|
+
}
|
|
2365
|
+
if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
|
|
2366
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
|
|
2367
|
+
}
|
|
2333
2368
|
if (!payload.email) {
|
|
2334
2369
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
|
|
2335
2370
|
}
|
|
2371
|
+
if (payload.email_verified !== true) {
|
|
2372
|
+
return (0, import_core8.setApiResponse)(
|
|
2373
|
+
import_core8.HTTP.BAD_REQUEST,
|
|
2374
|
+
"GOOGLE_AUTH_FAILED",
|
|
2375
|
+
"Google account email is not verified"
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2336
2378
|
const normalizedEmail = normalizeEmailSafe(payload.email);
|
|
2337
2379
|
if (!normalizedEmail) {
|
|
2338
2380
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "Invalid email in OAuth response");
|
|
@@ -2376,7 +2418,11 @@ function oauthController(store, config) {
|
|
|
2376
2418
|
},
|
|
2377
2419
|
{
|
|
2378
2420
|
status: 200,
|
|
2379
|
-
headers: cookieHeaders(
|
|
2421
|
+
headers: cookieHeaders([
|
|
2422
|
+
...tokenPairCookies(accessToken, refreshToken, config),
|
|
2423
|
+
// One-time value — clear it once the flow completes.
|
|
2424
|
+
"oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0"
|
|
2425
|
+
])
|
|
2380
2426
|
}
|
|
2381
2427
|
);
|
|
2382
2428
|
}
|