@netlify/identity 1.0.0 → 1.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 +70 -3
- package/dist/index.cjs +16 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -1
- package/dist/index.d.ts +72 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,10 +27,11 @@ If you need a pre-built login UI, the widget still works. For everything else (c
|
|
|
27
27
|
- [Installation](#installation)
|
|
28
28
|
- [Quick start](#quick-start)
|
|
29
29
|
- [API](#api)
|
|
30
|
-
- [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`, `onAuthChange`, `hydrateSession`, `refreshSession`, and more
|
|
30
|
+
- [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`, `onAuthChange`, `hydrateSession`, `refreshSession`, `verifyRequestOrigin`, and more
|
|
31
31
|
- [Admin Operations](#admin-operations) -- `admin.listUsers`, `admin.getUser`, `admin.createUser`, `admin.updateUser`, `admin.deleteUser`
|
|
32
|
-
- [Types](#types) -- `User`, `AuthEvent`, `CallbackResult`, `Settings`, `Admin`, `ListUsersOptions`, `CreateUserParams`, etc.
|
|
32
|
+
- [Types](#types) -- `User`, `AuthEvent`, `CallbackResult`, `Settings`, `Admin`, `ListUsersOptions`, `CreateUserParams`, `VerifyRequestOriginOptions`, etc.
|
|
33
33
|
- [Errors](#errors) -- `AuthError`, `MissingIdentityError`
|
|
34
|
+
- [Security: CSRF protection](#security-csrf-protection)
|
|
34
35
|
- [Framework integration](#framework-integration) -- Next.js, Remix, TanStack Start, Astro, SvelteKit
|
|
35
36
|
- [Guides](#guides)
|
|
36
37
|
- [React `useAuth` hook](#react-useauth-hook)
|
|
@@ -242,6 +243,20 @@ export async function onRequest(context, next) {
|
|
|
242
243
|
}
|
|
243
244
|
```
|
|
244
245
|
|
|
246
|
+
#### `verifyRequestOrigin`
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
verifyRequestOrigin(request: Request, options?: VerifyRequestOriginOptions): void
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
CSRF protection helper for server-side endpoints that call `login()`, `signup()`, or `logout()`. Compares the request's `Origin` header against the request's own origin (or an explicit allowlist via `options.allowedOrigins`) and throws if they don't match. Server-only.
|
|
253
|
+
|
|
254
|
+
The check runs unconditionally on every call: any HTTP method, with or without an `Origin` header. If you don't want the check on a particular route, don't call the helper there.
|
|
255
|
+
|
|
256
|
+
**Throws:** `AuthError` with status `403` when the request has no `Origin` header. `AuthError` with status `403` when the request's `Origin` is not in the allowed origins.
|
|
257
|
+
|
|
258
|
+
See [Security: CSRF protection](#security-csrf-protection) for the full threat model and per-framework guidance.
|
|
259
|
+
|
|
245
260
|
#### `requestPasswordRecovery`
|
|
246
261
|
|
|
247
262
|
```ts
|
|
@@ -569,6 +584,16 @@ The `token` field is only present for `invite` callbacks, where the user hasn't
|
|
|
569
584
|
|
|
570
585
|
For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is not set.
|
|
571
586
|
|
|
587
|
+
#### `VerifyRequestOriginOptions`
|
|
588
|
+
|
|
589
|
+
```ts
|
|
590
|
+
interface VerifyRequestOriginOptions {
|
|
591
|
+
allowedOrigins?: string[]
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Options for [`verifyRequestOrigin`](#verifyrequestorigin). When `allowedOrigins` is set, the list replaces the default same-origin check, so include the request's own origin if you still want it allowed. Each value is a full origin string with scheme and host (`'https://example.com'`).
|
|
596
|
+
|
|
572
597
|
### Errors
|
|
573
598
|
|
|
574
599
|
#### `AuthError`
|
|
@@ -588,6 +613,45 @@ class MissingIdentityError extends Error {}
|
|
|
588
613
|
|
|
589
614
|
Thrown when Identity is not configured in the current environment.
|
|
590
615
|
|
|
616
|
+
## Security: CSRF protection
|
|
617
|
+
|
|
618
|
+
If you expose server-side `login()`, `signup()`, or `logout()` through an HTTP endpoint, that endpoint needs Cross-Site Request Forgery (CSRF) protection. The library cannot enforce this itself because it only sees the email and password arguments handed to it, not the incoming request.
|
|
619
|
+
|
|
620
|
+
**Why it matters.** A specific flavor called _login CSRF_ lets an attacker trick a victim's browser into logging into the attacker's account. The victim then performs actions inside that session (saving payment info, linking third-party services, uploading content), and the attacker harvests the result later by signing in with the credentials they always controlled. `SameSite=Lax` cookies do not catch this attack because the session is being created on the victim's browser, not ridden from an existing one.
|
|
621
|
+
|
|
622
|
+
### `verifyRequestOrigin`
|
|
623
|
+
|
|
624
|
+
`verifyRequestOrigin(request, options?)` compares the request's `Origin` header against the request's own origin (or an explicit allowlist) and throws `AuthError` with status 403 on mismatch. Call it at the start of any handler that performs an auth mutation.
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
// netlify/functions/login.ts
|
|
628
|
+
import { login, verifyRequestOrigin } from '@netlify/identity'
|
|
629
|
+
import type { Context } from '@netlify/functions'
|
|
630
|
+
|
|
631
|
+
export default async (req: Request, context: Context) => {
|
|
632
|
+
verifyRequestOrigin(req)
|
|
633
|
+
const { email, password } = await req.json()
|
|
634
|
+
await login(email, password)
|
|
635
|
+
return new Response(null, { status: 302, headers: { Location: '/dashboard' } })
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
The helper runs unconditionally on every call. It checks any HTTP method, with or without an `Origin` header. If you don't want the check on a particular route, don't call the helper there.
|
|
640
|
+
|
|
641
|
+
### Custom allowed origins
|
|
642
|
+
|
|
643
|
+
By default, the helper accepts only the request's own origin. Pass `allowedOrigins` to allow additional trusted origins (for example, a separate frontend domain that POSTs to an API on another domain). The list replaces the default, so include the request's own origin if you still want it allowed:
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
verifyRequestOrigin(req, {
|
|
647
|
+
allowedOrigins: ['https://app.example.com', 'https://www.example.com'],
|
|
648
|
+
})
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### When to call the helper
|
|
652
|
+
|
|
653
|
+
Some frameworks check the request's `Origin` on state-changing requests by default; others don't. Check your framework's documentation. If same-origin enforcement is already on by default for the endpoint where you invoke `login()` / `signup()` / `logout()`, calling `verifyRequestOrigin` yourself is redundant. If it isn't, call `verifyRequestOrigin(request)` at the start of the handler before invoking the auth function.
|
|
654
|
+
|
|
591
655
|
## Framework integration
|
|
592
656
|
|
|
593
657
|
### Recommended pattern for SSR frameworks
|
|
@@ -661,11 +725,12 @@ Use `window.location.href` instead of Next.js `redirect()` after server-side aut
|
|
|
661
725
|
|
|
662
726
|
```tsx
|
|
663
727
|
// app/routes/login.tsx
|
|
664
|
-
import { login } from '@netlify/identity'
|
|
728
|
+
import { login, verifyRequestOrigin } from '@netlify/identity'
|
|
665
729
|
import { redirect, json } from '@remix-run/node'
|
|
666
730
|
import type { ActionFunctionArgs } from '@remix-run/node'
|
|
667
731
|
|
|
668
732
|
export async function action({ request }: ActionFunctionArgs) {
|
|
733
|
+
verifyRequestOrigin(request)
|
|
669
734
|
const formData = await request.formData()
|
|
670
735
|
const email = formData.get('email') as string
|
|
671
736
|
const password = formData.get('password') as string
|
|
@@ -693,6 +758,8 @@ export async function loader() {
|
|
|
693
758
|
|
|
694
759
|
Remix `redirect()` works after server-side `login()` because Remix actions return real HTTP responses. The browser receives a 302 with the `Set-Cookie` header already applied, so the next request includes the auth cookie. This is different from Next.js, where `redirect()` in a Server Action triggers a client-side (soft) navigation that may not include newly-set cookies.
|
|
695
760
|
|
|
761
|
+
> The example calls [`verifyRequestOrigin`](#verifyrequestorigin) at the top of the action. See [Security: CSRF protection](#security-csrf-protection) for when this is needed.
|
|
762
|
+
|
|
696
763
|
### TanStack Start
|
|
697
764
|
|
|
698
765
|
**Login from the browser (recommended):**
|
package/dist/index.cjs
CHANGED
|
@@ -51,7 +51,8 @@ __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
57
|
module.exports = __toCommonJS(index_exports);
|
|
57
58
|
|
|
@@ -832,6 +833,18 @@ var getSettings = async () => {
|
|
|
832
833
|
}
|
|
833
834
|
};
|
|
834
835
|
|
|
836
|
+
// src/csrf.ts
|
|
837
|
+
var verifyRequestOrigin = (request, options) => {
|
|
838
|
+
const origin = request.headers.get("origin");
|
|
839
|
+
if (!origin) {
|
|
840
|
+
throw new AuthError("Cross-origin request refused: missing Origin header.", 403);
|
|
841
|
+
}
|
|
842
|
+
const allowed = options?.allowedOrigins ?? [new URL(request.url).origin];
|
|
843
|
+
if (!allowed.includes(origin)) {
|
|
844
|
+
throw new AuthError(`Cross-origin request refused: Origin ${origin} did not match an allowed origin.`, 403);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
835
848
|
// src/account.ts
|
|
836
849
|
var resolveCurrentUser = async () => {
|
|
837
850
|
const client = getClient();
|
|
@@ -1061,6 +1074,7 @@ var admin = { listUsers, getUser: getUser2, createUser, updateUser: updateUser2,
|
|
|
1061
1074
|
requestPasswordRecovery,
|
|
1062
1075
|
signup,
|
|
1063
1076
|
updateUser,
|
|
1064
|
-
verifyEmailChange
|
|
1077
|
+
verifyEmailChange,
|
|
1078
|
+
verifyRequestOrigin
|
|
1065
1079
|
});
|
|
1066
1080
|
//# sourceMappingURL=index.cjs.map
|