@netlify/identity 1.0.0 → 1.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 +248 -60
- package/dist/{index.cjs → main.cjs} +53 -30
- package/dist/{index.d.ts → main.d.cts} +72 -1
- package/dist/{index.d.cts → main.d.ts} +72 -1
- package/dist/{index.js → main.js} +47 -25
- package/package.json +30 -41
- package/LICENSE +0 -21
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -27,9 +27,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
));
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
|
-
// src/
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
30
|
+
// src/main.ts
|
|
31
|
+
var main_exports = {};
|
|
32
|
+
__export(main_exports, {
|
|
33
33
|
AUTH_EVENTS: () => AUTH_EVENTS,
|
|
34
34
|
AuthError: () => AuthError,
|
|
35
35
|
MissingIdentityError: () => MissingIdentityError,
|
|
@@ -51,9 +51,10 @@ __export(index_exports, {
|
|
|
51
51
|
requestPasswordRecovery: () => requestPasswordRecovery,
|
|
52
52
|
signup: () => signup,
|
|
53
53
|
updateUser: () => updateUser,
|
|
54
|
-
verifyEmailChange: () => verifyEmailChange
|
|
54
|
+
verifyEmailChange: () => verifyEmailChange,
|
|
55
|
+
verifyRequestOrigin: () => verifyRequestOrigin
|
|
55
56
|
});
|
|
56
|
-
module.exports = __toCommonJS(
|
|
57
|
+
module.exports = __toCommonJS(main_exports);
|
|
57
58
|
|
|
58
59
|
// src/types.ts
|
|
59
60
|
var AUTH_PROVIDERS = ["google", "github", "gitlab", "bitbucket", "facebook", "email"];
|
|
@@ -149,7 +150,7 @@ var NF_JWT_COOKIE = "nf_jwt";
|
|
|
149
150
|
var NF_REFRESH_COOKIE = "nf_refresh";
|
|
150
151
|
var getCookie = (name) => {
|
|
151
152
|
if (typeof document === "undefined") return null;
|
|
152
|
-
const match =
|
|
153
|
+
const match = new RegExp(`(?:^|; )${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]*)`).exec(document.cookie);
|
|
153
154
|
if (!match) return null;
|
|
154
155
|
try {
|
|
155
156
|
return decodeURIComponent(match[1]);
|
|
@@ -231,13 +232,15 @@ var triggerNextjsDynamic = () => {
|
|
|
231
232
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
232
233
|
var fetchWithTimeout = async (url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) => {
|
|
233
234
|
const controller = new AbortController();
|
|
234
|
-
const timer = setTimeout(() =>
|
|
235
|
+
const timer = setTimeout(() => {
|
|
236
|
+
controller.abort();
|
|
237
|
+
}, timeoutMs);
|
|
235
238
|
try {
|
|
236
239
|
return await fetch(url, { ...options, signal: controller.signal });
|
|
237
240
|
} catch (error) {
|
|
238
241
|
if (error instanceof Error && error.name === "AbortError") {
|
|
239
242
|
const pathname = new URL(url).pathname;
|
|
240
|
-
throw new AuthError(`Identity request to ${pathname} timed out after ${timeoutMs}ms`);
|
|
243
|
+
throw new AuthError(`Identity request to ${pathname} timed out after ${String(timeoutMs)}ms`);
|
|
241
244
|
}
|
|
242
245
|
throw error;
|
|
243
246
|
} finally {
|
|
@@ -304,16 +307,18 @@ var startTokenRefresh = () => {
|
|
|
304
307
|
const nowS = Math.floor(Date.now() / 1e3);
|
|
305
308
|
const expiresAtS = typeof token.expires_at === "number" && token.expires_at > 1e12 ? Math.floor(token.expires_at / 1e3) : token.expires_at;
|
|
306
309
|
const delayMs = Math.max(0, expiresAtS - nowS - REFRESH_MARGIN_S) * 1e3;
|
|
307
|
-
refreshTimer = setTimeout(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
310
|
+
refreshTimer = setTimeout(() => {
|
|
311
|
+
void (async () => {
|
|
312
|
+
try {
|
|
313
|
+
const freshJwt = await user.jwt(true);
|
|
314
|
+
const freshDetails = user.tokenDetails();
|
|
315
|
+
setBrowserAuthCookies(freshJwt, freshDetails?.refresh_token);
|
|
316
|
+
emitAuthEvent(AUTH_EVENTS.TOKEN_REFRESH, toUser(user));
|
|
317
|
+
startTokenRefresh();
|
|
318
|
+
} catch {
|
|
319
|
+
stopTokenRefresh();
|
|
320
|
+
}
|
|
321
|
+
})();
|
|
317
322
|
}, delayMs);
|
|
318
323
|
};
|
|
319
324
|
var stopTokenRefresh = () => {
|
|
@@ -379,7 +384,7 @@ var refreshSession = async () => {
|
|
|
379
384
|
}
|
|
380
385
|
return null;
|
|
381
386
|
}
|
|
382
|
-
throw new AuthError(errorBody.msg
|
|
387
|
+
throw new AuthError(errorBody.msg ?? `Token refresh failed (${String(res.status)})`, res.status);
|
|
383
388
|
}
|
|
384
389
|
const data = await res.json();
|
|
385
390
|
const cookies = globalThis.Netlify?.context?.cookies;
|
|
@@ -426,7 +431,10 @@ var login = async (email, password) => {
|
|
|
426
431
|
}
|
|
427
432
|
if (!res.ok) {
|
|
428
433
|
const errorBody = await res.json().catch(() => ({}));
|
|
429
|
-
throw new AuthError(
|
|
434
|
+
throw new AuthError(
|
|
435
|
+
errorBody.msg ?? errorBody.error_description ?? `Login failed (${String(res.status)})`,
|
|
436
|
+
res.status
|
|
437
|
+
);
|
|
430
438
|
}
|
|
431
439
|
const data = await res.json();
|
|
432
440
|
const accessToken = data.access_token;
|
|
@@ -440,7 +448,7 @@ var login = async (email, password) => {
|
|
|
440
448
|
}
|
|
441
449
|
if (!userRes.ok) {
|
|
442
450
|
const errorBody = await userRes.json().catch(() => ({}));
|
|
443
|
-
throw new AuthError(errorBody.msg
|
|
451
|
+
throw new AuthError(errorBody.msg ?? `Failed to fetch user data (${String(userRes.status)})`, userRes.status);
|
|
444
452
|
}
|
|
445
453
|
const userData = await userRes.json();
|
|
446
454
|
const user = toUser(userData);
|
|
@@ -476,7 +484,7 @@ var signup = async (email, password, data) => {
|
|
|
476
484
|
}
|
|
477
485
|
if (!res.ok) {
|
|
478
486
|
const errorBody = await res.json().catch(() => ({}));
|
|
479
|
-
throw new AuthError(errorBody.msg
|
|
487
|
+
throw new AuthError(errorBody.msg ?? `Signup failed (${String(res.status)})`, res.status);
|
|
480
488
|
}
|
|
481
489
|
const responseData = await res.json();
|
|
482
490
|
const user = toUser(responseData);
|
|
@@ -630,7 +638,7 @@ var handleEmailChangeCallback = async (client, emailChangeToken) => {
|
|
|
630
638
|
if (!emailChangeRes.ok) {
|
|
631
639
|
const errorBody = await emailChangeRes.json().catch(() => ({}));
|
|
632
640
|
throw new AuthError(
|
|
633
|
-
errorBody.msg
|
|
641
|
+
errorBody.msg ?? `Email change verification failed (${String(emailChangeRes.status)})`,
|
|
634
642
|
emailChangeRes.status
|
|
635
643
|
);
|
|
636
644
|
}
|
|
@@ -692,7 +700,7 @@ var toRoles = (appMeta) => {
|
|
|
692
700
|
var toUser = (userData) => {
|
|
693
701
|
const userMeta = userData.user_metadata ?? {};
|
|
694
702
|
const appMeta = userData.app_metadata ?? {};
|
|
695
|
-
const name = userMeta.full_name
|
|
703
|
+
const name = userMeta.full_name ?? userMeta.name;
|
|
696
704
|
const pictureUrl = userMeta.avatar_url;
|
|
697
705
|
return {
|
|
698
706
|
id: userData.id,
|
|
@@ -718,7 +726,7 @@ var toUser = (userData) => {
|
|
|
718
726
|
var claimsToUser = (claims) => {
|
|
719
727
|
const appMeta = claims.app_metadata ?? {};
|
|
720
728
|
const userMeta = claims.user_metadata ?? {};
|
|
721
|
-
const name = userMeta.full_name
|
|
729
|
+
const name = userMeta.full_name ?? userMeta.name;
|
|
722
730
|
const pictureUrl = userMeta.avatar_url;
|
|
723
731
|
return {
|
|
724
732
|
id: claims.sub ?? "",
|
|
@@ -790,7 +798,7 @@ var getUser = async () => {
|
|
|
790
798
|
}
|
|
791
799
|
triggerNextjsDynamic();
|
|
792
800
|
const identityContext = globalThis.netlifyIdentityContext;
|
|
793
|
-
const serverJwt = identityContext?.token
|
|
801
|
+
const serverJwt = identityContext?.token ?? getServerCookie(NF_JWT_COOKIE);
|
|
794
802
|
if (serverJwt) {
|
|
795
803
|
const identityUrl = resolveIdentityUrl();
|
|
796
804
|
if (identityUrl) {
|
|
@@ -832,6 +840,18 @@ var getSettings = async () => {
|
|
|
832
840
|
}
|
|
833
841
|
};
|
|
834
842
|
|
|
843
|
+
// src/csrf.ts
|
|
844
|
+
var verifyRequestOrigin = (request, options) => {
|
|
845
|
+
const origin = request.headers.get("origin");
|
|
846
|
+
if (!origin) {
|
|
847
|
+
throw new AuthError("Cross-origin request refused: missing Origin header.", 403);
|
|
848
|
+
}
|
|
849
|
+
const allowed = options?.allowedOrigins ?? [new URL(request.url).origin];
|
|
850
|
+
if (!allowed.includes(origin)) {
|
|
851
|
+
throw new AuthError(`Cross-origin request refused: Origin ${origin} did not match an allowed origin.`, 403);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
835
855
|
// src/account.ts
|
|
836
856
|
var resolveCurrentUser = async () => {
|
|
837
857
|
const client = getClient();
|
|
@@ -907,7 +927,7 @@ var verifyEmailChange = async (token) => {
|
|
|
907
927
|
});
|
|
908
928
|
if (!res.ok) {
|
|
909
929
|
const errorBody = await res.json().catch(() => ({}));
|
|
910
|
-
throw new AuthError(errorBody.msg
|
|
930
|
+
throw new AuthError(errorBody.msg ?? `Email change verification failed (${String(res.status)})`, res.status);
|
|
911
931
|
}
|
|
912
932
|
const userData = await res.json();
|
|
913
933
|
const user = toUser(userData);
|
|
@@ -971,7 +991,10 @@ var adminFetch = async (path, options = {}) => {
|
|
|
971
991
|
}
|
|
972
992
|
if (!res.ok) {
|
|
973
993
|
const errorBody = await res.json().catch(() => ({}));
|
|
974
|
-
throw new AuthError(
|
|
994
|
+
throw new AuthError(
|
|
995
|
+
errorBody.msg ?? `Admin request failed (${String(res.status)})`,
|
|
996
|
+
res.status
|
|
997
|
+
);
|
|
975
998
|
}
|
|
976
999
|
return res;
|
|
977
1000
|
};
|
|
@@ -1061,6 +1084,6 @@ var admin = { listUsers, getUser: getUser2, createUser, updateUser: updateUser2,
|
|
|
1061
1084
|
requestPasswordRecovery,
|
|
1062
1085
|
signup,
|
|
1063
1086
|
updateUser,
|
|
1064
|
-
verifyEmailChange
|
|
1087
|
+
verifyEmailChange,
|
|
1088
|
+
verifyRequestOrigin
|
|
1065
1089
|
});
|
|
1066
|
-
//# sourceMappingURL=index.cjs.map
|
|
@@ -278,6 +278,10 @@ declare const onAuthChange: (callback: AuthCallback) => (() => void);
|
|
|
278
278
|
* try/catch. Next.js implements `redirect()` by throwing a special error; wrapping it in
|
|
279
279
|
* try/catch will swallow the redirect.
|
|
280
280
|
*
|
|
281
|
+
* **Server-side CSRF:** When called from a server endpoint, the endpoint must have CSRF
|
|
282
|
+
* protection. If your framework does not check the request's `Origin` by default, call
|
|
283
|
+
* {@link verifyRequestOrigin} at the start of the handler before invoking `login()`.
|
|
284
|
+
*
|
|
281
285
|
* @example
|
|
282
286
|
* ```ts
|
|
283
287
|
* // Next.js server action
|
|
@@ -295,6 +299,11 @@ declare const login: (email: string, password: string) => Promise<User>;
|
|
|
295
299
|
* In that case, no cookies are set and no auth event is emitted.
|
|
296
300
|
*
|
|
297
301
|
* @throws {AuthError} On duplicate email, validation failure, network error, or missing Netlify runtime.
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* **Server-side CSRF:** When called from a server endpoint, the endpoint must have CSRF
|
|
305
|
+
* protection. If your framework does not check the request's `Origin` by default, call
|
|
306
|
+
* {@link verifyRequestOrigin} at the start of the handler before invoking `signup()`.
|
|
298
307
|
*/
|
|
299
308
|
declare const signup: (email: string, password: string, data?: SignupData) => Promise<User>;
|
|
300
309
|
/**
|
|
@@ -304,6 +313,11 @@ declare const signup: (email: string, password: string, data?: SignupData) => Pr
|
|
|
304
313
|
* invalidation request fails. In the browser, emits a `'logout'` event via {@link onAuthChange}.
|
|
305
314
|
*
|
|
306
315
|
* @throws {AuthError} On missing Netlify runtime (server) or logout failure (browser).
|
|
316
|
+
*
|
|
317
|
+
* @remarks
|
|
318
|
+
* **Server-side CSRF:** When called from a server endpoint, the endpoint must have CSRF
|
|
319
|
+
* protection. If your framework does not check the request's `Origin` by default, call
|
|
320
|
+
* {@link verifyRequestOrigin} at the start of the handler before invoking `logout()`.
|
|
307
321
|
*/
|
|
308
322
|
declare const logout: () => Promise<void>;
|
|
309
323
|
/**
|
|
@@ -443,6 +457,63 @@ declare class MissingIdentityError extends Error {
|
|
|
443
457
|
constructor(message?: string);
|
|
444
458
|
}
|
|
445
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Options for {@link verifyRequestOrigin}.
|
|
462
|
+
*/
|
|
463
|
+
interface VerifyRequestOriginOptions {
|
|
464
|
+
/**
|
|
465
|
+
* Origins that are allowed to make state-changing requests to this endpoint.
|
|
466
|
+
*
|
|
467
|
+
* If omitted, the request is only accepted from its own origin (the origin of `request.url`),
|
|
468
|
+
* which is the right default for sites whose login form and login endpoint live on the same origin.
|
|
469
|
+
*
|
|
470
|
+
* Pass an explicit list when you trust additional origins (for example, a separate frontend domain
|
|
471
|
+
* posting to an API on another domain). The list replaces the default, so it must include every
|
|
472
|
+
* origin you want to allow, including the request's own origin if applicable.
|
|
473
|
+
*
|
|
474
|
+
* Each value should be a full origin string with scheme and host: `'https://example.com'`.
|
|
475
|
+
*/
|
|
476
|
+
allowedOrigins?: string[];
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Same-origin check for state-changing requests, can be used to defend against Cross-Site Request
|
|
480
|
+
* Forgery (CSRF) on server-side endpoints that call {@link login}, {@link signup}, or {@link logout}.
|
|
481
|
+
*
|
|
482
|
+
* Compares the incoming request's `Origin` header against the request's own origin (or an explicit
|
|
483
|
+
* allowlist via `options.allowedOrigins`) and throws if they don't match. Call this at the start of
|
|
484
|
+
* any server-side handler that performs an auth mutation, before invoking the auth function.
|
|
485
|
+
*
|
|
486
|
+
* The check runs unconditionally on every call: any HTTP method, with or without an `Origin` header.
|
|
487
|
+
* If you don't want the check to apply to a given method or path, simply don't call the helper there.
|
|
488
|
+
*
|
|
489
|
+
* @throws {AuthError} with status `403` when the request has no `Origin` header.
|
|
490
|
+
* @throws {AuthError} with status `403` when the request's `Origin` is not in the allowed origins.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* // Netlify Function
|
|
495
|
+
* import { login, verifyRequestOrigin } from '@netlify/identity'
|
|
496
|
+
* import type { Context } from '@netlify/functions'
|
|
497
|
+
*
|
|
498
|
+
* export default async (req: Request, context: Context) => {
|
|
499
|
+
* verifyRequestOrigin(req)
|
|
500
|
+
* const { email, password } = await req.json()
|
|
501
|
+
* await login(email, password)
|
|
502
|
+
* return new Response(null, { status: 302, headers: { Location: '/dashboard' } })
|
|
503
|
+
* }
|
|
504
|
+
* ```
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* // Allow a separate trusted origin (e.g. a marketing site posting to an app domain).
|
|
509
|
+
* // The list replaces the default, so include the request's own origin if you still want it allowed.
|
|
510
|
+
* verifyRequestOrigin(request, {
|
|
511
|
+
* allowedOrigins: ['https://app.example.com', 'https://www.example.com'],
|
|
512
|
+
* })
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
515
|
+
declare const verifyRequestOrigin: (request: Request, options?: VerifyRequestOriginOptions) => void;
|
|
516
|
+
|
|
446
517
|
/**
|
|
447
518
|
* The admin namespace for privileged user management operations.
|
|
448
519
|
* All methods are server-only and require the operator token
|
|
@@ -546,4 +617,4 @@ declare const verifyEmailChange: (token: string) => Promise<User>;
|
|
|
546
617
|
*/
|
|
547
618
|
declare const updateUser: (updates: UserUpdates) => Promise<User>;
|
|
548
619
|
|
|
549
|
-
export { AUTH_EVENTS, type Admin, type AdminUserUpdates, type AppMetadata, type AuthCallback, AuthError, type AuthEvent, type AuthProvider, type CallbackResult, type CreateUserParams, type IdentityConfig, type ListUsersOptions, MissingIdentityError, type Settings, type SignupData, type User, type UserUpdates, acceptInvite, admin, confirmEmail, getIdentityConfig, getSettings, getUser, handleAuthCallback, hydrateSession, isAuthenticated, login, logout, oauthLogin, onAuthChange, recoverPassword, refreshSession, requestPasswordRecovery, signup, updateUser, verifyEmailChange };
|
|
620
|
+
export { AUTH_EVENTS, type Admin, type AdminUserUpdates, type AppMetadata, type AuthCallback, AuthError, type AuthEvent, type AuthProvider, type CallbackResult, type CreateUserParams, type IdentityConfig, type ListUsersOptions, MissingIdentityError, type Settings, type SignupData, type User, type UserUpdates, type VerifyRequestOriginOptions, acceptInvite, admin, confirmEmail, getIdentityConfig, getSettings, getUser, handleAuthCallback, hydrateSession, isAuthenticated, login, logout, oauthLogin, onAuthChange, recoverPassword, refreshSession, requestPasswordRecovery, signup, updateUser, verifyEmailChange, verifyRequestOrigin };
|
|
@@ -278,6 +278,10 @@ declare const onAuthChange: (callback: AuthCallback) => (() => void);
|
|
|
278
278
|
* try/catch. Next.js implements `redirect()` by throwing a special error; wrapping it in
|
|
279
279
|
* try/catch will swallow the redirect.
|
|
280
280
|
*
|
|
281
|
+
* **Server-side CSRF:** When called from a server endpoint, the endpoint must have CSRF
|
|
282
|
+
* protection. If your framework does not check the request's `Origin` by default, call
|
|
283
|
+
* {@link verifyRequestOrigin} at the start of the handler before invoking `login()`.
|
|
284
|
+
*
|
|
281
285
|
* @example
|
|
282
286
|
* ```ts
|
|
283
287
|
* // Next.js server action
|
|
@@ -295,6 +299,11 @@ declare const login: (email: string, password: string) => Promise<User>;
|
|
|
295
299
|
* In that case, no cookies are set and no auth event is emitted.
|
|
296
300
|
*
|
|
297
301
|
* @throws {AuthError} On duplicate email, validation failure, network error, or missing Netlify runtime.
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* **Server-side CSRF:** When called from a server endpoint, the endpoint must have CSRF
|
|
305
|
+
* protection. If your framework does not check the request's `Origin` by default, call
|
|
306
|
+
* {@link verifyRequestOrigin} at the start of the handler before invoking `signup()`.
|
|
298
307
|
*/
|
|
299
308
|
declare const signup: (email: string, password: string, data?: SignupData) => Promise<User>;
|
|
300
309
|
/**
|
|
@@ -304,6 +313,11 @@ declare const signup: (email: string, password: string, data?: SignupData) => Pr
|
|
|
304
313
|
* invalidation request fails. In the browser, emits a `'logout'` event via {@link onAuthChange}.
|
|
305
314
|
*
|
|
306
315
|
* @throws {AuthError} On missing Netlify runtime (server) or logout failure (browser).
|
|
316
|
+
*
|
|
317
|
+
* @remarks
|
|
318
|
+
* **Server-side CSRF:** When called from a server endpoint, the endpoint must have CSRF
|
|
319
|
+
* protection. If your framework does not check the request's `Origin` by default, call
|
|
320
|
+
* {@link verifyRequestOrigin} at the start of the handler before invoking `logout()`.
|
|
307
321
|
*/
|
|
308
322
|
declare const logout: () => Promise<void>;
|
|
309
323
|
/**
|
|
@@ -443,6 +457,63 @@ declare class MissingIdentityError extends Error {
|
|
|
443
457
|
constructor(message?: string);
|
|
444
458
|
}
|
|
445
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Options for {@link verifyRequestOrigin}.
|
|
462
|
+
*/
|
|
463
|
+
interface VerifyRequestOriginOptions {
|
|
464
|
+
/**
|
|
465
|
+
* Origins that are allowed to make state-changing requests to this endpoint.
|
|
466
|
+
*
|
|
467
|
+
* If omitted, the request is only accepted from its own origin (the origin of `request.url`),
|
|
468
|
+
* which is the right default for sites whose login form and login endpoint live on the same origin.
|
|
469
|
+
*
|
|
470
|
+
* Pass an explicit list when you trust additional origins (for example, a separate frontend domain
|
|
471
|
+
* posting to an API on another domain). The list replaces the default, so it must include every
|
|
472
|
+
* origin you want to allow, including the request's own origin if applicable.
|
|
473
|
+
*
|
|
474
|
+
* Each value should be a full origin string with scheme and host: `'https://example.com'`.
|
|
475
|
+
*/
|
|
476
|
+
allowedOrigins?: string[];
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Same-origin check for state-changing requests, can be used to defend against Cross-Site Request
|
|
480
|
+
* Forgery (CSRF) on server-side endpoints that call {@link login}, {@link signup}, or {@link logout}.
|
|
481
|
+
*
|
|
482
|
+
* Compares the incoming request's `Origin` header against the request's own origin (or an explicit
|
|
483
|
+
* allowlist via `options.allowedOrigins`) and throws if they don't match. Call this at the start of
|
|
484
|
+
* any server-side handler that performs an auth mutation, before invoking the auth function.
|
|
485
|
+
*
|
|
486
|
+
* The check runs unconditionally on every call: any HTTP method, with or without an `Origin` header.
|
|
487
|
+
* If you don't want the check to apply to a given method or path, simply don't call the helper there.
|
|
488
|
+
*
|
|
489
|
+
* @throws {AuthError} with status `403` when the request has no `Origin` header.
|
|
490
|
+
* @throws {AuthError} with status `403` when the request's `Origin` is not in the allowed origins.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* // Netlify Function
|
|
495
|
+
* import { login, verifyRequestOrigin } from '@netlify/identity'
|
|
496
|
+
* import type { Context } from '@netlify/functions'
|
|
497
|
+
*
|
|
498
|
+
* export default async (req: Request, context: Context) => {
|
|
499
|
+
* verifyRequestOrigin(req)
|
|
500
|
+
* const { email, password } = await req.json()
|
|
501
|
+
* await login(email, password)
|
|
502
|
+
* return new Response(null, { status: 302, headers: { Location: '/dashboard' } })
|
|
503
|
+
* }
|
|
504
|
+
* ```
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* // Allow a separate trusted origin (e.g. a marketing site posting to an app domain).
|
|
509
|
+
* // The list replaces the default, so include the request's own origin if you still want it allowed.
|
|
510
|
+
* verifyRequestOrigin(request, {
|
|
511
|
+
* allowedOrigins: ['https://app.example.com', 'https://www.example.com'],
|
|
512
|
+
* })
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
515
|
+
declare const verifyRequestOrigin: (request: Request, options?: VerifyRequestOriginOptions) => void;
|
|
516
|
+
|
|
446
517
|
/**
|
|
447
518
|
* The admin namespace for privileged user management operations.
|
|
448
519
|
* All methods are server-only and require the operator token
|
|
@@ -546,4 +617,4 @@ declare const verifyEmailChange: (token: string) => Promise<User>;
|
|
|
546
617
|
*/
|
|
547
618
|
declare const updateUser: (updates: UserUpdates) => Promise<User>;
|
|
548
619
|
|
|
549
|
-
export { AUTH_EVENTS, type Admin, type AdminUserUpdates, type AppMetadata, type AuthCallback, AuthError, type AuthEvent, type AuthProvider, type CallbackResult, type CreateUserParams, type IdentityConfig, type ListUsersOptions, MissingIdentityError, type Settings, type SignupData, type User, type UserUpdates, acceptInvite, admin, confirmEmail, getIdentityConfig, getSettings, getUser, handleAuthCallback, hydrateSession, isAuthenticated, login, logout, oauthLogin, onAuthChange, recoverPassword, refreshSession, requestPasswordRecovery, signup, updateUser, verifyEmailChange };
|
|
620
|
+
export { AUTH_EVENTS, type Admin, type AdminUserUpdates, type AppMetadata, type AuthCallback, AuthError, type AuthEvent, type AuthProvider, type CallbackResult, type CreateUserParams, type IdentityConfig, type ListUsersOptions, MissingIdentityError, type Settings, type SignupData, type User, type UserUpdates, type VerifyRequestOriginOptions, acceptInvite, admin, confirmEmail, getIdentityConfig, getSettings, getUser, handleAuthCallback, hydrateSession, isAuthenticated, login, logout, oauthLogin, onAuthChange, recoverPassword, refreshSession, requestPasswordRecovery, signup, updateUser, verifyEmailChange, verifyRequestOrigin };
|
|
@@ -99,7 +99,7 @@ var NF_JWT_COOKIE = "nf_jwt";
|
|
|
99
99
|
var NF_REFRESH_COOKIE = "nf_refresh";
|
|
100
100
|
var getCookie = (name) => {
|
|
101
101
|
if (typeof document === "undefined") return null;
|
|
102
|
-
const match =
|
|
102
|
+
const match = new RegExp(`(?:^|; )${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]*)`).exec(document.cookie);
|
|
103
103
|
if (!match) return null;
|
|
104
104
|
try {
|
|
105
105
|
return decodeURIComponent(match[1]);
|
|
@@ -181,13 +181,15 @@ var triggerNextjsDynamic = () => {
|
|
|
181
181
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
182
182
|
var fetchWithTimeout = async (url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) => {
|
|
183
183
|
const controller = new AbortController();
|
|
184
|
-
const timer = setTimeout(() =>
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
controller.abort();
|
|
186
|
+
}, timeoutMs);
|
|
185
187
|
try {
|
|
186
188
|
return await fetch(url, { ...options, signal: controller.signal });
|
|
187
189
|
} catch (error) {
|
|
188
190
|
if (error instanceof Error && error.name === "AbortError") {
|
|
189
191
|
const pathname = new URL(url).pathname;
|
|
190
|
-
throw new AuthError(`Identity request to ${pathname} timed out after ${timeoutMs}ms`);
|
|
192
|
+
throw new AuthError(`Identity request to ${pathname} timed out after ${String(timeoutMs)}ms`);
|
|
191
193
|
}
|
|
192
194
|
throw error;
|
|
193
195
|
} finally {
|
|
@@ -254,16 +256,18 @@ var startTokenRefresh = () => {
|
|
|
254
256
|
const nowS = Math.floor(Date.now() / 1e3);
|
|
255
257
|
const expiresAtS = typeof token.expires_at === "number" && token.expires_at > 1e12 ? Math.floor(token.expires_at / 1e3) : token.expires_at;
|
|
256
258
|
const delayMs = Math.max(0, expiresAtS - nowS - REFRESH_MARGIN_S) * 1e3;
|
|
257
|
-
refreshTimer = setTimeout(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
refreshTimer = setTimeout(() => {
|
|
260
|
+
void (async () => {
|
|
261
|
+
try {
|
|
262
|
+
const freshJwt = await user.jwt(true);
|
|
263
|
+
const freshDetails = user.tokenDetails();
|
|
264
|
+
setBrowserAuthCookies(freshJwt, freshDetails?.refresh_token);
|
|
265
|
+
emitAuthEvent(AUTH_EVENTS.TOKEN_REFRESH, toUser(user));
|
|
266
|
+
startTokenRefresh();
|
|
267
|
+
} catch {
|
|
268
|
+
stopTokenRefresh();
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
267
271
|
}, delayMs);
|
|
268
272
|
};
|
|
269
273
|
var stopTokenRefresh = () => {
|
|
@@ -329,7 +333,7 @@ var refreshSession = async () => {
|
|
|
329
333
|
}
|
|
330
334
|
return null;
|
|
331
335
|
}
|
|
332
|
-
throw new AuthError(errorBody.msg
|
|
336
|
+
throw new AuthError(errorBody.msg ?? `Token refresh failed (${String(res.status)})`, res.status);
|
|
333
337
|
}
|
|
334
338
|
const data = await res.json();
|
|
335
339
|
const cookies = globalThis.Netlify?.context?.cookies;
|
|
@@ -376,7 +380,10 @@ var login = async (email, password) => {
|
|
|
376
380
|
}
|
|
377
381
|
if (!res.ok) {
|
|
378
382
|
const errorBody = await res.json().catch(() => ({}));
|
|
379
|
-
throw new AuthError(
|
|
383
|
+
throw new AuthError(
|
|
384
|
+
errorBody.msg ?? errorBody.error_description ?? `Login failed (${String(res.status)})`,
|
|
385
|
+
res.status
|
|
386
|
+
);
|
|
380
387
|
}
|
|
381
388
|
const data = await res.json();
|
|
382
389
|
const accessToken = data.access_token;
|
|
@@ -390,7 +397,7 @@ var login = async (email, password) => {
|
|
|
390
397
|
}
|
|
391
398
|
if (!userRes.ok) {
|
|
392
399
|
const errorBody = await userRes.json().catch(() => ({}));
|
|
393
|
-
throw new AuthError(errorBody.msg
|
|
400
|
+
throw new AuthError(errorBody.msg ?? `Failed to fetch user data (${String(userRes.status)})`, userRes.status);
|
|
394
401
|
}
|
|
395
402
|
const userData = await userRes.json();
|
|
396
403
|
const user = toUser(userData);
|
|
@@ -426,7 +433,7 @@ var signup = async (email, password, data) => {
|
|
|
426
433
|
}
|
|
427
434
|
if (!res.ok) {
|
|
428
435
|
const errorBody = await res.json().catch(() => ({}));
|
|
429
|
-
throw new AuthError(errorBody.msg
|
|
436
|
+
throw new AuthError(errorBody.msg ?? `Signup failed (${String(res.status)})`, res.status);
|
|
430
437
|
}
|
|
431
438
|
const responseData = await res.json();
|
|
432
439
|
const user = toUser(responseData);
|
|
@@ -580,7 +587,7 @@ var handleEmailChangeCallback = async (client, emailChangeToken) => {
|
|
|
580
587
|
if (!emailChangeRes.ok) {
|
|
581
588
|
const errorBody = await emailChangeRes.json().catch(() => ({}));
|
|
582
589
|
throw new AuthError(
|
|
583
|
-
errorBody.msg
|
|
590
|
+
errorBody.msg ?? `Email change verification failed (${String(emailChangeRes.status)})`,
|
|
584
591
|
emailChangeRes.status
|
|
585
592
|
);
|
|
586
593
|
}
|
|
@@ -642,7 +649,7 @@ var toRoles = (appMeta) => {
|
|
|
642
649
|
var toUser = (userData) => {
|
|
643
650
|
const userMeta = userData.user_metadata ?? {};
|
|
644
651
|
const appMeta = userData.app_metadata ?? {};
|
|
645
|
-
const name = userMeta.full_name
|
|
652
|
+
const name = userMeta.full_name ?? userMeta.name;
|
|
646
653
|
const pictureUrl = userMeta.avatar_url;
|
|
647
654
|
return {
|
|
648
655
|
id: userData.id,
|
|
@@ -668,7 +675,7 @@ var toUser = (userData) => {
|
|
|
668
675
|
var claimsToUser = (claims) => {
|
|
669
676
|
const appMeta = claims.app_metadata ?? {};
|
|
670
677
|
const userMeta = claims.user_metadata ?? {};
|
|
671
|
-
const name = userMeta.full_name
|
|
678
|
+
const name = userMeta.full_name ?? userMeta.name;
|
|
672
679
|
const pictureUrl = userMeta.avatar_url;
|
|
673
680
|
return {
|
|
674
681
|
id: claims.sub ?? "",
|
|
@@ -740,7 +747,7 @@ var getUser = async () => {
|
|
|
740
747
|
}
|
|
741
748
|
triggerNextjsDynamic();
|
|
742
749
|
const identityContext = globalThis.netlifyIdentityContext;
|
|
743
|
-
const serverJwt = identityContext?.token
|
|
750
|
+
const serverJwt = identityContext?.token ?? getServerCookie(NF_JWT_COOKIE);
|
|
744
751
|
if (serverJwt) {
|
|
745
752
|
const identityUrl = resolveIdentityUrl();
|
|
746
753
|
if (identityUrl) {
|
|
@@ -782,6 +789,18 @@ var getSettings = async () => {
|
|
|
782
789
|
}
|
|
783
790
|
};
|
|
784
791
|
|
|
792
|
+
// src/csrf.ts
|
|
793
|
+
var verifyRequestOrigin = (request, options) => {
|
|
794
|
+
const origin = request.headers.get("origin");
|
|
795
|
+
if (!origin) {
|
|
796
|
+
throw new AuthError("Cross-origin request refused: missing Origin header.", 403);
|
|
797
|
+
}
|
|
798
|
+
const allowed = options?.allowedOrigins ?? [new URL(request.url).origin];
|
|
799
|
+
if (!allowed.includes(origin)) {
|
|
800
|
+
throw new AuthError(`Cross-origin request refused: Origin ${origin} did not match an allowed origin.`, 403);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
785
804
|
// src/account.ts
|
|
786
805
|
var resolveCurrentUser = async () => {
|
|
787
806
|
const client = getClient();
|
|
@@ -857,7 +876,7 @@ var verifyEmailChange = async (token) => {
|
|
|
857
876
|
});
|
|
858
877
|
if (!res.ok) {
|
|
859
878
|
const errorBody = await res.json().catch(() => ({}));
|
|
860
|
-
throw new AuthError(errorBody.msg
|
|
879
|
+
throw new AuthError(errorBody.msg ?? `Email change verification failed (${String(res.status)})`, res.status);
|
|
861
880
|
}
|
|
862
881
|
const userData = await res.json();
|
|
863
882
|
const user = toUser(userData);
|
|
@@ -921,7 +940,10 @@ var adminFetch = async (path, options = {}) => {
|
|
|
921
940
|
}
|
|
922
941
|
if (!res.ok) {
|
|
923
942
|
const errorBody = await res.json().catch(() => ({}));
|
|
924
|
-
throw new AuthError(
|
|
943
|
+
throw new AuthError(
|
|
944
|
+
errorBody.msg ?? `Admin request failed (${String(res.status)})`,
|
|
945
|
+
res.status
|
|
946
|
+
);
|
|
925
947
|
}
|
|
926
948
|
return res;
|
|
927
949
|
};
|
|
@@ -1010,6 +1032,6 @@ export {
|
|
|
1010
1032
|
requestPasswordRecovery,
|
|
1011
1033
|
signup,
|
|
1012
1034
|
updateUser,
|
|
1013
|
-
verifyEmailChange
|
|
1035
|
+
verifyEmailChange,
|
|
1036
|
+
verifyRequestOrigin
|
|
1014
1037
|
};
|
|
1015
|
-
//# sourceMappingURL=index.js.map
|