UNPKG

16.1 kB Markdown View Raw
1---
2title: Sessions and Cookies
3---
4
5# Sessions and Cookies
6
7[MODES: framework, data]
8
9## Sessions
10
11Sessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users "log in", including social, e-commerce, business, and educational websites.
12
13When using React Router as your framework, sessions are managed on a per-route basis (rather than something like express middleware) in your `loader` and `action` methods using a "session storage" object (that implements the [`SessionStorage`][session-storage] interface). Session storage understands how to parse and generate cookies, and how to store session data in a database or filesystem.
14
15### Using Sessions
16
17This is an example of a cookie session storage:
18
19```ts filename=app/sessions.server.ts
20import { createCookieSessionStorage } from "react-router";
21
22type SessionData = {
23 userId: string;
24};
25
26type SessionFlashData = {
27 error: string;
28};
29
30const { getSession, commitSession, destroySession } =
31 createCookieSessionStorage<SessionData, SessionFlashData>(
32 {
33 // a Cookie from `createCookie` or the CookieOptions to create one
34 cookie: {
35 name: "__session",
36
37 // all of these are optional
38 domain: "reactrouter.com",
39 // Expires can also be set (although maxAge overrides it when used in combination).
40 // Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
41 //
42 // expires: new Date(Date.now() + 60_000),
43 httpOnly: true,
44 maxAge: 60,
45 path: "/",
46 sameSite: "lax",
47 secrets: ["s3cret1"],
48 secure: true,
49 },
50 },
51 );
52
53export { getSession, commitSession, destroySession };
54```
55
56We recommend setting up your session storage object in `app/sessions.server.ts` so all routes that need to access session data can import from the same spot.
57
58The input/output to a session storage object are HTTP cookies. `getSession()` retrieves the current session from the incoming request's `Cookie` header, and `commitSession()`/`destroySession()` provide the `Set-Cookie` header for the outgoing response.
59
60You'll use methods to get access to sessions in your `loader` and `action` functions.
61
62After retrieving a session with `getSession`, the returned session object has a handful of methods and properties:
63
64```tsx
65export async function action({
66 request,
67}: ActionFunctionArgs) {
68 const session = await getSession(
69 request.headers.get("Cookie"),
70 );
71 session.get("foo");
72 session.has("bar");
73 // etc.
74}
75```
76
77See the [Session API][session-api] for all methods available on the session object.
78
79### Login form example
80
81A login form might look something like this:
82
83```tsx filename=app/routes/login.tsx lines=[4-7,12-14,16,22,25,33-35,46,51,56,61]
84import { data, redirect } from "react-router";
85import type { Route } from "./+types/login";
86
87import {
88 getSession,
89 commitSession,
90} from "../sessions.server";
91
92export async function loader({
93 request,
94}: Route.LoaderArgs) {
95 const session = await getSession(
96 request.headers.get("Cookie"),
97 );
98
99 if (session.has("userId")) {
100 // Redirect to the home page if they are already signed in.
101 return redirect("/");
102 }
103
104 return data(
105 { error: session.get("error") },
106 {
107 headers: {
108 "Set-Cookie": await commitSession(session),
109 },
110 },
111 );
112}
113
114export async function action({
115 request,
116}: Route.ActionArgs) {
117 const session = await getSession(
118 request.headers.get("Cookie"),
119 );
120 const form = await request.formData();
121 const username = form.get("username");
122 const password = form.get("password");
123
124 const userId = await validateCredentials(
125 username,
126 password,
127 );
128
129 if (userId == null) {
130 session.flash("error", "Invalid username/password");
131
132 // Redirect back to the login page with errors.
133 return redirect("/login", {
134 headers: {
135 "Set-Cookie": await commitSession(session),
136 },
137 });
138 }
139
140 session.set("userId", userId);
141
142 // Login succeeded, send them to the home page.
143 return redirect("/", {
144 headers: {
145 "Set-Cookie": await commitSession(session),
146 },
147 });
148}
149
150export default function Login({
151 loaderData,
152}: Route.ComponentProps) {
153 const { error } = loaderData;
154
155 return (
156 <div>
157 {error ? <div className="error">{error}</div> : null}
158 <form method="POST">
159 <div>
160 <p>Please sign in</p>
161 </div>
162 <label>
163 Username: <input type="text" name="username" />
164 </label>
165 <label>
166 Password:{" "}
167 <input type="password" name="password" />
168 </label>
169 </form>
170 </div>
171 );
172}
173```
174
175And then a logout form might look something like this:
176
177```tsx filename=app/routes/logout.tsx
178import {
179 getSession,
180 destroySession,
181} from "../sessions.server";
182import type { Route } from "./+types/logout";
183
184export async function action({
185 request,
186}: Route.ActionArgs) {
187 const session = await getSession(
188 request.headers.get("Cookie"),
189 );
190 return redirect("/login", {
191 headers: {
192 "Set-Cookie": await destroySession(session),
193 },
194 });
195}
196
197export default function LogoutRoute() {
198 return (
199 <>
200 <p>Are you sure you want to log out?</p>
201 <Form method="post">
202 <button>Logout</button>
203 </Form>
204 <Link to="/">Never mind</Link>
205 </>
206 );
207}
208```
209
210<docs-warning>It's important that you logout (or perform any mutation for that matter) in an `action` and not a `loader`. Otherwise you open your users to [Cross-Site Request Forgery][csrf] attacks.</docs-warning>
211
212### Session Gotchas
213
214Because of nested routes, multiple loaders can be called to construct a single page. When using `session.flash()` or `session.unset()`, you need to be sure no other loaders in the request are going to want to read that, otherwise you'll get race conditions. Typically if you're using flash, you'll want to have a single loader read it, if another loader wants a flash message, use a different key for that loader.
215
216### Creating custom session storage
217
218React Router makes it easy to store sessions in your own database if needed. The [`createSessionStorage()`][create-session-storage] API requires a `cookie` (for options for creating a cookie, see [cookies][cookies]) and a set of create, read, update, and delete (CRUD) methods for managing the session data. The cookie is used to persist the session ID.
219
220- `createData` will be called from `commitSession` on the initial session creation when no session ID exists in the cookie
221- `readData` will be called from `getSession` when a session ID exists in the cookie
222- `updateData` will be called from `commitSession` when a session ID already exists in the cookie
223- `deleteData` is called from `destroySession`
224
225The following example shows how you could do this using a generic database client:
226
227```ts
228import { createSessionStorage } from "react-router";
229
230function createDatabaseSessionStorage({
231 cookie,
232 host,
233 port,
234}) {
235 // Configure your database client...
236 const db = createDatabaseClient(host, port);
237
238 return createSessionStorage({
239 cookie,
240 async createData(data, expires) {
241 // `expires` is a Date after which the data should be considered
242 // invalid. You could use it to invalidate the data somehow or
243 // automatically purge this record from your database.
244 const id = await db.insert(data);
245 return id;
246 },
247 async readData(id) {
248 return (await db.select(id)) || null;
249 },
250 async updateData(id, data, expires) {
251 await db.update(id, data);
252 },
253 async deleteData(id) {
254 await db.delete(id);
255 },
256 });
257}
258```
259
260And then you can use it like this:
261
262```ts
263const { getSession, commitSession, destroySession } =
264 createDatabaseSessionStorage({
265 host: "localhost",
266 port: 1234,
267 cookie: {
268 name: "__session",
269 sameSite: "lax",
270 },
271 });
272```
273
274The `expires` argument to `createData` and `updateData` is the same `Date` at which the cookie itself expires and is no longer valid. You can use this information to automatically purge the session record from your database to save on space, or to ensure that you do not otherwise return any data for old, expired cookies.
275
276### Additional session utils
277
278There are also several other session utilities available if you need them:
279
280- [`isSession`][is-session]
281- [`createMemorySessionStorage`][create-memory-session-storage]
282- [`createSession`][create-session] (custom storage)
283- [`createFileSessionStorage`][create-file-session-storage] (node)
284- [`createWorkersKVSessionStorage`][create-workers-kv-session-storage] (Cloudflare Workers)
285- [`createArcTableSessionStorage`][create-arc-table-session-storage] (architect, Amazon DynamoDB)
286
287## Cookies
288
289A [cookie][cookie] is a small piece of information that your server sends someone in a HTTP response that their browser will send back on subsequent requests. This technique is a fundamental building block of many interactive websites that adds state so you can build authentication (see [sessions][sessions]), shopping carts, user preferences, and many other features that require remembering who is "logged in".
290
291React Router's [`Cookie` interface][cookie-api] provides a logical, reusable container for cookie metadata.
292
293### Using cookies
294
295While you may create these cookies manually, it is more common to use a [session storage][sessions].
296
297In React Router, you will typically work with cookies in your `loader` and/or `action` functions, since those are the places where you need to read and write data.
298
299Let's say you have a banner on your e-commerce site that prompts users to check out the items you currently have on sale. The banner spans the top of your homepage, and includes a button on the side that allows the user to dismiss the banner so they don't see it for at least another week.
300
301First, create a cookie:
302
303```ts filename=app/cookies.server.ts
304import { createCookie } from "react-router";
305
306export const userPrefs = createCookie("user-prefs", {
307 maxAge: 604_800, // one week
308});
309```
310
311Then, you can `import` the cookie and use it in your `loader` and/or `action`. The `loader` in this case just checks the value of the user preference so you can use it in your component for deciding whether to render the banner. When the button is clicked, the `<form>` calls the `action` on the server and reloads the page without the banner.
312
313### User preferences example
314
315```tsx filename=app/routes/home.tsx lines=[4,9-11,18-20,29]
316import { Link, Form, redirect } from "react-router";
317import type { Route } from "./+types/home";
318
319import { userPrefs } from "../cookies.server";
320
321export async function loader({
322 request,
323}: Route.LoaderArgs) {
324 const cookieHeader = request.headers.get("Cookie");
325 const cookie =
326 (await userPrefs.parse(cookieHeader)) || {};
327 return { showBanner: cookie.showBanner };
328}
329
330export async function action({
331 request,
332}: Route.ActionArgs) {
333 const cookieHeader = request.headers.get("Cookie");
334 const cookie =
335 (await userPrefs.parse(cookieHeader)) || {};
336 const bodyParams = await request.formData();
337
338 if (bodyParams.get("bannerVisibility") === "hidden") {
339 cookie.showBanner = false;
340 }
341
342 return redirect("/", {
343 headers: {
344 "Set-Cookie": await userPrefs.serialize(cookie),
345 },
346 });
347}
348
349export default function Home({
350 loaderData,
351}: Route.ComponentProps) {
352 return (
353 <div>
354 {loaderData.showBanner ? (
355 <div>
356 <Link to="/sale">Don't miss our sale!</Link>
357 <Form method="post">
358 <input
359 type="hidden"
360 name="bannerVisibility"
361 value="hidden"
362 />
363 <button type="submit">Hide</button>
364 </Form>
365 </div>
366 ) : null}
367 <h1>Welcome!</h1>
368 </div>
369 );
370}
371```
372
373### Cookie attributes
374
375Cookies have [several attributes][cookie-attrs] that control when they expire, how they are accessed, and where they are sent. Any of these attributes may be specified either in `createCookie(name, options)`, or during `serialize()` when the `Set-Cookie` header is generated.
376
377```ts
378const cookie = createCookie("user-prefs", {
379 // These are defaults for this cookie.
380 path: "/",
381 sameSite: "lax",
382 httpOnly: true,
383 secure: true,
384 expires: new Date(Date.now() + 60_000),
385 maxAge: 60,
386});
387
388// You can either use the defaults:
389cookie.serialize(userPrefs);
390
391// Or override individual ones as needed:
392cookie.serialize(userPrefs, { sameSite: "strict" });
393```
394
395Please read [more info about these attributes][cookie-attrs] to get a better understanding of what they do.
396
397### Signing cookies
398
399It is possible to sign a cookie to automatically verify its contents when it is received. Since it's relatively easy to spoof HTTP headers, this is a good idea for any information that you do not want someone to be able to fake, like authentication information (see [sessions][sessions]).
400
401To sign a cookie, provide one or more `secrets` when you first create the cookie:
402
403```ts
404const cookie = createCookie("user-prefs", {
405 secrets: ["s3cret1"],
406});
407```
408
409Cookies that have one or more `secrets` will be stored and verified in a way that ensures the cookie's integrity.
410
411Secrets may be rotated by adding new secrets to the front of the `secrets` array. Cookies that have been signed with old secrets will still be decoded successfully in `cookie.parse()`, and the newest secret (the first one in the array) will always be used to sign outgoing cookies created in `cookie.serialize()`.
412
413```ts filename=app/cookies.server.ts
414export const cookie = createCookie("user-prefs", {
415 secrets: ["n3wsecr3t", "olds3cret"],
416});
417```
418
419```tsx filename=app/routes/my-route.tsx
420import { data } from "react-router";
421import { cookie } from "../cookies.server";
422import type { Route } from "./+types/my-route";
423
424export async function loader({
425 request,
426}: Route.LoaderArgs) {
427 const oldCookie = request.headers.get("Cookie");
428 // oldCookie may have been signed with "olds3cret", but still parses ok
429 const value = await cookie.parse(oldCookie);
430
431 return data("...", {
432 headers: {
433 // Set-Cookie is signed with "n3wsecr3t"
434 "Set-Cookie": await cookie.serialize(value),
435 },
436 });
437}
438```
439
440### Additional cookie utils
441
442There are also several other cookie utilities available if you need them:
443
444- [`isCookie`][is-cookie]
445- [`createCookie`][create-cookie]
446
447To learn more about each attribute, please see the [MDN Set-Cookie docs][cookie-attrs].
448
449[csrf]: https://developer.mozilla.org/en-US/docs/Glossary/CSRF
450[cookies]: #cookies
451[sessions]: #sessions
452[session-storage]: https://api.reactrouter.com/v7/interfaces/react-router.SessionStorage
453[session-api]: https://api.reactrouter.com/v7/interfaces/react-router.Session
454[is-session]: https://api.reactrouter.com/v7/functions/react-router.isSession
455[cookie-api]: https://api.reactrouter.com/v7/interfaces/react-router.Cookie
456[create-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createSessionStorage
457[create-session]: https://api.reactrouter.com/v7/functions/react-router.createSession
458[create-memory-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createMemorySessionStorage
459[create-file-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_node.createFileSessionStorage
460[create-workers-kv-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_cloudflare.createWorkersKVSessionStorage
461[create-arc-table-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_architect.createArcTableSessionStorage
462[cookie]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
463[cookie-attrs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
464[is-cookie]: https://api.reactrouter.com/v7/functions/react-router.isCookie
465[create-cookie]: https://api.reactrouter.com/v7/functions/react-router.createCookie