├── .editorconfig ├── .eslintrc.yaml ├── .gitattributes ├── .github ├── FUNDING.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── .node-version ├── .npmignore ├── .npmrc ├── .prettierignore ├── .release-it.yaml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── examples └── next │ ├── .env.development │ ├── .env.production │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── background.png │ ├── github-mark-white.svg │ └── github-mark.svg │ ├── src │ ├── app │ │ ├── GitHubLogo.tsx │ │ ├── app-router-client-component-redirect-route-handler-fetch │ │ │ ├── form.tsx │ │ │ ├── lib.ts │ │ │ ├── page.tsx │ │ │ └── session │ │ │ │ └── route.ts │ │ ├── app-router-client-component-route-handler-swr │ │ │ ├── form.tsx │ │ │ ├── lib.ts │ │ │ ├── page.tsx │ │ │ ├── protected-client │ │ │ │ └── page.tsx │ │ │ ├── protected-middleware │ │ │ │ └── page.tsx │ │ │ ├── protected-server │ │ │ │ └── page.tsx │ │ │ ├── session │ │ │ │ └── route.ts │ │ │ └── use-session.ts │ │ ├── app-router-magic-links │ │ │ ├── form.tsx │ │ │ ├── lib.ts │ │ │ ├── magic-login │ │ │ │ └── route.ts │ │ │ ├── page.tsx │ │ │ └── session │ │ │ │ └── route.ts │ │ ├── app-router-server-component-and-action │ │ │ ├── actions.ts │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── lib.ts │ │ │ ├── page.tsx │ │ │ └── submit-button.tsx │ │ ├── css.ts │ │ ├── fathom.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── title.tsx │ ├── get-the-code.tsx │ ├── middleware.ts │ ├── pages-components │ │ ├── pages-router-api-route-swr │ │ │ ├── form.tsx │ │ │ ├── lib.ts │ │ │ └── use-session.ts │ │ └── pages-router-redirect-api-route-fetch │ │ │ ├── form.tsx │ │ │ └── lib.ts │ └── pages │ │ ├── _app.tsx │ │ ├── api │ │ ├── pages-router-api-route-swr │ │ │ └── session.ts │ │ └── pages-router-redirect-api-route-fetch │ │ │ └── session.ts │ │ ├── pages-router-api-route-swr │ │ ├── index.tsx │ │ ├── protected-client │ │ │ └── index.tsx │ │ ├── protected-middleware │ │ │ └── index.tsx │ │ └── protected-server │ │ │ └── index.tsx │ │ └── pages-router-redirect-api-route-fetch │ │ └── index.tsx │ ├── tailwind.config.ts │ └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── sponsor ├── clerk-dark.svg └── clerk-light.svg ├── src ├── core.ts ├── index.test.ts └── index.ts ├── tsconfig.json ├── tsup.config.ts └── turbo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | root: true 2 | env: 3 | node: true 4 | extends: 5 | - plugin:import/recommended 6 | - plugin:import/typescript 7 | - eslint:recommended 8 | - plugin:@typescript-eslint/recommended 9 | - plugin:prettier/recommended 10 | ignorePatterns: 11 | - dist 12 | - coverage 13 | parser: "@typescript-eslint/parser" 14 | parserOptions: 15 | ecmaVersion: 2019 16 | project: 17 | - tsconfig.json 18 | sourceType: module 19 | settings: 20 | import/resolver: 21 | typescript: 22 | alwaysTryTypes: true 23 | project: . 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: [vvo, brc-dd] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | types: [opened, synchronize, reopened] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v2 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version-file: ".node-version" 20 | cache: "pnpm" 21 | - name: Install dependencies 22 | run: pnpm install 23 | - run: pnpm lint 24 | - run: pnpm test 25 | - uses: codecov/codecov-action@v3 26 | with: 27 | files: coverage/lcov.info 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | *.tgz 5 | .DS_Store 6 | coverage* 7 | *.tsbuildinfo 8 | .turbo 9 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.18.3 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | pnpm-lock.yaml 4 | CHANGELOG.md 5 | node_modules 6 | .next 7 | -------------------------------------------------------------------------------- /.release-it.yaml: -------------------------------------------------------------------------------- 1 | git: 2 | commitMessage: 'chore: release v${version}' 3 | github: 4 | release: true 5 | releaseName: v${version} 6 | plugins: 7 | '@release-it/conventional-changelog': 8 | preset: conventionalcommits 9 | infile: CHANGELOG.md 10 | ignoreRecommendedBump: true 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [8.0.0-alpha.0](https://github.com/vvo/iron-session/compare/v6.2.1...v8.0.0-alpha.0) (2023-05-27) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * rewrite (#574) 9 | 10 | ### Features 11 | 12 | * rewrite ([#574](https://github.com/vvo/iron-session/issues/574)) ([ecdd626](https://github.com/vvo/iron-session/commit/ecdd6260641cd9a61c671fd18a7ef980148ca76a)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * handle ttl and max-age properly in case of overriden options in save/destroy calls ([3c00b13](https://github.com/vvo/iron-session/commit/3c00b1325079c594930fda82157deec3a70d1dd7)) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Hey there! We are really excited that you are interested in contributing. Before 4 | submitting your contribution, please make sure to take a moment and read through 5 | the following guide: 6 | 7 | ## Repository Setup 8 | 9 | ```sh 10 | fnm install 11 | corepack enable 12 | pnpm install 13 | ``` 14 | 15 | ## Development 16 | 17 | ```sh 18 | pnpm dev 19 | ``` 20 | 21 | ## Tests 22 | 23 | ```sh 24 | pnpm test 25 | ``` 26 | 27 | ## Submitting a Contribution 28 | 29 | 1. Fork the repository 30 | 2. Create a new branch for your contribution 31 | 3. Make your changes 32 | 4. Submit a pull request 33 | 34 | Some points to keep in mind: 35 | 36 | - Before starting to work on a feature, please make sure to open an issue first 37 | and discuss it with the maintainers. 38 | - If you are working on a bug fix, please provide a detailed description of the 39 | bug in the pull request. 40 | - It's OK to have multiple small commits as you work on the PR - GitHub will 41 | automatically squash them before merging. 42 | - Please make sure the PR title follows 43 | [Conventional Commits](https://www.conventionalcommits.org/) format. 44 | - Please make sure to rebase your branch on top of the latest `main` branch 45 | before submitting the PR. 46 | 47 | Recommendations (not required, may not apply to all PRs): 48 | 49 | - Add tests for your changes. 50 | - Update the documentation if necessary. 51 | 52 | ## License 53 | 54 | When you submit code changes, your submissions are understood to be under 55 | [the same MIT license](LICENSE.md) that covers the project. Feel free to contact 56 | the maintainers if that's a concern. 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vincent Voyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iron-session ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/vvo/iron-session/ci.yaml) [![GitHub license](https://img.shields.io/github/license/vvo/iron-session?style=flat)](https://github.com/vvo/iron-session/blob/master/LICENSE) [![npm](https://img.shields.io/npm/v/iron-session)](https://www.npmjs.com/package/iron-session) ![npm](https://img.shields.io/npm/dm/iron-session) ![npm package minimized gzipped size (select exports)](https://img.shields.io/bundlejs/size/iron-session?exports=getIronSession) 2 | 3 | **`iron-session` is a secure, stateless, and cookie-based session library for JavaScript.** 4 | 5 |
our sponsor:
6 | 7 | --- 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Clerk is a complete suite of embeddable UIs, flexible APIs, and admin dashboards to authenticate and manage your users.

17 |

18 | 19 | Add authentication in 7 minutes 👉 20 | 21 |

22 |
23 | 24 | --- 25 | 26 | The session data is stored in signed and encrypted cookies which are decoded by your server code in a stateless fashion (= no network involved). This is the same technique used by frameworks like 27 | [Ruby On Rails](https://guides.rubyonrails.org/security.html#session-storage). 28 | 29 |

Online demo and examples: https://get-iron-session.vercel.app 👀
30 | Featured in the Next.js documentation ⭐️

31 | 32 | ## Table of Contents 33 | 34 | - [Table of Contents](#table-of-contents) 35 | - [Installation](#installation) 36 | - [Usage](#usage) 37 | - [Examples](#examples) 38 | - [Project status](#project-status) 39 | - [Session options](#session-options) 40 | - [API](#api) 41 | - [`getIronSession(req, res, sessionOptions): Promise>`](#getironsessiontreq-res-sessionoptions-promiseironsessiont) 42 | - [`getIronSession(cookieStore, sessionOptions): Promise>`](#getironsessiontcookiestore-sessionoptions-promiseironsessiont) 43 | - [`session.save(): Promise`](#sessionsave-promisevoid) 44 | - [`session.destroy(): void`](#sessiondestroy-void) 45 | - [`session.updateConfig(sessionOptions: SessionOptions): void`](#sessionupdateconfigsessionoptions-sessionoptions-void) 46 | - [`sealData(data: unknown, { password, ttl }): Promise`](#sealdatadata-unknown--password-ttl--promisestring) 47 | - [`unsealData(seal: string, { password, ttl }): Promise`](#unsealdatatseal-string--password-ttl--promiset) 48 | - [FAQ](#faq) 49 | - [Why use pure cookies for sessions?](#why-use-pure-cookies-for-sessions) 50 | - [How to invalidate sessions?](#how-to-invalidate-sessions) 51 | - [Can I use something else than cookies?](#can-i-use-something-else-than-cookies) 52 | - [How is this different from JWT?](#how-is-this-different-from-jwt) 53 | - [Credits](#credits) 54 | - [Good Reads](#good-reads) 55 | 56 | ## Installation 57 | 58 | ```sh 59 | pnpm add iron-session 60 | ``` 61 | 62 | ## Usage 63 | 64 | *We have extensive examples here too: https://get-iron-session.vercel.app/.* 65 | 66 | To get a session, there's a single method to know: `getIronSession`. 67 | 68 | ```ts 69 | // Next.js API Routes and Node.js/Express/Connect. 70 | import { getIronSession } from 'iron-session'; 71 | 72 | export async function get(req, res) { 73 | const session = await getIronSession(req, res, { password: "...", cookieName: "..." }); 74 | return session; 75 | } 76 | 77 | export async function post(req, res) { 78 | const session = await getIronSession(req, res, { password: "...", cookieName: "..." }); 79 | session.username = "Alison"; 80 | await session.save(); 81 | } 82 | ``` 83 | 84 | ```ts 85 | // Next.js Route Handlers (App Router) 86 | import { cookies } from 'next/headers'; 87 | import { getIronSession } from 'iron-session'; 88 | 89 | export async function GET() { 90 | const session = await getIronSession(cookies(), { password: "...", cookieName: "..." }); 91 | return session; 92 | } 93 | 94 | export async function POST() { 95 | const session = await getIronSession(cookies(), { password: "...", cookieName: "..." }); 96 | session.username = "Alison"; 97 | await session.save(); 98 | } 99 | ``` 100 | 101 | ```tsx 102 | // Next.js Server Components and Server Actions (App Router) 103 | import { cookies } from 'next/headers'; 104 | import { getIronSession } from 'iron-session'; 105 | 106 | async function getIronSessionData() { 107 | const session = await getIronSession(cookies(), { password: "...", cookieName: "..." }); 108 | return session 109 | } 110 | 111 | async function Profile() { 112 | const session = await getIronSessionData(); 113 | 114 | return
{session.username}
; 115 | } 116 | ``` 117 | 118 | ## Examples 119 | 120 | We have many different patterns and examples on the online demo, have a look: https://get-iron-session.vercel.app/. 121 | 122 | ## Project status 123 | 124 | ✅ Production ready and maintained. 125 | 126 | ## Session options 127 | 128 | Two options are required: `password` and `cookieName`. Everything else is automatically computed and usually doesn't need to be changed.**** 129 | 130 | - `password`, **required**: Private key used to encrypt the cookie. It has to be at least 32 characters long. Use to generate strong passwords. `password` can be either a `string` or an `object` with incrementing keys like this: `{2: "...", 1: "..."}` to allow for password rotation. iron-session will use the highest numbered key for new cookies. 131 | - `cookieName`, **required**: Name of the cookie to be stored 132 | - `ttl`, _optional_: In seconds. Default to the equivalent of 14 days. You can set this to `0` and iron-session will compute the maximum allowed value by cookies. 133 | - `cookieOptions`, _optional_: Any option available from [jshttp/cookie#serialize](https://github.com/jshttp/cookie#cookieserializename-value-options) except for `encode` which is not a Set-Cookie Attribute. See [Mozilla Set-Cookie Attributes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes) and [Chrome Cookie Fields](https://developer.chrome.com/docs/devtools/application/cookies/#fields). Default to: 134 | 135 | ```js 136 | { 137 | httpOnly: true, 138 | secure: true, // set this to false in local (non-HTTPS) development 139 | sameSite: "lax",// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax 140 | maxAge: (ttl === 0 ? 2147483647 : ttl) - 60, // Expire cookie before the session expires. 141 | path: "/", 142 | } 143 | ``` 144 | 145 | ## API 146 | 147 | ### `getIronSession(req, res, sessionOptions): Promise>` 148 | 149 | ```ts 150 | type SessionData = { 151 | // Your data 152 | } 153 | 154 | const session = await getIronSession(req, res, sessionOptions); 155 | ``` 156 | 157 | ### `getIronSession(cookieStore, sessionOptions): Promise>` 158 | 159 | ```ts 160 | type SessionData = { 161 | // Your data 162 | } 163 | 164 | const session = await getIronSession(cookies(), sessionOptions); 165 | ``` 166 | 167 | ### `session.save(): Promise` 168 | 169 | Saves the session. This is an asynchronous operation. It must be done and awaited before headers are sent to the client. 170 | 171 | ```ts 172 | await session.save() 173 | ``` 174 | 175 | ### `session.destroy(): void` 176 | 177 | Destroys the session. This is a synchronous operation as it only removes the cookie. It must be done before headers are sent to the client. 178 | 179 | ```ts 180 | session.destroy() 181 | ``` 182 | 183 | ### `session.updateConfig(sessionOptions: SessionOptions): void` 184 | 185 | Updates the configuration of the session with new session options. You still need to call save() if you want them to be applied. 186 | 187 | ### `sealData(data: unknown, { password, ttl }): Promise` 188 | 189 | This is the underlying method and seal mechanism that powers `iron-session`. You can use it to seal any `data` you want and pass it around. One usecase are magic links: you generate a seal that contains a user id to login and send it to a route on your website (like `/magic-login`). Once received, you can safely decode the seal with `unsealData` and log the user in. 190 | 191 | ### `unsealData(seal: string, { password, ttl }): Promise` 192 | 193 | This is the opposite of `sealData` and allow you to decode a seal to get the original data back. 194 | 195 | ## FAQ 196 | 197 | ### Why use pure cookies for sessions? 198 | 199 | This makes your sessions stateless: since the data is passed around in cookies, you do not need any server or service to store session data. 200 | 201 | More information can also be found on the [Ruby On Rails website](https://guides.rubyonrails.org/security.html#session-storage) which uses the same technique. 202 | 203 | ### How to invalidate sessions? 204 | 205 | Sessions cannot be instantly invalidated (or "disconnect this customer") as there is typically no state stored about sessions on the server by default. However, in most applications, the first step upon receiving an authenticated request is to validate the user and their permissions in the database. So, to easily disconnect customers (or invalidate sessions), you can add an `isBlocked`` state in the database and create a UI to block customers. 206 | 207 | Then, every time a request is received that involves reading or altering sensitive data, make sure to check this flag. 208 | 209 | ### Can I use something else than cookies? 210 | 211 | Yes, we expose `sealData` and `unsealData` which are not tied to cookies. This way you can seal and unseal any object in your application and move seals around to login users. 212 | 213 | ### How is this different from [JWT](https://jwt.io/)? 214 | 215 | Not so much: 216 | 217 | - JWT is a standard, it stores metadata in the JWT token themselves to ensure communication between different systems is flawless. 218 | - JWT tokens are not encrypted, the payload is visible by customers if they manage to inspect the seal. You would have to use [JWE](https://tools.ietf.org/html/rfc7516) to achieve the same. 219 | - @hapi/iron mechanism is not a standard, it's a way to sign and encrypt data into seals 220 | 221 | Depending on your own needs and preferences, `iron-session` may or may not fit you. 222 | 223 | ## Credits 224 | 225 | - [Eran Hammer and hapi.js contributors](https://github.com/hapijs/iron/graphs/contributors) 226 | for creating the underlying cryptography library 227 | [`@hapi/iron`](https://hapi.dev/module/iron/). 228 | - [Divyansh Singh](https://github.com/brc-dd) for reimplementing `@hapi/iron` as 229 | [`iron-webcrypto`](https://github.com/brc-dd/iron-webcrypto) using standard 230 | web APIs. 231 | - [Hoang Vo](https://github.com/hoangvvo) for advice and guidance while building 232 | this module. Hoang built 233 | [`next-connect`](https://github.com/hoangvvo/next-connect) and 234 | [`next-session`](https://github.com/hoangvvo/next-session). 235 | - All the 236 | [contributors](https://github.com/vvo/iron-session/graphs/contributors) for 237 | making this project better. 238 | 239 | ## Good Reads 240 | 241 | - 242 | - 243 | -------------------------------------------------------------------------------- /examples/next/.env.development: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=https://localhost:3000 2 | NEXT_PUBLIC_URL=https://localhost:3000 3 | -------------------------------------------------------------------------------- /examples/next/.env.production: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=https://$NEXT_PUBLIC_VERCEL_URL 2 | NEXT_PUBLIC_URL=https://$NEXT_PUBLIC_VERCEL_URL 3 | -------------------------------------------------------------------------------- /examples/next/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "root": true, 4 | "settings": { 5 | "next": { 6 | "rootDir": "examples/next" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/next/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | certificates -------------------------------------------------------------------------------- /examples/next/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | pnpm dev 9 | ``` 10 | 11 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 12 | 13 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 14 | 15 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /examples/next/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --experimental-https", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "fathom-client": "3.6.0", 13 | "iron-session": "workspace:*", 14 | "next": "14.2.3", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "swr": "^2.2.5" 18 | }, 19 | "devDependencies": { 20 | "@tailwindcss/forms": "^0.5.7", 21 | "@types/node": "20.14.2", 22 | "@types/react": "^18.3.3", 23 | "@types/react-dom": "^18.3.0", 24 | "autoprefixer": "^10.4.19", 25 | "eslint": "^8.54.0", 26 | "eslint-config-next": "14.2.3", 27 | "postcss": "^8.4.38", 28 | "tailwindcss": "^3.4.4", 29 | "typescript": "^5.4.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/next/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/next/public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvo/iron-session/abbd341e684598c04b07aa7874052c057927d9c2/examples/next/public/background.png -------------------------------------------------------------------------------- /examples/next/public/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/next/public/github-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/next/src/app/GitHubLogo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default function GitHubLogo() { 4 | return ( 5 | <> 6 | GitHub Logo 13 | GitHub Logo 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-redirect-route-handler-fetch/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as css from "@/app/css"; 4 | 5 | import { useEffect, useState } from "react"; 6 | import { SessionData } from "./lib"; 7 | import { defaultSession } from "./lib"; 8 | 9 | export function Form() { 10 | const [session, setSession] = useState(defaultSession); 11 | const [isLoading, setIsLoading] = useState(true); 12 | 13 | useEffect(() => { 14 | fetch("/app-router-client-component-redirect-route-handler-fetch/session") 15 | .then((res) => res.json()) 16 | .then((session) => { 17 | setSession(session); 18 | setIsLoading(false); 19 | }); 20 | }, []); 21 | 22 | if (isLoading) { 23 | return

Loading...

; 24 | } 25 | 26 | if (session.isLoggedIn) { 27 | return ( 28 | <> 29 |

30 | Logged in user: {session.username} 31 |

32 | 33 | 34 | ); 35 | } 36 | 37 | return ; 38 | } 39 | 40 | function LoginForm() { 41 | return ( 42 |
47 | 61 |
62 | 63 |
64 |
65 | ); 66 | } 67 | 68 | function LogoutButton() { 69 | return ( 70 |

71 | 75 | Logout 76 | 77 |

78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-redirect-route-handler-fetch/lib.ts: -------------------------------------------------------------------------------- 1 | import { SessionOptions } from "iron-session"; 2 | 3 | export interface SessionData { 4 | username: string; 5 | isLoggedIn: boolean; 6 | } 7 | 8 | export const defaultSession: SessionData = { 9 | username: "", 10 | isLoggedIn: false, 11 | }; 12 | 13 | export const sessionOptions: SessionOptions = { 14 | password: "complex_password_at_least_32_characters_long", 15 | cookieName: 16 | "iron-examples-app-router-client-component-redirect-route-handler-fetch", 17 | cookieOptions: { 18 | // secure only works in `https` environments 19 | // if your localhost is not on `https`, then use: `secure: process.env.NODE_ENV === "production"` 20 | secure: true, 21 | }, 22 | }; 23 | 24 | export function sleep(ms: number) { 25 | return new Promise((resolve) => setTimeout(resolve, ms)); 26 | } 27 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-redirect-route-handler-fetch/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import * as css from "@/app/css"; 3 | 4 | import { Metadata } from "next"; 5 | import { Form } from "./form"; 6 | import { Title } from "../title"; 7 | import { GetTheCode } from "../../get-the-code"; 8 | 9 | export const metadata: Metadata = { 10 | title: 11 | "🛠 iron-session examples: Client components, route handlers, redirects and fetch", 12 | }; 13 | 14 | export default function AppRouterRedirect() { 15 | return ( 16 |
17 | 18 | 19 | <p className="italic max-w-xl"> 20 | <u>How to test</u>: Login and refresh the page to see iron-session in 21 | action. 22 | </p> 23 | 24 | <div className="grid grid-cols-1 gap-4 p-10 border border-slate-500 rounded-md max-w-xl"> 25 | <Form /> 26 | </div> 27 | 28 | <GetTheCode path="app/app-router-client-component-redirect-route-handler-fetch" /> 29 | <HowItWorks /> 30 | 31 | <p> 32 | <Link href="/" className={css.link}> 33 | ← All examples 34 | </Link> 35 | </p> 36 | </main> 37 | ); 38 | } 39 | 40 | function HowItWorks() { 41 | return ( 42 | <details className="max-w-2xl space-y-4"> 43 | <summary className="cursor-pointer">How it works</summary> 44 | 45 | <ol className="list-decimal list-inside"> 46 | <li> 47 | The form is submitted to 48 | /app-router-client-component-redirect-route-handler-fetch/session (API 49 | route) via a POST call (non-fetch). The API route sets the session 50 | data and redirects back to 51 | /app-router-client-component-redirect-route-handler-fetch (this page). 52 | </li> 53 | <li> 54 | The page gets the session data via a fetch call to 55 | /app-router-client-component-redirect-route-handler-fetch/session (API 56 | route). The API route either return the session data (logged in) or a 57 | default session (not logged in). 58 | </li> 59 | <li> 60 | The logout is a regular link navigating to 61 | /app-router-client-component-redirect-route-handler-fetch/logout which 62 | destroy the session and redirects back to 63 | /app-router-client-component-redirect-route-handler-fetch (this page). 64 | </li> 65 | </ol> 66 | 67 | <p> 68 | <strong>Pros</strong>: Straightforward. It does not rely on too many 69 | APIs. This is what we would have implemented a few years ago and is good 70 | enough for many websites. 71 | </p> 72 | <p> 73 | <strong>Cons</strong>: No synchronization. The session is not updated 74 | between tabs and windows. If you login or logout in one window or tab, 75 | the others are still showing the previous state. Also, we rely on full 76 | page navigation and redirects for login and logout. We could remove them 77 | by using fetch instead. 78 | </p> 79 | </details> 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-redirect-route-handler-fetch/session/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { cookies } from "next/headers"; 3 | import { getIronSession } from "iron-session"; 4 | import { defaultSession, sessionOptions } from "../lib"; 5 | import { redirect } from "next/navigation"; 6 | import { sleep, SessionData } from "../lib"; 7 | 8 | // /app-router-client-component-redirect-route-handler-fetch/session 9 | export async function POST(request: NextRequest) { 10 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 11 | 12 | const formData = await request.formData(); 13 | 14 | session.isLoggedIn = true; 15 | session.username = (formData.get("username") as string) ?? "No username"; 16 | await session.save(); 17 | 18 | // simulate looking up the user in db 19 | await sleep(250); 20 | 21 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303 22 | // not using redirect() yet: https://github.com/vercel/next.js/issues/51592#issuecomment-1810212676 23 | return Response.redirect( 24 | `${request.nextUrl.origin}/app-router-client-component-redirect-route-handler-fetch`, 25 | 303, 26 | ); 27 | } 28 | 29 | // /app-router-client-component-redirect-route-handler-fetch/session 30 | // /app-router-client-component-redirect-route-handler-fetch/session?action=logout 31 | export async function GET(request: NextRequest) { 32 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 33 | 34 | const action = new URL(request.url).searchParams.get("action"); 35 | // /app-router-client-component-redirect-route-handler-fetch/session?action=logout 36 | if (action === "logout") { 37 | session.destroy(); 38 | return redirect( 39 | "/app-router-client-component-redirect-route-handler-fetch", 40 | ); 41 | } 42 | 43 | // simulate looking up the user in db 44 | await sleep(250); 45 | 46 | if (session.isLoggedIn !== true) { 47 | return Response.json(defaultSession); 48 | } 49 | 50 | return Response.json(session); 51 | } 52 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as css from "@/app/css"; 4 | import useSession from "./use-session"; 5 | import { defaultSession } from "./lib"; 6 | 7 | export function Form() { 8 | const { session, isLoading, increment } = useSession(); 9 | 10 | if (isLoading) { 11 | return <p className="text-lg">Loading...</p>; 12 | } 13 | 14 | if (session.isLoggedIn) { 15 | return ( 16 | <> 17 | <p className="text-lg"> 18 | Logged in user: <strong>{session.username}</strong> 19 |   20 | <button 21 | className={css.button} 22 | onClick={() => { 23 | increment(null, { 24 | optimisticData: { 25 | ...session, 26 | counter: session.counter + 1, 27 | }, 28 | revalidate: false, 29 | }); 30 | }} 31 | > 32 | {session.counter} 33 | </button> 34 | </p> 35 | <LogoutButton /> 36 | </> 37 | ); 38 | } 39 | 40 | return <LoginForm />; 41 | } 42 | 43 | function LoginForm() { 44 | const { login } = useSession(); 45 | 46 | return ( 47 | <form 48 | onSubmit={function (event) { 49 | event.preventDefault(); 50 | const formData = new FormData(event.currentTarget); 51 | const username = formData.get("username") as string; 52 | login(username, { 53 | optimisticData: { 54 | isLoggedIn: true, 55 | username, 56 | counter: 0, 57 | }, 58 | }); 59 | }} 60 | method="POST" 61 | className={css.form} 62 | > 63 | <label className="block text-lg"> 64 | <span className={css.label}>Username</span> 65 | <input 66 | type="text" 67 | name="username" 68 | className={css.input} 69 | placeholder="" 70 | defaultValue="Alison" 71 | required 72 | // for demo purposes, disabling autocomplete 1password here 73 | autoComplete="off" 74 | data-1p-ignore 75 | /> 76 | </label> 77 | <div> 78 | <input type="submit" value="Login" className={css.button} /> 79 | </div> 80 | </form> 81 | ); 82 | } 83 | 84 | function LogoutButton() { 85 | const { logout } = useSession(); 86 | 87 | return ( 88 | <p> 89 | <a 90 | className={css.button} 91 | onClick={(event) => { 92 | event.preventDefault(); 93 | logout(null, { 94 | optimisticData: defaultSession, 95 | }); 96 | }} 97 | > 98 | Logout 99 | </a> 100 | </p> 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/lib.ts: -------------------------------------------------------------------------------- 1 | import { SessionOptions } from "iron-session"; 2 | 3 | export interface SessionData { 4 | username: string; 5 | isLoggedIn: boolean; 6 | counter: number; 7 | } 8 | 9 | export const defaultSession: SessionData = { 10 | username: "", 11 | isLoggedIn: false, 12 | counter: 0, 13 | }; 14 | 15 | export const sessionOptions: SessionOptions = { 16 | password: "complex_password_at_least_32_characters_long", 17 | cookieName: "iron-examples-app-router-client-component-route-handler-swr", 18 | cookieOptions: { 19 | // secure only works in `https` environments 20 | // if your localhost is not on `https`, then use: `secure: process.env.NODE_ENV === "production"` 21 | secure: true, 22 | }, 23 | }; 24 | 25 | export function sleep(ms: number) { 26 | return new Promise((resolve) => setTimeout(resolve, ms)); 27 | } 28 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import * as css from "@/app/css"; 3 | 4 | import { Metadata } from "next"; 5 | import { Form } from "./form"; 6 | import { Title } from "@/app/title"; 7 | import { GetTheCode } from "../../get-the-code"; 8 | 9 | export const metadata: Metadata = { 10 | title: "🛠 iron-session examples: Client components, route handlers and SWR", 11 | }; 12 | 13 | export default function AppRouterSWR() { 14 | return ( 15 | <main className="p-10 space-y-5"> 16 | <Title 17 | subtitle={ 18 | <> 19 | + client components, route handlers, and{" "} 20 | <a 21 | className={css.link} 22 | href="https://swr.vercel.app" 23 | target="_blank" 24 | > 25 | SWR 26 | </a> 27 | </> 28 | } 29 | /> 30 | 31 | <p className="italic max-w-xl"> 32 | <u>How to test</u>: Login and refresh the page to see iron-session in 33 | action. Bonus: open multiple tabs to see the state being reflected by 34 | SWR automatically. 35 | </p> 36 | 37 | <div className="grid grid-cols-1 gap-4 p-10 border border-slate-500 rounded-md max-w-xl"> 38 | <Form /> 39 | <div className="space-y-2"> 40 | <hr /> 41 | <p> 42 | The following pages are protected and will redirect back here if 43 | you're not logged in: 44 | </p> 45 | {/* convert the following paragraphs into a ul li */} 46 | <ul className="list-disc list-inside"> 47 | <li> 48 | <Link 49 | href="/app-router-client-component-route-handler-swr/protected-client" 50 | className={css.link} 51 | > 52 | Protected page via client component → 53 | </Link> 54 | </li> 55 | <li> 56 | <Link 57 | href="/app-router-client-component-route-handler-swr/protected-server" 58 | className={css.link} 59 | // required to avoid caching issues when navigating between tabs/windows 60 | prefetch={false} 61 | > 62 | Protected page via server component → 63 | </Link>{" "} 64 | </li> 65 | <li> 66 | <Link 67 | href="/app-router-client-component-route-handler-swr/protected-middleware" 68 | className={css.link} 69 | > 70 | Protected page via middleware → 71 | </Link>{" "} 72 | </li> 73 | </ul> 74 | </div> 75 | </div> 76 | 77 | <GetTheCode path="app/app-router-client-component-route-handler-swr" /> 78 | <HowItWorks /> 79 | 80 | <p> 81 | <Link href="/" className={css.link}> 82 | ← All examples 83 | </Link> 84 | </p> 85 | </main> 86 | ); 87 | } 88 | 89 | function HowItWorks() { 90 | return ( 91 | <details className="max-w-2xl space-y-4"> 92 | <summary className="cursor-pointer">How it works</summary> 93 | 94 | <ol className="list-decimal list-inside"> 95 | <li> 96 | During login, the form is submitted with SWR's{" "} 97 | <a 98 | href="https://swr.vercel.app/docs/mutation#useswrmutation" 99 | className={css.link} 100 | > 101 | useSWRMutation 102 | </a> 103 | . This makes a POST /session request using fetch. 104 | </li> 105 | <li> 106 | {" "} 107 | During logout, the form is submitted with SWR's{" "} 108 | <a 109 | href="https://swr.vercel.app/docs/mutation#useswrmutation" 110 | className={css.link} 111 | > 112 | useSWRMutation 113 | </a> 114 | . This makes a DELETE /session request using fetch. 115 | </li> 116 | <li> 117 | In all other places, the content of the session is optimistally 118 | rendered using the most recent value, and never gets outdated. This is 119 | automatically handled by SWR using mutations and revalidation. 120 | </li> 121 | </ol> 122 | </details> 123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/protected-client/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Title } from "@/app/title"; 4 | import useSession from "../use-session"; 5 | import { useEffect } from "react"; 6 | import { useRouter } from "next/navigation"; 7 | import * as css from "@/app/css"; 8 | import Link from "next/link"; 9 | 10 | export default function ProtectedClient() { 11 | return ( 12 | <main className="p-10 space-y-5"> 13 | <Title subtitle="Protected page" /> 14 | <Content /> 15 | <p> 16 | <Link 17 | href="/app-router-client-component-route-handler-swr" 18 | className={css.link} 19 | > 20 | ← Back 21 | </Link> 22 | </p> 23 | </main> 24 | ); 25 | } 26 | 27 | function Content() { 28 | const { session, isLoading } = useSession(); 29 | const router = useRouter(); 30 | 31 | useEffect(() => { 32 | if (!isLoading && !session.isLoggedIn) { 33 | router.replace("/app-router-client-component-route-handler-swr"); 34 | } 35 | }, [isLoading, session.isLoggedIn, router]); 36 | 37 | if (isLoading) { 38 | return <p className="text-lg">Loading...</p>; 39 | } 40 | 41 | return ( 42 | <div className="max-w-xl space-y-2"> 43 | <p> 44 | Hello <strong>{session.username}!</strong> 45 | </p> 46 | <p> 47 | This page is protected and can only be accessed if you are logged in. 48 | Otherwise you will be redirected to the login page. 49 | </p> 50 | <p>The check is done via a fetch call on the client using SWR.</p> 51 | <p> 52 | One benefit of using{" "} 53 | <a href="https://swr.vercel.app" target="_blank" className={css.link}> 54 | SWR 55 | </a> 56 | : if you open the page in different tabs/windows, and logout from one 57 | place, every other tab/window will be synced and logged out. 58 | </p> 59 | </div> 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/protected-middleware/page.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from "@/app/title"; 2 | import * as css from "@/app/css"; 3 | 4 | import { cookies } from "next/headers"; 5 | import { getIronSession } from "iron-session"; 6 | import { SessionData, sessionOptions } from "../lib"; 7 | import Link from "next/link"; 8 | 9 | // Broken: None of these parameters is working, thus we have caching issues 10 | // TODO fix this 11 | export const dynamic = "force-dynamic"; 12 | export const revalidate = 0; 13 | 14 | async function getSession() { 15 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 16 | return session; 17 | } 18 | 19 | export default function ProtectedServer() { 20 | return ( 21 | <main className="p-10 space-y-5"> 22 | <Title subtitle="Protected page" /> 23 | <Content /> 24 | <p> 25 | <Link 26 | href="/app-router-client-component-route-handler-swr" 27 | className={css.link} 28 | > 29 | ← Back 30 | </Link> 31 | </p> 32 | </main> 33 | ); 34 | } 35 | 36 | async function Content() { 37 | const session = await getSession(); 38 | 39 | return ( 40 | <div className="max-w-xl space-y-2"> 41 | <p> 42 | Hello <strong>{session.username}!</strong> 43 | </p> 44 | <p> 45 | This page is protected and can only be accessed if you are logged in. 46 | Otherwise you will be redirected to the login page. 47 | </p> 48 | <p>The check is done via a middleware.</p> 49 | </div> 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/protected-server/page.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from "@/app/title"; 2 | import { Suspense } from "react"; 3 | import * as css from "@/app/css"; 4 | 5 | import { cookies } from "next/headers"; 6 | import { redirect } from "next/navigation"; 7 | import { getIronSession } from "iron-session"; 8 | import { SessionData, sessionOptions } from "../lib"; 9 | import Link from "next/link"; 10 | 11 | // Broken: None of these parameters is working, thus we have caching issues 12 | // TODO fix this 13 | export const dynamic = "force-dynamic"; 14 | export const revalidate = 0; 15 | 16 | async function getSession() { 17 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 18 | 19 | return session; 20 | } 21 | 22 | export default function ProtectedServer() { 23 | return ( 24 | <main className="p-10 space-y-5"> 25 | <Title subtitle="Protected page" /> 26 | <Suspense fallback={<p className="text-lg">Loading...</p>}> 27 | <Content /> 28 | </Suspense> 29 | <p> 30 | <Link 31 | href="/app-router-client-component-route-handler-swr" 32 | className={css.link} 33 | > 34 | ← Back 35 | </Link> 36 | </p> 37 | </main> 38 | ); 39 | } 40 | 41 | async function Content() { 42 | const session = await getSession(); 43 | 44 | if (!session.isLoggedIn) { 45 | redirect("/app-router-client-component-route-handler-swr"); 46 | } 47 | 48 | return ( 49 | <div className="max-w-xl space-y-2"> 50 | <p> 51 | Hello <strong>{session.username}!</strong> 52 | </p> 53 | <p> 54 | This page is protected and can only be accessed if you are logged in. 55 | Otherwise you will be redirected to the login page. 56 | </p> 57 | <p>The check is done via a server component.</p> 58 | </div> 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/session/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { cookies } from "next/headers"; 3 | import { getIronSession } from "iron-session"; 4 | import { defaultSession, sessionOptions } from "../lib"; 5 | import { sleep, SessionData } from "../lib"; 6 | 7 | // login 8 | export async function POST(request: NextRequest) { 9 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 10 | 11 | const { username = "No username" } = (await request.json()) as { 12 | username: string; 13 | }; 14 | 15 | session.isLoggedIn = true; 16 | session.username = username; 17 | session.counter = 0; 18 | await session.save(); 19 | 20 | // simulate looking up the user in db 21 | await sleep(250); 22 | 23 | return Response.json(session); 24 | } 25 | 26 | export async function PATCH() { 27 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 28 | 29 | session.counter++; 30 | await session.save(); 31 | 32 | return Response.json(session); 33 | } 34 | 35 | // read session 36 | export async function GET() { 37 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 38 | 39 | // simulate looking up the user in db 40 | await sleep(250); 41 | 42 | if (session.isLoggedIn !== true) { 43 | return Response.json(defaultSession); 44 | } 45 | 46 | return Response.json(session); 47 | } 48 | 49 | // logout 50 | export async function DELETE() { 51 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 52 | 53 | session.destroy(); 54 | 55 | return Response.json(defaultSession); 56 | } 57 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-client-component-route-handler-swr/use-session.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { SessionData, defaultSession } from "./lib"; 3 | import useSWRMutation from "swr/mutation"; 4 | 5 | const sessionApiRoute = 6 | "/app-router-client-component-route-handler-swr/session"; 7 | 8 | async function fetchJson<JSON = unknown>( 9 | input: RequestInfo, 10 | init?: RequestInit, 11 | ): Promise<JSON> { 12 | return fetch(input, { 13 | headers: { 14 | accept: "application/json", 15 | "content-type": "application/json", 16 | }, 17 | ...init, 18 | }).then((res) => res.json()); 19 | } 20 | 21 | function doLogin(url: string, { arg }: { arg: string }) { 22 | return fetchJson<SessionData>(url, { 23 | method: "POST", 24 | body: JSON.stringify({ username: arg }), 25 | }); 26 | } 27 | 28 | function doLogout(url: string) { 29 | return fetchJson<SessionData>(url, { 30 | method: "DELETE", 31 | }); 32 | } 33 | 34 | function doIncrement(url: string) { 35 | return fetchJson<SessionData>(url, { 36 | method: "PATCH", 37 | }); 38 | } 39 | 40 | export default function useSession() { 41 | const { data: session, isLoading } = useSWR( 42 | sessionApiRoute, 43 | fetchJson<SessionData>, 44 | { 45 | fallbackData: defaultSession, 46 | }, 47 | ); 48 | 49 | const { trigger: login } = useSWRMutation(sessionApiRoute, doLogin, { 50 | // the login route already provides the updated information, no need to revalidate 51 | revalidate: false, 52 | }); 53 | const { trigger: logout } = useSWRMutation(sessionApiRoute, doLogout); 54 | const { trigger: increment } = useSWRMutation(sessionApiRoute, doIncrement); 55 | 56 | return { session, logout, login, increment, isLoading }; 57 | } 58 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-magic-links/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as css from "@/app/css"; 4 | 5 | import { useEffect, useState } from "react"; 6 | import { SessionData, defaultSession } from "./lib"; 7 | 8 | export function Form() { 9 | const [session, setSession] = useState<SessionData>(defaultSession); 10 | const [isLoading, setIsLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | fetch("/app-router-magic-links/session") 14 | .then((res) => res.json()) 15 | .then((session) => { 16 | setSession(session); 17 | setIsLoading(false); 18 | }); 19 | }, []); 20 | 21 | if (isLoading) { 22 | return <p className="text-lg">Loading...</p>; 23 | } 24 | 25 | if (session.isLoggedIn) { 26 | return ( 27 | <> 28 | <p className="text-lg"> 29 | Logged in user: <strong>{session.username}</strong> 30 | </p> 31 | <LogoutButton /> 32 | </> 33 | ); 34 | } 35 | 36 | return <LoginForm />; 37 | } 38 | 39 | function LoginForm() { 40 | return ( 41 | <form 42 | action="/app-router-magic-links/session" 43 | method="POST" 44 | className={css.form} 45 | > 46 | <label className="block text-lg"> 47 | <span className={css.label}>Username</span> 48 | <input 49 | type="text" 50 | name="username" 51 | className={css.input} 52 | placeholder="" 53 | defaultValue="Alison" 54 | required 55 | // for demo purposes, disabling autocomplete 1password here 56 | autoComplete="off" 57 | data-1p-ignore 58 | /> 59 | </label> 60 | <div> 61 | <input 62 | type="submit" 63 | value="Get magic login link" 64 | className={css.button} 65 | /> 66 | </div> 67 | </form> 68 | ); 69 | } 70 | 71 | function LogoutButton() { 72 | return ( 73 | <p> 74 | <a 75 | href="/app-router-magic-links/session?action=logout" 76 | className={css.button} 77 | > 78 | Logout 79 | </a> 80 | </p> 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-magic-links/lib.ts: -------------------------------------------------------------------------------- 1 | import { SessionOptions } from "iron-session"; 2 | 3 | export interface SessionData { 4 | username: string; 5 | isLoggedIn: boolean; 6 | } 7 | 8 | export const defaultSession: SessionData = { 9 | username: "", 10 | isLoggedIn: false, 11 | }; 12 | 13 | export const sessionOptions: SessionOptions = { 14 | password: "complex_password_at_least_32_characters_long", 15 | cookieName: "iron-examples-app-router-magic-links", 16 | cookieOptions: { 17 | secure: process.env.NODE_ENV === "production", 18 | }, 19 | }; 20 | 21 | export function sleep(ms: number) { 22 | return new Promise((resolve) => setTimeout(resolve, ms)); 23 | } 24 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-magic-links/magic-login/route.ts: -------------------------------------------------------------------------------- 1 | import { getIronSession, unsealData } from "iron-session"; 2 | import { cookies } from "next/headers"; 3 | import { NextRequest } from "next/server"; 4 | import { SessionData, sessionOptions } from "../lib"; 5 | 6 | export async function GET(request: NextRequest) { 7 | const seal = new URL(request.url).searchParams.get("seal") as string; 8 | const { username } = await unsealData<{ username: string }>(seal, { 9 | password: sessionOptions.password, 10 | }); 11 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 12 | session.isLoggedIn = true; 13 | session.username = username ?? "No username"; 14 | await session.save(); 15 | 16 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303 17 | // not using redirect() yet: https://github.com/vercel/next.js/issues/51592#issuecomment-1810212676 18 | return Response.redirect( 19 | `${request.nextUrl.origin}/app-router-magic-links`, 20 | 303, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-magic-links/page.tsx: -------------------------------------------------------------------------------- 1 | import * as css from "@/app/css"; 2 | import Link from "next/link"; 3 | 4 | import { Metadata } from "next"; 5 | import { GetTheCode } from "../../get-the-code"; 6 | import { Title } from "../title"; 7 | import { Form } from "./form"; 8 | 9 | export const metadata: Metadata = { 10 | title: "🛠 iron-session examples: Magic links", 11 | }; 12 | 13 | export default function AppRouterRedirect() { 14 | return ( 15 | <main className="p-10 space-y-5"> 16 | <Title subtitle="+ client components, route handlers, redirects, and fetch" /> 17 | 18 | <p className="italic max-w-xl"> 19 | <u>How to test</u>: Login and refresh the page to see iron-session in 20 | action. 21 | </p> 22 | 23 | <div className="grid grid-cols-1 gap-4 p-10 border border-slate-500 rounded-md max-w-xl"> 24 | <Form /> 25 | </div> 26 | 27 | <GetTheCode path="app/app-router-magic-links" /> 28 | <HowItWorks /> 29 | 30 | <p> 31 | <Link href="/" className={css.link}> 32 | ← All examples 33 | </Link> 34 | </p> 35 | </main> 36 | ); 37 | } 38 | 39 | function HowItWorks() { 40 | return ( 41 | <details className="max-w-2xl space-y-4"> 42 | <summary className="cursor-pointer">How it works</summary> 43 | 44 | <ol className="list-decimal list-inside"> 45 | <li> 46 | The form is submitted to /app-router-magic-links/session (API route) 47 | via a POST call (non-fetch). The API route generates a sealed token 48 | and returns the magic link to client so it can be either sent or used 49 | right away. When the magic link is visited it sets the session data 50 | and redirects back to /app-router-magic-links (this page) 51 | </li> 52 | <li> 53 | The page gets the session data via a fetch call to 54 | /app-router-magic-links/session (API route). The API route either 55 | return the session data (logged in) or a default session (not logged 56 | in). 57 | </li> 58 | <li> 59 | The logout is a regular link navigating to 60 | /app-router-magic-links/logout which destroy the session and redirects 61 | back to /app-router-magic-links (this page). 62 | </li> 63 | </ol> 64 | 65 | <p> 66 | <strong>Pros</strong>: Simple. 67 | </p> 68 | <p> 69 | <strong>Cons</strong>: Dangerous if not used properly. Without any 70 | invalidations or blacklists, the magic link can be used multiple times 71 | if compromised. 72 | </p> 73 | </details> 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-magic-links/session/route.ts: -------------------------------------------------------------------------------- 1 | import { getIronSession, sealData } from "iron-session"; 2 | import { cookies } from "next/headers"; 3 | import { redirect } from "next/navigation"; 4 | import { NextRequest } from "next/server"; 5 | import { SessionData, defaultSession, sessionOptions, sleep } from "../lib"; 6 | 7 | // /app-router-magic-links/session 8 | export async function POST(request: NextRequest) { 9 | const formData = await request.formData(); 10 | const username = formData.get("username") as string; 11 | const fifteenMinutesInSeconds = 15 * 60; 12 | 13 | const seal = await sealData( 14 | { username }, 15 | { 16 | password: "complex_password_at_least_32_characters_long", 17 | ttl: fifteenMinutesInSeconds, 18 | }, 19 | ); 20 | 21 | // In a real application you would send back this data and use it to send an email 22 | // For the example purposes we will just display a webpage 23 | // return Response.json({ 24 | // ok: true, 25 | // // Ideally this would be an email or text message with a link to the magic link route 26 | // magic_link: `${process.env.PUBLIC_URL}/app-router-magic-links/magic-login?seal=${seal}`, 27 | // }); 28 | const link = `${process.env.NEXT_PUBLIC_URL}/app-router-magic-links/magic-login?seal=${seal}`; 29 | return new Response( 30 | `<h1>Here is your magic link:</h1><h3><a href="${link}">${link}</a></h3><h3>You can now open this link in a private browser window and see yourself being logged in immediately.</h3><h3>👈 <a href="/app-router-magic-links">Go back</a></h3>`, 31 | { 32 | headers: { 33 | "content-type": "text/html; charset=utf-8", 34 | }, 35 | }, 36 | ); 37 | } 38 | 39 | // /app-router-magic-links/session 40 | // /app-router-magic-links/session?action=logout 41 | export async function GET(request: NextRequest) { 42 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 43 | 44 | console.log(new URL(request.url).searchParams); 45 | const action = new URL(request.url).searchParams.get("action"); 46 | // /app-router-magic-links/session?action=logout 47 | if (action === "logout") { 48 | session.destroy(); 49 | return redirect("/app-router-magic-links"); 50 | } 51 | 52 | // simulate looking up the user in db 53 | await sleep(250); 54 | 55 | if (session.isLoggedIn !== true) { 56 | return Response.json(defaultSession); 57 | } 58 | 59 | return Response.json(session); 60 | } 61 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-server-component-and-action/actions.ts: -------------------------------------------------------------------------------- 1 | import { SessionData } from "./lib"; 2 | import { defaultSession, sessionOptions, sleep } from "./lib"; 3 | import { getIronSession, IronSession } from "iron-session"; 4 | import { cookies } from "next/headers"; 5 | import { revalidatePath } from "next/cache"; 6 | 7 | export async function getSession(shouldSleep = true) { 8 | const session = await getIronSession<SessionData>(cookies(), sessionOptions); 9 | 10 | if (!session.isLoggedIn) { 11 | session.isLoggedIn = defaultSession.isLoggedIn; 12 | session.username = defaultSession.username; 13 | } 14 | 15 | if (shouldSleep) { 16 | // simulate looking up the user in db 17 | await sleep(250); 18 | } 19 | 20 | return session; 21 | } 22 | 23 | export async function logout() { 24 | "use server"; 25 | 26 | // false => no db call for logout 27 | const session = await getSession(false); 28 | session.destroy(); 29 | revalidatePath("/app-router-server-component-and-action"); 30 | } 31 | 32 | export async function login(formData: FormData) { 33 | "use server"; 34 | 35 | const session = await getSession(); 36 | 37 | session.username = (formData.get("username") as string) ?? "No username"; 38 | session.isLoggedIn = true; 39 | await session.save(); 40 | revalidatePath("/app-router-server-component-and-action"); 41 | } 42 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-server-component-and-action/form.tsx: -------------------------------------------------------------------------------- 1 | import * as css from "@/app/css"; 2 | 3 | import { SubmitButton } from "./submit-button"; 4 | import { Input } from "./input"; 5 | import { getSession, login, logout } from "./actions"; 6 | 7 | export async function Form() { 8 | const session = await getSession(); 9 | 10 | if (session.isLoggedIn) { 11 | return ( 12 | <> 13 | <p className="text-lg"> 14 | Logged in user: <strong>{session.username}</strong> 15 | </p> 16 | <LogoutButton /> 17 | </> 18 | ); 19 | } 20 | 21 | return <LoginForm />; 22 | } 23 | 24 | function LoginForm() { 25 | return ( 26 | <form action={login} className={css.form}> 27 | <label className="block text-lg"> 28 | <span className={css.label}>Username</span> 29 | <Input /> 30 | </label> 31 | <div> 32 | <SubmitButton value="Login" /> 33 | </div> 34 | </form> 35 | ); 36 | } 37 | 38 | function LogoutButton() { 39 | return ( 40 | <form action={logout} className={css.form}> 41 | <div> 42 | <SubmitButton value="Logout" /> 43 | </div> 44 | </form> 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-server-component-and-action/input.tsx: -------------------------------------------------------------------------------- 1 | // We're using a client component to show a loading state 2 | "use client"; 3 | 4 | import * as css from "@/app/css"; 5 | import { useFormStatus } from "react-dom"; 6 | 7 | export function Input() { 8 | const { pending } = useFormStatus(); 9 | 10 | return ( 11 | <input 12 | type="text" 13 | disabled={pending} 14 | name="username" 15 | className={css.input} 16 | placeholder="" 17 | defaultValue="Alison" 18 | required 19 | // for demo purposes, disabling autocomplete 1password here 20 | autoComplete="off" 21 | data-1p-ignore 22 | /> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-server-component-and-action/lib.ts: -------------------------------------------------------------------------------- 1 | import { SessionOptions } from "iron-session"; 2 | 3 | export interface SessionData { 4 | username: string; 5 | isLoggedIn: boolean; 6 | } 7 | 8 | export const defaultSession: SessionData = { 9 | username: "", 10 | isLoggedIn: false, 11 | }; 12 | 13 | export const sessionOptions: SessionOptions = { 14 | password: "complex_password_at_least_32_characters_long", 15 | cookieName: "iron-examples-app-router-server-component-and-action", 16 | cookieOptions: { 17 | // secure only works in `https` environments 18 | // if your localhost is not on `https`, then use: `secure: process.env.NODE_ENV === "production"` 19 | secure: true, 20 | }, 21 | }; 22 | 23 | export function sleep(ms: number) { 24 | return new Promise((resolve) => setTimeout(resolve, ms)); 25 | } 26 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-server-component-and-action/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Metadata } from "next"; 4 | import { Form } from "./form"; 5 | import { Suspense } from "react"; 6 | import * as css from "@/app/css"; 7 | import { Title } from "../title"; 8 | import { GetTheCode } from "../../get-the-code"; 9 | 10 | export const metadata: Metadata = { 11 | title: "🛠 iron-session examples: Server components, and server actions", 12 | }; 13 | 14 | export default async function AppRouter() { 15 | return ( 16 | <main className="p-10 space-y-5"> 17 | <Title subtitle="+ server components, and server actions" /> 18 | 19 | <p className="italic max-w-xl"> 20 | <u>How to test</u>: Login and refresh the page to see iron-session in 21 | action. 22 | </p> 23 | 24 | <div className="grid grid-cols-1 gap-4 p-10 border border-slate-500 rounded-md max-w-xl"> 25 | <Suspense fallback={<p className="text-lg">Loading...</p>}> 26 | <Form /> 27 | </Suspense> 28 | </div> 29 | 30 | <GetTheCode path="app/app-router-server-component-and-action" /> 31 | <HowItWorks /> 32 | 33 | <p> 34 | <Link href="/" className={css.link}> 35 | ← All examples 36 | </Link> 37 | </p> 38 | </main> 39 | ); 40 | } 41 | 42 | function HowItWorks() { 43 | return ( 44 | <details className="max-w-2xl space-y-4"> 45 | <summary className="cursor-pointer">How it works</summary> 46 | 47 | <ol className="list-decimal list-inside"> 48 | <li>During login, the page uses a server action.</li> 49 | <li>During logout, the page uses a server action.</li> 50 | <li> 51 | When displaying session data, the server component gets the data and 52 | pass it to the page. 53 | </li> 54 | </ol> 55 | </details> 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /examples/next/src/app/app-router-server-component-and-action/submit-button.tsx: -------------------------------------------------------------------------------- 1 | // We're using a client component to show a loading state 2 | "use client"; 3 | 4 | import { useFormStatus } from "react-dom"; 5 | import * as css from "@/app/css"; 6 | 7 | export function SubmitButton({ value }: { value: string }) { 8 | const { pending } = useFormStatus(); 9 | 10 | return ( 11 | <input 12 | type="submit" 13 | value={pending ? "Loading…" : value} 14 | disabled={pending} 15 | className={css.button} 16 | /> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/next/src/app/css.ts: -------------------------------------------------------------------------------- 1 | export const button = 2 | "hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-900 dark:text-white border border-slate-900 dark:border-white py-2 px-4 rounded focus:outline-2 cursor-pointer"; 3 | 4 | export const form = "max-w-md grid grid-cols-1 gap-6"; 5 | 6 | export const label = "text-slate-700 dark:text-slate-300"; 7 | 8 | export const input = 9 | "mt-1 block w-full disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-500 dark:text-slate-900"; 10 | 11 | export const link = 12 | "text-indigo-500 dark:text-indigo-400 underline hover:no-underline"; 13 | -------------------------------------------------------------------------------- /examples/next/src/app/fathom.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { load, trackPageview } from "fathom-client"; 4 | import { useEffect, Suspense } from "react"; 5 | import { usePathname, useSearchParams } from "next/navigation"; 6 | 7 | function TrackPageView() { 8 | const pathname = usePathname(); 9 | const searchParams = useSearchParams(); 10 | 11 | // Load the Fathom script on mount 12 | useEffect(() => { 13 | load("YKGUEAZB", { 14 | auto: false, 15 | }); 16 | }, []); 17 | 18 | // Record a pageview when route changes 19 | useEffect(() => { 20 | if (!pathname) return; 21 | 22 | trackPageview({ 23 | url: pathname + searchParams?.toString(), 24 | referrer: document.referrer, 25 | }); 26 | }, [pathname, searchParams]); 27 | 28 | return null; 29 | } 30 | 31 | export default function Fathom() { 32 | return ( 33 | <Suspense fallback={null}> 34 | <TrackPageView /> 35 | </Suspense> 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /examples/next/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvo/iron-session/abbd341e684598c04b07aa7874052c057927d9c2/examples/next/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/next/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply bg-slate-50 dark:bg-slate-800 text-slate-900 dark:text-white; 7 | } 8 | -------------------------------------------------------------------------------- /examples/next/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import Fathom from "./fathom"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "🛠 iron-session examples", 10 | description: "Set of examples for iron-session", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | <html lang="en"> 20 | <body className={inter.className}> 21 | <Fathom /> 22 | {children} 23 | </body> 24 | </html> 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /examples/next/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import * as css from "@/app/css"; 3 | import { Title } from "./title"; 4 | 5 | export default function Home() { 6 | return ( 7 | <main className="p-10 space-y-5"> 8 | <Title subtitle="" category="Home" /> 9 | <p className="max-w-xl"> 10 | This website showcase different ways to use the iron-session library. 11 | <br /> 12 | Note: We've added delay to simulate database requests at login. 13 | </p> 14 | <h2 className="text-slate-700 dark:text-slate-300 text-xl"> 15 | Main Examples: 16 | </h2> 17 | <ul className="list-disc list-inside"> 18 | <li> 19 | <Link 20 | // rewrite className with template literal 21 | className={`${css.link} text-lg`} 22 | href="/app-router-client-component-route-handler-swr" 23 | > 24 | App router + client components, route handlers, and SWR 25 | </Link>{" "} 26 | 🤩 27 | </li> 28 | <li> 29 | <Link 30 | href="/pages-router-api-route-swr" 31 | className={`${css.link} text-lg`} 32 | > 33 | Pages Router + API routes, getServerSideProps, and SWR 34 | </Link>{" "} 35 | 🤩 36 | </li> 37 | </ul> 38 | <p className="indent-10 text-center text-md text-slate-700 dark:text-slate-400 max-w-lg"> 39 | ☝️ These two examples are the most advanced and the ones we recommend 40 | for now. 41 | </p> 42 | <h2 className="text-slate-700 dark:text-slate-300 text-xl"> 43 | Other Examples: 44 | </h2> 45 | <ul className="list-disc list-inside"> 46 | <li> 47 | <Link 48 | className={`${css.link} text-lg`} 49 | href="/app-router-server-component-and-action" 50 | // prefetch = false to avoid caching issues when navigating between tabs/windows 51 | prefetch={false} 52 | > 53 | App router + server components, and server actions 54 | </Link> 55 | </li> 56 | <li> 57 | <Link 58 | className={`${css.link} text-lg`} 59 | href="/app-router-client-component-redirect-route-handler-fetch" 60 | > 61 | App router + client components, route handlers, redirects, and fetch 62 | </Link> 63 | </li> 64 | <li> 65 | <Link 66 | href="/pages-router-redirect-api-route-fetch" 67 | className={`${css.link} text-lg`} 68 | > 69 | Pages Router + API routes, redirects, and fetch 70 | </Link> 71 | </li> 72 | <li> 73 | <Link 74 | href="/app-router-magic-links" 75 | className={`${css.link} text-lg`} 76 | > 77 | Magic links 78 | </Link> 79 | </li> 80 | <li className="text-slate-500"> 81 | OAuth login example (SWR) (Help needed) 82 | </li> 83 | <li className="text-slate-500"> 84 | Pages Router (and App Router?) req.session wrappers (Help needed) 85 | </li> 86 | </ul> 87 | </main> 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /examples/next/src/app/title.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import * as css from "@/app/css"; 3 | import GitHubLogo from "./GitHubLogo"; 4 | 5 | export function Title({ 6 | category = "App router", 7 | subtitle, 8 | }: { 9 | category?: string; 10 | subtitle: JSX.Element | string; 11 | }) { 12 | return ( 13 | <div> 14 | <h1> 15 | <div className="flex items-center gap-2"> 16 | <div className="text-2xl"> 17 | <span className="hidden dark:inline">🌝</span> 18 | <span className="dark:hidden">🛠</span>{" "} 19 | <Link className={css.link} href="/"> 20 | iron-session 21 | </Link>{" "} 22 | <span className="text-slate-700 dark:text-slate-300"> 23 | examples: {category} 24 | </span> 25 | </div> 26 | <span className="text-slate-300 dark:text-slate-700 text-xl"> 27 | {" "} 28 | |{" "} 29 | </span> 30 | <div> 31 | <div className="flex items-center gap-2 text-md"> 32 | <GitHubLogo />{" "} 33 | <a 34 | href="https://github.com/vvo/iron-session" 35 | target="_blank" 36 | className="text-slate-700 dark:text-slate-300 underline hover:no-underline" 37 | > 38 | vvo/iron-session 39 | </a> 40 | </div> 41 | </div> 42 | </div> 43 | </h1> 44 | <h2 className="text-lg text-slate-500 dark:text-slate-400">{subtitle}</h2> 45 | </div> 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/next/src/get-the-code.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import GitHubLogo from "./app/GitHubLogo"; 3 | 4 | export function GetTheCode({ path }: { path: string }) { 5 | return ( 6 | <div className="flex items-center gap-2 text-md"> 7 | <GitHubLogo />{" "} 8 | <a 9 | href={`https://github.com/vvo/iron-session/tree/main/examples/next/src/${path}`} 10 | target="_blank" 11 | className="underline hover:no-underline" 12 | > 13 | Get the code for this example 14 | </a> 15 | </div> 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/next/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | 3 | import { sessionOptions as appRouterClientComponentRouteHandlerSwrIronOptions } from "./app/app-router-client-component-route-handler-swr/lib"; 4 | import { sessionOptions as pagesRouterApiRouteSwrIronOptions } from "./pages-components/pages-router-api-route-swr/lib"; 5 | import { cookies } from "next/headers"; 6 | import { SessionOptions, getIronSession } from "iron-session"; 7 | 8 | // Only here for the multi examples demo, in your app this would be imported from elsewhere 9 | interface SessionData { 10 | username: string; 11 | isLoggedIn: boolean; 12 | } 13 | 14 | const sessionOptions: Record<string, SessionOptions> = { 15 | "/app-router-client-component-route-handler-swr/protected-middleware": 16 | appRouterClientComponentRouteHandlerSwrIronOptions, 17 | "/pages-router-api-route-swr/protected-middleware": 18 | pagesRouterApiRouteSwrIronOptions, 19 | }; 20 | 21 | // This function can be marked `async` if using `await` inside 22 | export async function middleware(request: NextRequest) { 23 | const session = await getIronSession<SessionData>( 24 | cookies(), 25 | sessionOptions[request.nextUrl.pathname], 26 | ); 27 | 28 | if (!session.isLoggedIn) { 29 | const redirectTo = request.nextUrl.pathname.split( 30 | "/protected-middleware", 31 | )[0]; 32 | 33 | return Response.redirect(`${request.nextUrl.origin}${redirectTo}`, 302); 34 | } 35 | } 36 | 37 | // See "Matching Paths" below to learn more 38 | export const config = { 39 | matcher: "/:path+/protected-middleware", 40 | }; 41 | -------------------------------------------------------------------------------- /examples/next/src/pages-components/pages-router-api-route-swr/form.tsx: -------------------------------------------------------------------------------- 1 | import * as css from "@/app/css"; 2 | import useSession from "./use-session"; 3 | import { defaultSession } from "./lib"; 4 | 5 | export function Form() { 6 | const { session, isLoading } = useSession(); 7 | 8 | if (isLoading) { 9 | return <p className="text-lg">Loading...</p>; 10 | } 11 | 12 | if (session.isLoggedIn) { 13 | return ( 14 | <> 15 | <p className="text-lg"> 16 | Logged in user: <strong>{session.username}</strong> 17 | </p> 18 | <LogoutButton /> 19 | </> 20 | ); 21 | } 22 | 23 | return <LoginForm />; 24 | } 25 | 26 | function LoginForm() { 27 | const { login } = useSession(); 28 | 29 | return ( 30 | <form 31 | onSubmit={function (event) { 32 | event.preventDefault(); 33 | const formData = new FormData(event.currentTarget); 34 | const username = formData.get("username") as string; 35 | login(username, { 36 | optimisticData: { 37 | isLoggedIn: true, 38 | username, 39 | }, 40 | }); 41 | }} 42 | method="POST" 43 | className={css.form} 44 | > 45 | <label className="block text-lg"> 46 | <span className={css.label}>Username</span> 47 | <input 48 | type="text" 49 | name="username" 50 | className={css.input} 51 | placeholder="" 52 | defaultValue="Alison" 53 | required 54 | // for demo purposes, disabling autocomplete 1password here 55 | autoComplete="off" 56 | data-1p-ignore 57 | /> 58 | </label> 59 | <div> 60 | <input type="submit" value="Login" className={css.button} /> 61 | </div> 62 | </form> 63 | ); 64 | } 65 | 66 | function LogoutButton() { 67 | const { logout } = useSession(); 68 | 69 | return ( 70 | <p> 71 | <a 72 | className={css.button} 73 | onClick={(event) => { 74 | event.preventDefault(); 75 | logout(null, { 76 | optimisticData: defaultSession, 77 | }); 78 | }} 79 | > 80 | Logout 81 | </a> 82 | </p> 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /examples/next/src/pages-components/pages-router-api-route-swr/lib.ts: -------------------------------------------------------------------------------- 1 | import { SessionOptions } from "iron-session"; 2 | 3 | export interface SessionData { 4 | username: string; 5 | isLoggedIn: boolean; 6 | } 7 | 8 | export const defaultSession: SessionData = { 9 | username: "", 10 | isLoggedIn: false, 11 | }; 12 | 13 | export const sessionOptions: SessionOptions = { 14 | password: "complex_password_at_least_32_characters_long", 15 | cookieName: "iron-examples-pages-router-api-route-swr", 16 | cookieOptions: { 17 | // secure only works in `https` environments 18 | // if your localhost is not on `https`, then use: `secure: process.env.NODE_ENV === "production"` 19 | secure: true, 20 | }, 21 | }; 22 | 23 | export function sleep(ms: number) { 24 | return new Promise((resolve) => setTimeout(resolve, ms)); 25 | } 26 | -------------------------------------------------------------------------------- /examples/next/src/pages-components/pages-router-api-route-swr/use-session.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { SessionData, defaultSession } from "./lib"; 3 | import useSWRMutation from "swr/mutation"; 4 | 5 | const sessionApiRoute = "/api/pages-router-api-route-swr/session"; 6 | 7 | async function fetchJson<JSON = unknown>( 8 | input: RequestInfo, 9 | init?: RequestInit, 10 | ): Promise<JSON> { 11 | return fetch(input, { 12 | headers: { 13 | accept: "application/json", 14 | "content-type": "application/json", 15 | }, 16 | ...init, 17 | }).then((res) => res.json()); 18 | } 19 | 20 | function doLogin(url: string, { arg }: { arg: string }) { 21 | return fetchJson<SessionData>(url, { 22 | method: "POST", 23 | body: JSON.stringify({ username: arg }), 24 | }); 25 | } 26 | 27 | function doLogout(url: string) { 28 | return fetchJson<SessionData>(url, { 29 | method: "DELETE", 30 | }); 31 | } 32 | 33 | export default function useSession() { 34 | const { data: session, isLoading } = useSWR( 35 | sessionApiRoute, 36 | fetchJson<SessionData>, 37 | { 38 | fallbackData: defaultSession, 39 | }, 40 | ); 41 | 42 | const { trigger: login } = useSWRMutation(sessionApiRoute, doLogin, { 43 | // the login route already provides the updated information, no need to revalidate 44 | revalidate: false, 45 | }); 46 | const { trigger: logout } = useSWRMutation(sessionApiRoute, doLogout); 47 | 48 | return { session, logout, login, isLoading }; 49 | } 50 | -------------------------------------------------------------------------------- /examples/next/src/pages-components/pages-router-redirect-api-route-fetch/form.tsx: -------------------------------------------------------------------------------- 1 | import * as css from "@/app/css"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { SessionData } from "./lib"; 5 | import { defaultSession } from "./lib"; 6 | import Link from "next/link"; 7 | 8 | export function Form() { 9 | const [session, setSession] = useState<SessionData>(defaultSession); 10 | const [isLoading, setIsLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | fetch("/api/pages-router-redirect-api-route-fetch/session") 14 | .then((res) => res.json()) 15 | .then((session) => { 16 | setSession(session); 17 | setIsLoading(false); 18 | }); 19 | }, []); 20 | 21 | if (isLoading) { 22 | return <p className="text-lg">Loading...</p>; 23 | } 24 | 25 | if (session.isLoggedIn) { 26 | return ( 27 | <> 28 | <p className="text-lg"> 29 | Logged in user: <strong>{session.username}</strong> 30 | </p> 31 | <LogoutButton /> 32 | </> 33 | ); 34 | } 35 | 36 | return <LoginForm />; 37 | } 38 | 39 | function LoginForm() { 40 | return ( 41 | <form 42 | action="/api/pages-router-redirect-api-route-fetch/session" 43 | method="POST" 44 | className={css.form} 45 | > 46 | <label className="block text-lg"> 47 | <span className={css.label}>Username</span> 48 | <input 49 | type="text" 50 | name="username" 51 | className={css.input} 52 | placeholder="" 53 | defaultValue="Alison" 54 | required 55 | // for demo purposes, disabling autocomplete 1password here 56 | autoComplete="off" 57 | data-1p-ignore 58 | /> 59 | </label> 60 | <div> 61 | <input type="submit" value="Login" className={css.button} /> 62 | </div> 63 | </form> 64 | ); 65 | } 66 | 67 | function LogoutButton() { 68 | return ( 69 | <p> 70 | <Link 71 | href="/api/pages-router-redirect-api-route-fetch/session?action=logout" 72 | className={css.button} 73 | > 74 | Logout 75 | </Link> 76 | </p> 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /examples/next/src/pages-components/pages-router-redirect-api-route-fetch/lib.ts: -------------------------------------------------------------------------------- 1 | import { SessionOptions } from "iron-session"; 2 | 3 | export interface SessionData { 4 | username: string; 5 | isLoggedIn: boolean; 6 | } 7 | 8 | export const defaultSession: SessionData = { 9 | username: "", 10 | isLoggedIn: false, 11 | }; 12 | 13 | export const sessionOptions: SessionOptions = { 14 | password: "complex_password_at_least_32_characters_long", 15 | cookieName: "iron-examples-pages-router-redirect-api-route-fetch", 16 | cookieOptions: { 17 | // secure only works in `https` environments 18 | // if your localhost is not on `https`, then use: `secure: process.env.NODE_ENV === "production"` 19 | secure: true, 20 | }, 21 | }; 22 | 23 | export function sleep(ms: number) { 24 | return new Promise((resolve) => setTimeout(resolve, ms)); 25 | } 26 | -------------------------------------------------------------------------------- /examples/next/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import React, { useEffect } from "react"; 3 | import Router from "next/router"; 4 | import * as Fathom from "fathom-client"; 5 | 6 | import "@/app/globals.css"; 7 | 8 | import { Inter } from "next/font/google"; 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | // Record a pageview when route changes 12 | Router.events.on("routeChangeComplete", (as, routeProps) => { 13 | if (!routeProps.shallow) { 14 | Fathom.trackPageview(); 15 | } 16 | }); 17 | 18 | export default function MyApp({ Component, pageProps }: AppProps) { 19 | // Initialize Fathom when the app loads 20 | useEffect(() => { 21 | Fathom.load("YKGUEAZB"); 22 | }, []); 23 | 24 | return ( 25 | <main className={inter.className}> 26 | <Component {...pageProps} /> 27 | </main> 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/next/src/pages/api/pages-router-api-route-swr/session.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getIronSession } from "iron-session"; 3 | import { 4 | defaultSession, 5 | sessionOptions, 6 | sleep, 7 | SessionData, 8 | } from "../../../pages-components/pages-router-api-route-swr/lib"; 9 | 10 | // login 11 | export default async function handler( 12 | request: NextApiRequest, 13 | response: NextApiResponse, 14 | ) { 15 | const session = await getIronSession<SessionData>( 16 | request, 17 | response, 18 | sessionOptions, 19 | ); 20 | 21 | if (request.method === "POST") { 22 | const { username = "No username" } = request.body; 23 | 24 | session.isLoggedIn = true; 25 | session.username = username; 26 | await session.save(); 27 | 28 | // simulate looking up the user in db 29 | await sleep(250); 30 | 31 | return response.json(session); 32 | } else if (request.method === "GET") { 33 | // simulate looking up the user in db 34 | await sleep(250); 35 | 36 | if (session.isLoggedIn !== true) { 37 | return response.json(defaultSession); 38 | } 39 | 40 | return response.json(session); 41 | } else if (request.method === "DELETE") { 42 | session.destroy(); 43 | 44 | return response.json(defaultSession); 45 | } 46 | 47 | return response.status(405).end(`Method ${request.method} Not Allowed`); 48 | } 49 | -------------------------------------------------------------------------------- /examples/next/src/pages/api/pages-router-redirect-api-route-fetch/session.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getIronSession } from "iron-session"; 3 | import { 4 | sessionOptions, 5 | sleep, 6 | defaultSession, 7 | SessionData, 8 | } from "../../../pages-components/pages-router-redirect-api-route-fetch/lib"; 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse, 13 | ) { 14 | const session = await getIronSession<SessionData>(req, res, sessionOptions); 15 | 16 | // POST request handling (for session creation) 17 | if (req.method === "POST") { 18 | session.isLoggedIn = true; 19 | session.username = req.body.username ?? "No username"; 20 | await session.save(); 21 | 22 | await sleep(250); 23 | 24 | // Redirect after creating session 25 | res.status(303).redirect("/pages-router-redirect-api-route-fetch"); 26 | return; 27 | } 28 | 29 | // GET request handling 30 | if (req.method === "GET") { 31 | const action = req.query.action as string; 32 | 33 | // Handle logout 34 | if (action === "logout") { 35 | session.destroy(); 36 | res.redirect("/pages-router-redirect-api-route-fetch"); 37 | return; 38 | } 39 | 40 | // Handle session retrieval 41 | await sleep(250); 42 | 43 | if (session.isLoggedIn !== true) { 44 | res.status(200).json(defaultSession); 45 | } else { 46 | res.status(200).json(session); 47 | } 48 | return; 49 | } 50 | 51 | // If the method is not supported 52 | res.status(405).end(`Method ${req.method} Not Allowed`); 53 | } 54 | -------------------------------------------------------------------------------- /examples/next/src/pages/pages-router-api-route-swr/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import * as css from "@/app/css"; 3 | 4 | import { Form } from "@/pages-components/pages-router-api-route-swr/form"; 5 | import { Title } from "@/app/title"; 6 | import { GetTheCode } from "@/get-the-code"; 7 | import Head from "next/head"; 8 | 9 | export default function AppRouterSWR() { 10 | return ( 11 | <main className="p-10 space-y-5"> 12 | <Head> 13 | <title> 14 | 🛠 iron-session examples: Pages Router, API routes, and SWR 15 | 16 | 17 | 20 | + API routes, and{" "} 21 | <a 22 | className={css.link} 23 | href="https://swr.vercel.app" 24 | target="_blank" 25 | > 26 | SWR 27 | </a> 28 | </> 29 | } 30 | /> 31 | 32 | <p className="italic max-w-xl"> 33 | <u>How to test</u>: Login and refresh the page to see iron-session in 34 | action. Bonus: open multiple tabs to see the state being reflected by 35 | SWR automatically. 36 | </p> 37 | 38 | <div className="grid grid-cols-1 gap-4 p-10 border border-slate-500 rounded-md max-w-xl"> 39 | <Form /> 40 | <div className="space-y-2"> 41 | <hr /> 42 | <p> 43 | The following pages are protected and will redirect back here if 44 | you're not logged in: 45 | </p> 46 | {/* convert the following paragraphs into a ul li */} 47 | <ul className="list-disc list-inside"> 48 | <li> 49 | <Link 50 | href="/pages-router-api-route-swr/protected-client" 51 | className={css.link} 52 | > 53 | Protected page via client call → 54 | </Link> 55 | </li> 56 | <li> 57 | <Link 58 | href="/pages-router-api-route-swr/protected-server" 59 | className={css.link} 60 | > 61 | Protected page via getServerSideProps → 62 | </Link>{" "} 63 | </li> 64 | <li> 65 | <Link 66 | href="/pages-router-api-route-swr/protected-middleware" 67 | className={css.link} 68 | > 69 | Protected page via middleware → 70 | </Link>{" "} 71 | </li> 72 | </ul> 73 | </div> 74 | </div> 75 | 76 | <GetTheCode path="pages/pages-router-api-route-swr" /> 77 | <HowItWorks /> 78 | 79 | <p> 80 | <Link href="/" className={css.link}> 81 | ← All examples 82 | </Link> 83 | </p> 84 | </main> 85 | ); 86 | } 87 | 88 | function HowItWorks() { 89 | return ( 90 | <details className="max-w-2xl space-y-4"> 91 | <summary className="cursor-pointer">How it works</summary> 92 | 93 | <ol className="list-decimal list-inside"> 94 | <li> 95 | During login, the form is submitted with SWR's{" "} 96 | <a 97 | href="https://swr.vercel.app/docs/mutation#useswrmutation" 98 | className={css.link} 99 | > 100 | useSWRMutation 101 | </a> 102 | . This makes a POST /session request using fetch. 103 | </li> 104 | <li> 105 | {" "} 106 | During logout, the form is submitted with SWR's{" "} 107 | <a 108 | href="https://swr.vercel.app/docs/mutation#useswrmutation" 109 | className={css.link} 110 | > 111 | useSWRMutation 112 | </a> 113 | . This makes a DELETE /session request using fetch. 114 | </li> 115 | <li> 116 | In all other places, the content of the session is optimistally 117 | rendered using the most recent value, and never gets outdated. This is 118 | automatically handled by SWR using mutations and revalidation. 119 | </li> 120 | </ol> 121 | </details> 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /examples/next/src/pages/pages-router-api-route-swr/protected-client/index.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from "@/app/title"; 2 | import useSession from "@/pages-components/pages-router-api-route-swr/use-session"; 3 | import { useEffect } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import * as css from "@/app/css"; 6 | import Link from "next/link"; 7 | 8 | export default function ProtectedClient() { 9 | return ( 10 | <main className="p-10 space-y-5"> 11 | <Title subtitle="Protected page" /> 12 | <Content /> 13 | <p> 14 | <Link href="/pages-router-api-route-swr" className={css.link}> 15 | ← Back 16 | </Link> 17 | </p> 18 | </main> 19 | ); 20 | } 21 | 22 | function Content() { 23 | const { session, isLoading } = useSession(); 24 | const router = useRouter(); 25 | 26 | useEffect(() => { 27 | if (!isLoading && !session.isLoggedIn) { 28 | router.replace("/pages-router-api-route-swr"); 29 | } 30 | }, [isLoading, session.isLoggedIn, router]); 31 | 32 | if (isLoading) { 33 | return <p className="text-lg">Loading...</p>; 34 | } 35 | 36 | return ( 37 | <div className="max-w-xl space-y-2"> 38 | <p> 39 | Hello <strong>{session.username}!</strong> 40 | </p> 41 | <p> 42 | This page is protected and can only be accessed if you are logged in. 43 | Otherwise you will be redirected to the login page. 44 | </p> 45 | <p>The check is done via a fetch call on the client using SWR.</p> 46 | <p> 47 | One benefit of using{" "} 48 | <a href="https://swr.vercel.app" target="_blank" className={css.link}> 49 | SWR 50 | </a> 51 | : if you open the page in different tabs/windows, and logout from one 52 | place, every other tab/window will be synced and logged out. 53 | </p> 54 | </div> 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /examples/next/src/pages/pages-router-api-route-swr/protected-middleware/index.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from "@/app/title"; 2 | import * as css from "@/app/css"; 3 | 4 | import { getIronSession } from "iron-session"; 5 | import { 6 | SessionData, 7 | sessionOptions, 8 | } from "@/pages-components/pages-router-api-route-swr/lib"; 9 | import Link from "next/link"; 10 | import type { InferGetServerSidePropsType, GetServerSideProps } from "next"; 11 | 12 | export default function ProtectedServer({ 13 | session, 14 | }: InferGetServerSidePropsType<typeof getServerSideProps>) { 15 | return ( 16 | <main className="p-10 space-y-5"> 17 | <Title subtitle="Protected page" /> 18 | <Content session={session} /> 19 | <p> 20 | <Link href="/pages-router-api-route-swr" className={css.link}> 21 | ← Back 22 | </Link> 23 | </p> 24 | </main> 25 | ); 26 | } 27 | 28 | export const getServerSideProps = (async (context) => { 29 | const session = await getIronSession<SessionData>( 30 | context.req, 31 | context.res, 32 | sessionOptions, 33 | ); 34 | 35 | if (!session.isLoggedIn) { 36 | return { 37 | redirect: { 38 | destination: "/pages-router-api-route-swr", 39 | permanent: false, 40 | }, 41 | }; 42 | } 43 | 44 | return { props: { session } }; 45 | }) satisfies GetServerSideProps<{ 46 | session: SessionData; 47 | }>; 48 | 49 | function Content({ session }: { session: SessionData }) { 50 | return ( 51 | <div className="max-w-xl space-y-2"> 52 | <p> 53 | Hello <strong>{session.username}!</strong> 54 | </p> 55 | <p> 56 | This page is protected and can only be accessed if you are logged in. 57 | Otherwise you will be redirected to the login page. 58 | </p> 59 | <p> 60 | The isLoggedIn check is done by a middleware and the data comes from 61 | getServerSideProps. 62 | </p> 63 | </div> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/next/src/pages/pages-router-api-route-swr/protected-server/index.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from "@/app/title"; 2 | import * as css from "@/app/css"; 3 | 4 | import { getIronSession } from "iron-session"; 5 | import { 6 | SessionData, 7 | sessionOptions, 8 | } from "@/pages-components/pages-router-api-route-swr/lib"; 9 | import Link from "next/link"; 10 | import type { InferGetServerSidePropsType, GetServerSideProps } from "next"; 11 | 12 | export default function ProtectedServer({ 13 | session, 14 | }: InferGetServerSidePropsType<typeof getServerSideProps>) { 15 | return ( 16 | <main className="p-10 space-y-5"> 17 | <Title subtitle="Protected page" /> 18 | <Content session={session} /> 19 | <p> 20 | <Link href="/pages-router-api-route-swr" className={css.link}> 21 | ← Back 22 | </Link> 23 | </p> 24 | </main> 25 | ); 26 | } 27 | 28 | export const getServerSideProps = (async (context) => { 29 | const session = await getIronSession<SessionData>( 30 | context.req, 31 | context.res, 32 | sessionOptions, 33 | ); 34 | 35 | if (!session.isLoggedIn) { 36 | return { 37 | redirect: { 38 | destination: "/pages-router-api-route-swr", 39 | permanent: false, 40 | }, 41 | }; 42 | } 43 | 44 | return { props: { session } }; 45 | }) satisfies GetServerSideProps<{ 46 | session: SessionData; 47 | }>; 48 | 49 | function Content({ session }: { session: SessionData }) { 50 | return ( 51 | <div className="max-w-xl space-y-2"> 52 | <p> 53 | Hello <strong>{session.username}!</strong> 54 | </p> 55 | <p> 56 | This page is protected and can only be accessed if you are logged in. 57 | Otherwise you will be redirected to the login page. 58 | </p> 59 | <p> 60 | getServerSideProps is used for the isLoggedIn check and to get the 61 | session data. 62 | </p> 63 | </div> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/next/src/pages/pages-router-redirect-api-route-fetch/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetTheCode } from "@/get-the-code"; 2 | import { Title } from "@/app/title"; 3 | import * as css from "@/app/css"; 4 | import Link from "next/link"; 5 | import { Form } from "@/pages-components/pages-router-redirect-api-route-fetch/form"; 6 | import Head from "next/head"; 7 | 8 | export default function PagesRouterRedirect() { 9 | return ( 10 | <main className="p-10 space-y-5"> 11 | <Head> 12 | <title> 13 | 🛠 iron-session examples: Pages Router, API routes, redirects and 14 | fetch 15 | 16 | 17 | 21 | 22 | <p className="italic max-w-xl"> 23 | <u>How to test</u>: Login and refresh the page to see iron-session in 24 | action. 25 | </p> 26 | 27 | <div className="grid grid-cols-1 gap-4 p-10 border border-slate-500 rounded-md max-w-xl"> 28 | <Form /> 29 | </div> 30 | 31 | <GetTheCode path="pages/pages-router-redirect-api-route-fetch" /> 32 | <HowItWorks /> 33 | 34 | <p> 35 | <Link href="/" className={css.link}> 36 | ← All examples 37 | </Link> 38 | </p> 39 | </main> 40 | ); 41 | } 42 | 43 | function HowItWorks() { 44 | return ( 45 | <details className="max-w-2xl space-y-4"> 46 | <summary className="cursor-pointer">How it works</summary> 47 | 48 | <ol className="list-decimal list-inside"> 49 | <li> 50 | The form is submitted to 51 | /api/pages-router-redirect-api-route-fetch/session (API route) via a 52 | POST call (non-fetch). The API route sets the session data and 53 | redirects back to /pages-router-redirect-api-route-fetch (this page). 54 | </li> 55 | <li> 56 | The page gets the session data via a fetch call to 57 | /api/pages-router-redirect-api-route-fetch/session (API route). The 58 | API route either return the session data (logged in) or a default 59 | session (not logged in). 60 | </li> 61 | <li> 62 | The logout is a regular link navigating to 63 | /api/pages-router-redirect-api-route-fetch/session?action=logout which 64 | destroy the session and redirects back to 65 | /pages-router-redirect-api-route-fetch (this page). 66 | </li> 67 | </ol> 68 | 69 | <p> 70 | <strong>Pros</strong>: Straightforward. It does not rely on too many 71 | APIs. This is what we would have implemented a few years ago and is good 72 | enough for many websites. 73 | </p> 74 | <p> 75 | <strong>Cons</strong>: No synchronization. The session is not updated 76 | between tabs and windows. If you login or logout in one window or tab, 77 | the others are still showing the previous state. Also, we rely on full 78 | page navigation and redirects for login and logout. We could remove them 79 | by using fetch instead. 80 | </p> 81 | </details> 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /examples/next/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import formsPlugin from "@tailwindcss/forms"; 3 | 4 | const config: Config = { 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | plugins: [formsPlugin], 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /examples/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | }, 24 | "composite": true 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iron-session", 3 | "version": "8.0.4", 4 | "description": "Secure, stateless, and cookie-based session library for JavaScript", 5 | "keywords": [ 6 | "session", 7 | "secure", 8 | "stateless", 9 | "cookie", 10 | "encryption", 11 | "security", 12 | "next.js", 13 | "node.js" 14 | ], 15 | "bugs": "https://github.com/vvo/iron-session/issues", 16 | "repository": "github:vvo/iron-session", 17 | "funding": [ 18 | "https://github.com/sponsors/vvo", 19 | "https://github.com/sponsors/brc-dd" 20 | ], 21 | "license": "MIT", 22 | "author": "Vincent Voyer <vincent@codeagain.com> (https://github.com/vvo)", 23 | "sideEffects": false, 24 | "type": "module", 25 | "exports": { 26 | "import": "./dist/index.js", 27 | "require": "./dist/index.cjs" 28 | }, 29 | "main": "./dist/index.cjs", 30 | "files": [ 31 | "dist/*" 32 | ], 33 | "scripts": { 34 | "build": "tsup", 35 | "dev": "pnpm build && concurrently \"pnpm build --watch\" \"pnpm --filter=next-example dev\" ", 36 | "lint": "tsc --noEmit && tsc --noEmit -p examples/next/tsconfig.json && pnpm eslint . && publint", 37 | "prepare": "pnpm build && tsc --noEmit", 38 | "start": "turbo start --filter=next-example", 39 | "test": "c8 -r text -r lcov node --import tsx --test src/*.test.ts && pnpm build", 40 | "test:watch": "node --import tsx --test --watch src/*.test.ts" 41 | }, 42 | "prettier": { 43 | "plugins": [ 44 | "prettier-plugin-packagejson" 45 | ], 46 | "trailingComma": "all" 47 | }, 48 | "dependencies": { 49 | "cookie": "^0.7.2", 50 | "iron-webcrypto": "^1.2.1", 51 | "uncrypto": "^0.1.3" 52 | }, 53 | "devDependencies": { 54 | "@types/cookie": "0.6.0", 55 | "@types/node": "20.17.24", 56 | "@typescript-eslint/eslint-plugin": "7.18.0", 57 | "@typescript-eslint/parser": "7.18.0", 58 | "c8": "10.1.3", 59 | "concurrently": "8.2.2", 60 | "eslint": "8.57.1", 61 | "eslint-config-prettier": "9.1.0", 62 | "eslint-import-resolver-node": "0.3.9", 63 | "eslint-import-resolver-typescript": "3.9.1", 64 | "eslint-plugin-import": "2.31.0", 65 | "eslint-plugin-prettier": "5.2.5", 66 | "prettier": "3.5.3", 67 | "prettier-plugin-packagejson": "2.5.10", 68 | "publint": "0.3.9", 69 | "tsup": "8.4.0", 70 | "tsx": "4.19.3", 71 | "turbo": "^2.0.5", 72 | "typescript": "5.8.2" 73 | }, 74 | "packageManager": "pnpm@9.6.0", 75 | "publishConfig": { 76 | "access": "public", 77 | "registry": "https://registry.npmjs.org" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - examples/* 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "packageRules": [ 5 | { 6 | "matchUpdateTypes": ["minor", "patch"], 7 | "matchDepTypes": ["devDependencies"], 8 | "automerge": true 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /sponsor/clerk-dark.svg: -------------------------------------------------------------------------------- 1 | <svg width="110" height="32" viewBox="0 0 110 32" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <ellipse cx="16.0003" cy="16" rx="4.99998" ry="5" fill="#6C47FF" style="fill:#6C47FF;fill:color(display-p3 0.4235 0.2784 1.0000);fill-opacity:1;"/> 3 | <path d="M25.0091 27.8382C25.4345 28.2636 25.3918 28.9679 24.8919 29.3027C22.3488 31.0062 19.2899 31.9997 15.999 31.9997C12.7082 31.9997 9.64934 31.0062 7.10615 29.3027C6.60632 28.9679 6.56361 28.2636 6.989 27.8382L10.6429 24.1843C10.9732 23.854 11.4855 23.8019 11.9012 24.0148C13.1302 24.6445 14.5232 24.9997 15.999 24.9997C17.4749 24.9997 18.8678 24.6445 20.0969 24.0148C20.5126 23.8019 21.0249 23.854 21.3552 24.1843L25.0091 27.8382Z" fill="#6C47FF" style="fill:#6C47FF;fill:color(display-p3 0.4235 0.2784 1.0000);fill-opacity:1;"/> 4 | <path d="M24.8928 2.697C25.3926 3.0318 25.4353 3.73609 25.0099 4.16149L21.356 7.81544C21.0258 8.14569 20.5134 8.19785 20.0978 7.98491C18.8687 7.35525 17.4758 7 15.9999 7C11.0294 7 6.99997 11.0294 6.99997 16C6.99997 17.4759 7.35522 18.8688 7.98488 20.0979C8.19782 20.5136 8.14565 21.0259 7.81541 21.3561L4.16147 25.0101C3.73607 25.4355 3.03178 25.3927 2.69698 24.8929C0.993522 22.3497 0 19.2909 0 16C0 7.16344 7.16341 0 15.9999 0C19.2908 0 22.3496 0.993529 24.8928 2.697Z" fill="#BAB1FF" style="fill:#BAB1FF;fill:color(display-p3 0.7294 0.6941 1.0000);fill-opacity:1;"/> 5 | <path fill-rule="evenodd" clip-rule="evenodd" d="M100.405 21.2489C100.421 21.2324 100.442 21.2231 100.465 21.2231C100.493 21.2231 100.518 21.2375 100.533 21.2613L105.275 28.8821C105.321 28.9554 105.401 29 105.487 29L109.75 29C109.946 29 110.066 28.7848 109.963 28.6183L103.457 18.1226C103.399 18.0278 103.41 17.9056 103.485 17.823L109.752 10.908C109.898 10.7473 109.784 10.4901 109.567 10.4901H105.12C105.05 10.4901 104.983 10.5194 104.936 10.5711L97.6842 18.4755C97.5301 18.6435 97.25 18.5345 97.25 18.3065V3.25C97.25 3.11193 97.138 3 97 3H93.25C93.1119 3 93 3.11193 93 3.25V28.75C93 28.8881 93.1119 29 93.25 29L97 29C97.138 29 97.25 28.8881 97.25 28.75V24.7373C97.25 24.6741 97.2739 24.6132 97.317 24.567L100.405 21.2489ZM52.2502 3.25C52.2502 3.11193 52.3621 3 52.5002 3H56.2501C56.3882 3 56.5001 3.11193 56.5001 3.25V28.75C56.5001 28.8881 56.3882 29 56.2501 29H52.5002C52.3621 29 52.2502 28.8881 52.2502 28.75V3.25ZM46.958 23.5912C46.8584 23.5052 46.7094 23.5117 46.6137 23.602C46.0293 24.1537 45.3447 24.595 44.5947 24.9028C43.7719 25.2407 42.8873 25.4108 41.995 25.4028C41.2415 25.4252 40.4913 25.2963 39.7906 25.0241C39.09 24.7519 38.4537 24.3422 37.9209 23.8202C36.9531 22.8322 36.396 21.4215 36.396 19.7399C36.396 16.3735 38.6356 14.0709 41.995 14.0709C42.896 14.0585 43.7888 14.241 44.6094 14.6052C45.3533 14.9355 46.0214 15.4077 46.5748 15.9934C46.6694 16.0936 46.8266 16.1052 46.9309 16.015L49.4625 13.8244C49.5659 13.7349 49.5785 13.5786 49.4873 13.4767C47.583 11.3488 44.5997 10.25 41.7627 10.25C36.0506 10.25 32.0003 14.1031 32.0003 19.7719C32.0003 22.5756 33.0069 24.9365 34.7044 26.6036C36.402 28.2707 38.8203 29.25 41.6108 29.25C45.1097 29.25 47.9259 27.9082 49.577 26.187C49.6739 26.086 49.6632 25.9252 49.5572 25.8338L46.958 23.5912ZM77.1575 20.9877C77.1436 21.1129 77.0371 21.2066 76.9111 21.2066H63.7746C63.615 21.2066 63.4961 21.3546 63.5377 21.5087C64.1913 23.9314 66.1398 25.3973 68.7994 25.3973C69.6959 25.4161 70.5846 25.2317 71.3968 24.8582C72.1536 24.5102 72.8249 24.0068 73.3659 23.3828C73.4314 23.3073 73.5454 23.2961 73.622 23.3602L76.2631 25.6596C76.3641 25.7476 76.3782 25.8999 76.2915 26.0021C74.697 27.8832 72.1135 29.25 68.5683 29.25C63.1142 29.25 59.0001 25.4731 59.0001 19.7348C59.0001 16.9197 59.9693 14.559 61.5847 12.8921C62.4374 12.0349 63.4597 11.3584 64.5882 10.9043C65.7168 10.4502 66.9281 10.2281 68.1473 10.2517C73.6753 10.2517 77.25 14.1394 77.25 19.5075C77.2431 20.0021 77.2123 20.4961 77.1575 20.9877ZM63.6166 17.5038C63.5702 17.6581 63.6894 17.8084 63.8505 17.8084H72.5852C72.7467 17.8084 72.8659 17.6572 72.8211 17.5021C72.2257 15.4416 70.7153 14.0666 68.3696 14.0666C67.6796 14.0447 66.993 14.1696 66.3565 14.4326C65.7203 14.6957 65.149 15.0908 64.6823 15.5907C64.1914 16.1473 63.8285 16.7998 63.6166 17.5038ZM90.2473 10.2527C90.3864 10.2512 90.5 10.3636 90.5 10.5027V14.7013C90.5 14.8469 90.3762 14.9615 90.2311 14.9508C89.8258 14.9207 89.4427 14.8952 89.1916 14.8952C85.9204 14.8952 84 17.1975 84 20.2195V28.75C84 28.8881 83.8881 29 83.75 29H80C79.862 29 79.75 28.8881 79.75 28.75V10.7623C79.75 10.6242 79.862 10.5123 80 10.5123H83.75C83.8881 10.5123 84 10.6242 84 10.7623V13.287C84 13.3013 84.0116 13.3128 84.0258 13.3128C84.034 13.3128 84.0416 13.3089 84.0465 13.3024C85.5124 11.3448 87.676 10.2559 89.9617 10.2559L90.2473 10.2527Z" fill="#131316" style="fill:#131316;fill:color(display-p3 0.0745 0.0745 0.0863);fill-opacity:1;"/> 6 | </svg> 7 | -------------------------------------------------------------------------------- /sponsor/clerk-light.svg: -------------------------------------------------------------------------------- 1 | <svg width="110" height="32" viewBox="0 0 110 32" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <ellipse cx="16.0003" cy="16" rx="4.99998" ry="5" fill="#9785FF" style="fill:#9785FF;fill:color(display-p3 0.5922 0.5216 1.0000);fill-opacity:1;"/> 3 | <path d="M25.0091 27.8382C25.4345 28.2636 25.3918 28.9679 24.8919 29.3027C22.3488 31.0062 19.2899 31.9997 15.9991 31.9997C12.7082 31.9997 9.64935 31.0062 7.10616 29.3027C6.60633 28.9679 6.56361 28.2636 6.98901 27.8382L10.6429 24.1843C10.9732 23.854 11.4855 23.8019 11.9012 24.0148C13.1303 24.6445 14.5232 24.9997 15.9991 24.9997C17.4749 24.9997 18.8678 24.6445 20.0969 24.0148C20.5126 23.8019 21.0249 23.854 21.3552 24.1843L25.0091 27.8382Z" fill="#9785FF" style="fill:#9785FF;fill:color(display-p3 0.5922 0.5216 1.0000);fill-opacity:1;"/> 4 | <path opacity="0.6" d="M24.8928 2.697C25.3926 3.0318 25.4353 3.73609 25.0099 4.16149L21.356 7.81544C21.0258 8.14569 20.5134 8.19785 20.0978 7.98491C18.8687 7.35525 17.4758 7 15.9999 7C11.0294 7 6.99997 11.0294 6.99997 16C6.99997 17.4759 7.35522 18.8688 7.98488 20.0979C8.19782 20.5136 8.14565 21.0259 7.81541 21.3561L4.16147 25.0101C3.73607 25.4355 3.03178 25.3927 2.69698 24.8929C0.993522 22.3497 0 19.2909 0 16C0 7.16344 7.16341 0 15.9999 0C19.2908 0 22.3496 0.993529 24.8928 2.697Z" fill="#9785FF" style="fill:#9785FF;fill:color(display-p3 0.5922 0.5216 1.0000);fill-opacity:1;"/> 5 | <path fill-rule="evenodd" clip-rule="evenodd" d="M100.405 21.2489C100.421 21.2324 100.442 21.2231 100.465 21.2231C100.493 21.2231 100.518 21.2375 100.533 21.2613L105.275 28.8821C105.321 28.9554 105.401 29 105.487 29L109.75 29C109.946 29 110.066 28.7848 109.963 28.6183L103.457 18.1226C103.399 18.0278 103.41 17.9056 103.485 17.823L109.752 10.908C109.898 10.7473 109.784 10.4901 109.567 10.4901H105.12C105.05 10.4901 104.983 10.5194 104.936 10.5711L97.6842 18.4755C97.5301 18.6435 97.25 18.5345 97.25 18.3065V3.25C97.25 3.11193 97.138 3 97 3H93.25C93.1119 3 93 3.11193 93 3.25V28.75C93 28.8881 93.1119 29 93.25 29L97 29C97.138 29 97.25 28.8881 97.25 28.75V24.7373C97.25 24.6741 97.2739 24.6132 97.317 24.567L100.405 21.2489ZM52.2502 3.25C52.2502 3.11193 52.3621 3 52.5002 3H56.2501C56.3882 3 56.5001 3.11193 56.5001 3.25V28.75C56.5001 28.8881 56.3882 29 56.2501 29H52.5002C52.3621 29 52.2502 28.8881 52.2502 28.75V3.25ZM46.958 23.5912C46.8584 23.5052 46.7094 23.5117 46.6137 23.602C46.0293 24.1537 45.3447 24.595 44.5947 24.9028C43.7719 25.2407 42.8873 25.4108 41.995 25.4028C41.2415 25.4252 40.4913 25.2963 39.7906 25.0241C39.09 24.7519 38.4537 24.3422 37.9209 23.8202C36.9531 22.8322 36.396 21.4215 36.396 19.7399C36.396 16.3735 38.6356 14.0709 41.995 14.0709C42.896 14.0585 43.7888 14.241 44.6094 14.6052C45.3533 14.9355 46.0214 15.4077 46.5748 15.9934C46.6694 16.0936 46.8266 16.1052 46.9309 16.015L49.4625 13.8244C49.5659 13.7349 49.5785 13.5786 49.4873 13.4767C47.583 11.3488 44.5997 10.25 41.7627 10.25C36.0506 10.25 32.0003 14.1031 32.0003 19.7719C32.0003 22.5756 33.0069 24.9365 34.7044 26.6036C36.402 28.2707 38.8203 29.25 41.6108 29.25C45.1097 29.25 47.9259 27.9082 49.577 26.187C49.6739 26.086 49.6632 25.9252 49.5572 25.8338L46.958 23.5912ZM77.1575 20.9877C77.1436 21.1129 77.0371 21.2066 76.9111 21.2066H63.7746C63.615 21.2066 63.4961 21.3546 63.5377 21.5087C64.1913 23.9314 66.1398 25.3973 68.7994 25.3973C69.6959 25.4161 70.5846 25.2317 71.3968 24.8582C72.1536 24.5102 72.8249 24.0068 73.3659 23.3828C73.4314 23.3073 73.5454 23.2961 73.622 23.3602L76.2631 25.6596C76.3641 25.7476 76.3782 25.8999 76.2915 26.0021C74.697 27.8832 72.1135 29.25 68.5683 29.25C63.1142 29.25 59.0001 25.4731 59.0001 19.7348C59.0001 16.9197 59.9693 14.559 61.5847 12.8921C62.4374 12.0349 63.4597 11.3584 64.5882 10.9043C65.7168 10.4502 66.9281 10.2281 68.1473 10.2517C73.6753 10.2517 77.25 14.1394 77.25 19.5075C77.2431 20.0021 77.2123 20.4961 77.1575 20.9877ZM63.6166 17.5038C63.5702 17.6581 63.6894 17.8084 63.8505 17.8084H72.5852C72.7467 17.8084 72.8659 17.6572 72.8211 17.5021C72.2257 15.4416 70.7153 14.0666 68.3696 14.0666C67.6796 14.0447 66.993 14.1696 66.3565 14.4326C65.7203 14.6957 65.149 15.0908 64.6823 15.5907C64.1914 16.1473 63.8285 16.7998 63.6166 17.5038ZM90.2473 10.2527C90.3864 10.2512 90.5 10.3636 90.5 10.5027V14.7013C90.5 14.8469 90.3762 14.9615 90.2311 14.9508C89.8258 14.9207 89.4427 14.8952 89.1916 14.8952C85.9204 14.8952 84 17.1975 84 20.2195V28.75C84 28.8881 83.8881 29 83.75 29H80C79.862 29 79.75 28.8881 79.75 28.75V10.7623C79.75 10.6242 79.862 10.5123 80 10.5123H83.75C83.8881 10.5123 84 10.6242 84 10.7623V13.287C84 13.3013 84.0116 13.3128 84.0258 13.3128C84.034 13.3128 84.0416 13.3089 84.0465 13.3024C85.5124 11.3448 87.676 10.2559 89.9617 10.2559L90.2473 10.2527Z" fill="white" style="fill:white;fill:white;fill-opacity:1;"/> 6 | </svg> 7 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "http"; 2 | import { parse, serialize, type CookieSerializeOptions } from "cookie"; 3 | import { 4 | defaults as ironDefaults, 5 | seal as ironSeal, 6 | unseal as ironUnseal, 7 | } from "iron-webcrypto"; 8 | 9 | type PasswordsMap = Record<string, string>; 10 | type Password = PasswordsMap | string; 11 | type RequestType = IncomingMessage | Request; 12 | type ResponseType = Response | ServerResponse; 13 | 14 | /** 15 | * {@link https://wicg.github.io/cookie-store/#dictdef-cookielistitem CookieListItem} 16 | * as specified by W3C. 17 | */ 18 | interface CookieListItem 19 | extends Pick< 20 | CookieSerializeOptions, 21 | "domain" | "path" | "sameSite" | "secure" 22 | > { 23 | /** A string with the name of a cookie. */ 24 | name: string; 25 | /** A string containing the value of the cookie. */ 26 | value: string; 27 | /** A number of milliseconds or Date interface containing the expires of the cookie. */ 28 | expires?: CookieSerializeOptions["expires"] | number; 29 | } 30 | 31 | /** 32 | * Superset of {@link CookieListItem} extending it with 33 | * the `httpOnly`, `maxAge` and `priority` properties. 34 | */ 35 | type ResponseCookie = CookieListItem & 36 | Pick<CookieSerializeOptions, "httpOnly" | "maxAge" | "priority">; 37 | 38 | /** 39 | * The high-level type definition of the .get() and .set() methods 40 | * of { cookies() } from "next/headers" 41 | */ 42 | export interface CookieStore { 43 | get: (name: string) => { name: string; value: string } | undefined; 44 | set: { 45 | (name: string, value: string, cookie?: Partial<ResponseCookie>): void; 46 | (options: ResponseCookie): void; 47 | }; 48 | } 49 | 50 | /** 51 | * Set-Cookie Attributes do not include `encode`. We omit this from our `cookieOptions` type. 52 | * 53 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie 54 | * @see https://developer.chrome.com/docs/devtools/application/cookies/ 55 | */ 56 | type CookieOptions = Omit<CookieSerializeOptions, "encode">; 57 | 58 | export interface SessionOptions { 59 | /** 60 | * The cookie name that will be used inside the browser. Make sure it's unique 61 | * given your application. 62 | * 63 | * @example 'vercel-session' 64 | */ 65 | cookieName: string; 66 | 67 | /** 68 | * The password(s) that will be used to encrypt the cookie. Can either be a string 69 | * or an object. 70 | * 71 | * When you provide multiple passwords then all of them will be used to decrypt 72 | * the cookie. But only the most recent (`= highest key`, `2` in the example) 73 | * password will be used to encrypt the cookie. This allows password rotation. 74 | * 75 | * @example { 1: 'password-1', 2: 'password-2' } 76 | */ 77 | password: Password; 78 | 79 | /** 80 | * The time (in seconds) that the session will be valid for. Also sets the 81 | * `max-age` attribute of the cookie automatically (`= ttl - 60s`, so that the 82 | * cookie always expire before the session). 83 | * 84 | * `ttl = 0` means no expiration. 85 | * 86 | * @default 1209600 87 | */ 88 | ttl?: number; 89 | 90 | /** 91 | * The options that will be passed to the cookie library. 92 | * 93 | * If you want to use "session cookies" (cookies that are deleted when the browser 94 | * is closed) then you need to pass `cookieOptions: { maxAge: undefined }` 95 | * 96 | * @see https://github.com/jshttp/cookie#options-1 97 | */ 98 | cookieOptions?: CookieOptions; 99 | } 100 | 101 | export type IronSession<T> = T & { 102 | /** 103 | * Encrypts the session data and sets the cookie. 104 | */ 105 | readonly save: () => Promise<void>; 106 | 107 | /** 108 | * Destroys the session data and removes the cookie. 109 | */ 110 | readonly destroy: () => void; 111 | 112 | /** 113 | * Update the session configuration. You still need to call save() to send the new cookie. 114 | */ 115 | readonly updateConfig: (newSessionOptions: SessionOptions) => void; 116 | }; 117 | 118 | // default time allowed to check for iron seal validity when ttl passed 119 | // see https://hapi.dev/module/iron/api/?v=7.0.1#options 120 | const timestampSkewSec = 60; 121 | const fourteenDaysInSeconds = 14 * 24 * 3600; 122 | 123 | // We store a token major version to handle data format changes so that the cookies 124 | // can be kept alive between upgrades, no need to disconnect everyone. 125 | const currentMajorVersion = 2; 126 | const versionDelimiter = "~"; 127 | 128 | const defaultOptions: Required<Pick<SessionOptions, "ttl" | "cookieOptions">> = 129 | { 130 | ttl: fourteenDaysInSeconds, 131 | cookieOptions: { httpOnly: true, secure: true, sameSite: "lax", path: "/" }, 132 | }; 133 | 134 | function normalizeStringPasswordToMap(password: Password): PasswordsMap { 135 | return typeof password === "string" ? { 1: password } : password; 136 | } 137 | 138 | function parseSeal(seal: string): { 139 | sealWithoutVersion: string; 140 | tokenVersion: number | null; 141 | } { 142 | const [sealWithoutVersion, tokenVersionAsString] = 143 | seal.split(versionDelimiter); 144 | const tokenVersion = 145 | tokenVersionAsString == null ? null : parseInt(tokenVersionAsString, 10); 146 | 147 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 148 | return { sealWithoutVersion: sealWithoutVersion!, tokenVersion }; 149 | } 150 | 151 | function computeCookieMaxAge(ttl: number): number { 152 | if (ttl === 0) { 153 | // ttl = 0 means no expiration 154 | // but in reality cookies have to expire (can't have no max-age) 155 | // 2147483647 is the max value for max-age in cookies 156 | // see https://stackoverflow.com/a/11685301/147079 157 | return 2147483647; 158 | } 159 | 160 | // The next line makes sure browser will expire cookies before seals are considered expired by the server. 161 | // It also allows for clock difference of 60 seconds between server and clients. 162 | return ttl - timestampSkewSec; 163 | } 164 | 165 | function getCookie(req: RequestType, cookieName: string): string { 166 | return ( 167 | parse( 168 | ("headers" in req && typeof req.headers.get === "function" 169 | ? req.headers.get("cookie") 170 | : (req as IncomingMessage).headers.cookie) ?? "", 171 | )[cookieName] ?? "" 172 | ); 173 | } 174 | 175 | function getServerActionCookie( 176 | cookieName: string, 177 | cookieHandler: CookieStore, 178 | ): string { 179 | const cookieObject = cookieHandler.get(cookieName); 180 | const cookie = cookieObject?.value; 181 | if (typeof cookie === "string") { 182 | return cookie; 183 | } 184 | return ""; 185 | } 186 | 187 | function setCookie(res: ResponseType, cookieValue: string): void { 188 | if ("headers" in res && typeof res.headers.append === "function") { 189 | res.headers.append("set-cookie", cookieValue); 190 | return; 191 | } 192 | let existingSetCookie = (res as ServerResponse).getHeader("set-cookie") ?? []; 193 | if (!Array.isArray(existingSetCookie)) { 194 | existingSetCookie = [existingSetCookie.toString()]; 195 | } 196 | (res as ServerResponse).setHeader("set-cookie", [ 197 | ...existingSetCookie, 198 | cookieValue, 199 | ]); 200 | } 201 | 202 | export function createSealData(_crypto: Crypto) { 203 | return async function sealData( 204 | data: unknown, 205 | { 206 | password, 207 | ttl = fourteenDaysInSeconds, 208 | }: { password: Password; ttl?: number }, 209 | ): Promise<string> { 210 | const passwordsMap = normalizeStringPasswordToMap(password); 211 | 212 | const mostRecentPasswordId = Math.max( 213 | ...Object.keys(passwordsMap).map(Number), 214 | ); 215 | const passwordForSeal = { 216 | id: mostRecentPasswordId.toString(), 217 | secret: passwordsMap[mostRecentPasswordId]!, 218 | }; 219 | 220 | const seal = await ironSeal(_crypto, data, passwordForSeal, { 221 | ...ironDefaults, 222 | ttl: ttl * 1000, 223 | }); 224 | 225 | return `${seal}${versionDelimiter}${currentMajorVersion}`; 226 | }; 227 | } 228 | 229 | export function createUnsealData(_crypto: Crypto) { 230 | return async function unsealData<T>( 231 | seal: string, 232 | { 233 | password, 234 | ttl = fourteenDaysInSeconds, 235 | }: { password: Password; ttl?: number }, 236 | ): Promise<T> { 237 | const passwordsMap = normalizeStringPasswordToMap(password); 238 | const { sealWithoutVersion, tokenVersion } = parseSeal(seal); 239 | 240 | try { 241 | const data = 242 | (await ironUnseal(_crypto, sealWithoutVersion, passwordsMap, { 243 | ...ironDefaults, 244 | ttl: ttl * 1000, 245 | })) ?? {}; 246 | 247 | if (tokenVersion === 2) { 248 | return data as T; 249 | } 250 | 251 | // @ts-expect-error `persistent` does not exist on newer tokens 252 | return { ...data.persistent } as T; 253 | } catch (error) { 254 | if ( 255 | error instanceof Error && 256 | /^(Expired seal|Bad hmac value|Cannot find password|Incorrect number of sealed components)/.test( 257 | error.message, 258 | ) 259 | ) { 260 | // if seal expired or 261 | // if seal is not valid (encrypted using a different password, when passwords are badly rotated) or 262 | // if we can't find back the password in the seal 263 | // then we just start a new session over 264 | return {} as T; 265 | } 266 | 267 | throw error; 268 | } 269 | }; 270 | } 271 | 272 | function getSessionConfig( 273 | sessionOptions: SessionOptions, 274 | ): Required<SessionOptions> { 275 | const options = { 276 | ...defaultOptions, 277 | ...sessionOptions, 278 | cookieOptions: { 279 | ...defaultOptions.cookieOptions, 280 | ...(sessionOptions.cookieOptions || {}), 281 | }, 282 | }; 283 | 284 | if ( 285 | sessionOptions.cookieOptions && 286 | "maxAge" in sessionOptions.cookieOptions 287 | ) { 288 | if (sessionOptions.cookieOptions.maxAge === undefined) { 289 | // session cookies, do not set maxAge, consider token as infinite 290 | options.ttl = 0; 291 | } 292 | } else { 293 | options.cookieOptions.maxAge = computeCookieMaxAge(options.ttl); 294 | } 295 | 296 | return options; 297 | } 298 | 299 | const badUsageMessage = 300 | "iron-session: Bad usage: use getIronSession(req, res, options) or getIronSession(cookieStore, options)."; 301 | 302 | export function createGetIronSession( 303 | sealData: ReturnType<typeof createSealData>, 304 | unsealData: ReturnType<typeof createUnsealData>, 305 | ) { 306 | return getIronSession; 307 | 308 | async function getIronSession<T extends object>( 309 | cookies: CookieStore, 310 | sessionOptions: SessionOptions, 311 | ): Promise<IronSession<T>>; 312 | async function getIronSession<T extends object>( 313 | req: RequestType, 314 | res: ResponseType, 315 | sessionOptions: SessionOptions, 316 | ): Promise<IronSession<T>>; 317 | async function getIronSession<T extends object>( 318 | reqOrCookieStore: RequestType | CookieStore, 319 | resOrsessionOptions: ResponseType | SessionOptions, 320 | sessionOptions?: SessionOptions, 321 | ): Promise<IronSession<T>> { 322 | if (!reqOrCookieStore) { 323 | throw new Error(badUsageMessage); 324 | } 325 | 326 | if (!resOrsessionOptions) { 327 | throw new Error(badUsageMessage); 328 | } 329 | 330 | if (!sessionOptions) { 331 | return getIronSessionFromCookieStore<T>( 332 | reqOrCookieStore as CookieStore, 333 | resOrsessionOptions as SessionOptions, 334 | sealData, 335 | unsealData, 336 | ); 337 | } 338 | 339 | const req = reqOrCookieStore as RequestType; 340 | const res = resOrsessionOptions as ResponseType; 341 | 342 | if (!sessionOptions) { 343 | throw new Error(badUsageMessage); 344 | } 345 | 346 | if (!sessionOptions.cookieName) { 347 | throw new Error("iron-session: Bad usage. Missing cookie name."); 348 | } 349 | 350 | if (!sessionOptions.password) { 351 | throw new Error("iron-session: Bad usage. Missing password."); 352 | } 353 | 354 | const passwordsMap = normalizeStringPasswordToMap(sessionOptions.password); 355 | 356 | if (Object.values(passwordsMap).some((password) => password.length < 32)) { 357 | throw new Error( 358 | "iron-session: Bad usage. Password must be at least 32 characters long.", 359 | ); 360 | } 361 | 362 | let sessionConfig = getSessionConfig(sessionOptions); 363 | 364 | const sealFromCookies = getCookie(req, sessionConfig.cookieName); 365 | const session = sealFromCookies 366 | ? await unsealData<T>(sealFromCookies, { 367 | password: passwordsMap, 368 | ttl: sessionConfig.ttl, 369 | }) 370 | : ({} as T); 371 | 372 | Object.defineProperties(session, { 373 | updateConfig: { 374 | value: function updateConfig(newSessionOptions: SessionOptions) { 375 | sessionConfig = getSessionConfig(newSessionOptions); 376 | }, 377 | }, 378 | save: { 379 | value: async function save() { 380 | if ("headersSent" in res && res.headersSent) { 381 | throw new Error( 382 | "iron-session: Cannot set session cookie: session.save() was called after headers were sent. Make sure to call it before any res.send() or res.end()", 383 | ); 384 | } 385 | 386 | const seal = await sealData(session, { 387 | password: passwordsMap, 388 | ttl: sessionConfig.ttl, 389 | }); 390 | const cookieValue = serialize( 391 | sessionConfig.cookieName, 392 | seal, 393 | sessionConfig.cookieOptions, 394 | ); 395 | 396 | if (cookieValue.length > 4096) { 397 | throw new Error( 398 | `iron-session: Cookie length is too big (${cookieValue.length} bytes), browsers will refuse it. Try to remove some data.`, 399 | ); 400 | } 401 | 402 | setCookie(res, cookieValue); 403 | }, 404 | }, 405 | 406 | destroy: { 407 | value: function destroy() { 408 | Object.keys(session).forEach((key) => { 409 | delete (session as Record<string, unknown>)[key]; 410 | }); 411 | const cookieValue = serialize(sessionConfig.cookieName, "", { 412 | ...sessionConfig.cookieOptions, 413 | maxAge: 0, 414 | }); 415 | 416 | setCookie(res, cookieValue); 417 | }, 418 | }, 419 | }); 420 | 421 | return session as IronSession<T>; 422 | } 423 | } 424 | 425 | async function getIronSessionFromCookieStore<T extends object>( 426 | cookieStore: CookieStore, 427 | sessionOptions: SessionOptions, 428 | sealData: ReturnType<typeof createSealData>, 429 | unsealData: ReturnType<typeof createUnsealData>, 430 | ): Promise<IronSession<T>> { 431 | if (!sessionOptions.cookieName) { 432 | throw new Error("iron-session: Bad usage. Missing cookie name."); 433 | } 434 | 435 | if (!sessionOptions.password) { 436 | throw new Error("iron-session: Bad usage. Missing password."); 437 | } 438 | 439 | const passwordsMap = normalizeStringPasswordToMap(sessionOptions.password); 440 | 441 | if (Object.values(passwordsMap).some((password) => password.length < 32)) { 442 | throw new Error( 443 | "iron-session: Bad usage. Password must be at least 32 characters long.", 444 | ); 445 | } 446 | 447 | let sessionConfig = getSessionConfig(sessionOptions); 448 | const sealFromCookies = getServerActionCookie( 449 | sessionConfig.cookieName, 450 | cookieStore, 451 | ); 452 | const session = sealFromCookies 453 | ? await unsealData<T>(sealFromCookies, { 454 | password: passwordsMap, 455 | ttl: sessionConfig.ttl, 456 | }) 457 | : ({} as T); 458 | 459 | Object.defineProperties(session, { 460 | updateConfig: { 461 | value: function updateConfig(newSessionOptions: SessionOptions) { 462 | sessionConfig = getSessionConfig(newSessionOptions); 463 | }, 464 | }, 465 | save: { 466 | value: async function save() { 467 | const seal = await sealData(session, { 468 | password: passwordsMap, 469 | ttl: sessionConfig.ttl, 470 | }); 471 | 472 | const cookieLength = 473 | sessionConfig.cookieName.length + 474 | seal.length + 475 | JSON.stringify(sessionConfig.cookieOptions).length; 476 | 477 | if (cookieLength > 4096) { 478 | throw new Error( 479 | `iron-session: Cookie length is too big (${cookieLength} bytes), browsers will refuse it. Try to remove some data.`, 480 | ); 481 | } 482 | 483 | cookieStore.set( 484 | sessionConfig.cookieName, 485 | seal, 486 | sessionConfig.cookieOptions, 487 | ); 488 | }, 489 | }, 490 | 491 | destroy: { 492 | value: function destroy() { 493 | Object.keys(session).forEach((key) => { 494 | delete (session as Record<string, unknown>)[key]; 495 | }); 496 | 497 | const cookieOptions = { ...sessionConfig.cookieOptions, maxAge: 0 }; 498 | cookieStore.set(sessionConfig.cookieName, "", cookieOptions); 499 | }, 500 | }, 501 | }); 502 | 503 | return session as IronSession<T>; 504 | } 505 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual, doesNotMatch, equal, match, rejects } from "node:assert"; 2 | import { mock, test } from "node:test"; 3 | import type { IncomingMessage, ServerResponse } from "node:http"; 4 | import type { SessionOptions } from "./index.js"; 5 | import { getIronSession, sealData } from "./index.js"; 6 | 7 | const password = "Gbm49ATjnqnkCCCdhV4uDBhbfnPqsCW0"; 8 | const cookieName = "test"; 9 | 10 | interface Data { 11 | user?: { id: number; meta?: string }; 12 | } 13 | 14 | const getSession = async ( 15 | req: IncomingMessage | Request, 16 | res: Response | ServerResponse, 17 | options: SessionOptions, 18 | ) => getIronSession<Data>(req, res, options); 19 | 20 | await test("should throw if the request parameter is missing", async () => { 21 | await rejects( 22 | // @ts-expect-error we're verifying JavaScript runtime checks here (DX) 23 | getSession(), 24 | "Error: iron-session: Bad usage: use getIronSession(req, res, options) or getIronSession(cookies, options).", 25 | ); 26 | }); 27 | 28 | await test("should throw if the response parameter is missing", async () => { 29 | await rejects( 30 | // @ts-expect-error we're verifying JavaScript runtime checks here (DX) 31 | getSession({}), 32 | "Error: iron-session: Bad usage: use getIronSession(req, res, options) or getIronSession(cookies, options).", 33 | ); 34 | }); 35 | 36 | await test("should throw if the cookie name is missing in options", async () => { 37 | await rejects( 38 | getSession({} as Request, {} as Response, {} as SessionOptions), 39 | /Missing cookie name/, 40 | ); 41 | }); 42 | 43 | await test("should throw if password is missing in options", async () => { 44 | await rejects( 45 | getSession({} as Request, {} as Response, { cookieName } as SessionOptions), 46 | /Missing password/, 47 | ); 48 | }); 49 | 50 | await test("should throw if password is less than 32 characters", async () => { 51 | await rejects( 52 | getSession({} as Request, {} as Response, { 53 | cookieName, 54 | password: "123456789012345678901234567890", 55 | }), 56 | /Password must be at least 32 characters long/, 57 | ); 58 | }); 59 | 60 | await test("should return blank session if no cookie is set", async () => { 61 | const session = await getSession({ headers: {} } as Request, {} as Response, { 62 | cookieName, 63 | password, 64 | }); 65 | deepEqual(session, {}); 66 | }); 67 | 68 | await test("should set a cookie in the response object on save", async () => { 69 | const res = { 70 | getHeader: mock.fn(), 71 | setHeader: mock.fn(), 72 | }; 73 | 74 | const session = await getSession( 75 | { headers: {} } as Request, 76 | res as unknown as ServerResponse, 77 | { 78 | cookieName, 79 | password, 80 | }, 81 | ); 82 | session.user = { id: 1 }; 83 | await session.save(); 84 | 85 | const [name, value] = res.setHeader.mock.calls[0]?.arguments ?? []; 86 | equal(name, "set-cookie"); 87 | match( 88 | value[0], 89 | /^test=.{265}; Max-Age=1209540; Path=\/; HttpOnly; Secure; SameSite=Lax$/, 90 | ); 91 | 92 | mock.reset(); 93 | }); 94 | 95 | await test("should allow deleting then saving session data", async () => { 96 | const res = { getHeader: mock.fn(), setHeader: mock.fn() }; 97 | 98 | let session = await getSession( 99 | { headers: {} } as Request, 100 | res as unknown as ServerResponse, 101 | { 102 | cookieName, 103 | password, 104 | }, 105 | ); 106 | session.user = { id: 1 }; 107 | await session.save(); 108 | 109 | let cookie = res.setHeader.mock.calls[0]?.arguments[1][0].split(";")[0]; 110 | session = await getSession( 111 | { headers: { cookie } } as IncomingMessage, 112 | res as unknown as ServerResponse, 113 | { 114 | cookieName, 115 | password, 116 | }, 117 | ); 118 | deepEqual(session, { user: { id: 1 } }); 119 | 120 | delete session.user; 121 | await session.save(); 122 | 123 | cookie = res.setHeader.mock.calls[1]?.arguments[1][0].split(";")[0]; 124 | session = await getSession( 125 | { headers: { cookie } } as IncomingMessage, 126 | res as unknown as ServerResponse, 127 | { 128 | cookieName, 129 | password, 130 | }, 131 | ); 132 | deepEqual(session, {}); 133 | 134 | mock.reset(); 135 | }); 136 | 137 | await test("should set max-age to a large number if ttl is 0", async () => { 138 | const res = { getHeader: mock.fn(), setHeader: mock.fn() }; 139 | 140 | const session = await getSession( 141 | { headers: {} } as IncomingMessage, 142 | res as unknown as ServerResponse, 143 | { 144 | cookieName, 145 | password, 146 | ttl: 0, 147 | }, 148 | ); 149 | session.user = { id: 1 }; 150 | await session.save(); 151 | 152 | const cookie = res.setHeader.mock.calls[0]?.arguments[1][0]; 153 | match(cookie, /Max-Age=2147483647;/); 154 | 155 | mock.reset(); 156 | }); 157 | 158 | await test("should respect provided max-age in cookie options", async () => { 159 | const res = { getHeader: mock.fn(), setHeader: mock.fn() }; 160 | const options = { cookieName, password, cookieOptions: { maxAge: 60 } }; 161 | 162 | const session = await getSession( 163 | { headers: {} } as IncomingMessage, 164 | res as unknown as ServerResponse, 165 | options, 166 | ); 167 | session.user = { id: 1 }; 168 | await session.save(); 169 | 170 | const cookie = res.setHeader.mock.calls[0]?.arguments[1][0]; 171 | match(cookie, /Max-Age=60;/); 172 | 173 | mock.reset(); 174 | }); 175 | 176 | await test("should not set max-age for session cookies", async () => { 177 | const res = { getHeader: mock.fn(), setHeader: mock.fn() }; 178 | const options = { 179 | cookieName, 180 | password, 181 | cookieOptions: { maxAge: undefined }, 182 | }; 183 | 184 | const session = await getSession( 185 | { headers: {} } as IncomingMessage, 186 | res as unknown as ServerResponse, 187 | options, 188 | ); 189 | session.user = { id: 1 }; 190 | await session.save(); 191 | 192 | const cookie = res.setHeader.mock.calls[0]?.arguments[1][0]; 193 | doesNotMatch(cookie, /Max-Age/); 194 | 195 | mock.reset(); 196 | }); 197 | 198 | await test("should expire the cookie on destroying the session", async () => { 199 | const res = { getHeader: mock.fn(), setHeader: mock.fn() }; 200 | 201 | const session = await getSession( 202 | { headers: {} } as IncomingMessage, 203 | res as unknown as ServerResponse, 204 | { 205 | cookieName, 206 | password, 207 | }, 208 | ); 209 | session.user = { id: 1 }; 210 | await session.save(); 211 | 212 | let cookie = res.setHeader.mock.calls[0]?.arguments[1][0]; 213 | match(cookie, /Max-Age=1209540;/); 214 | 215 | deepEqual(session, { user: { id: 1 } }); 216 | session.destroy(); 217 | deepEqual(session, {}); 218 | 219 | cookie = res.setHeader.mock.calls[1]?.arguments[1][0]; 220 | match(cookie, /Max-Age=0;/); 221 | 222 | mock.reset(); 223 | }); 224 | 225 | await test("should reset the session if the seal is expired", async () => { 226 | const real = Date.now; 227 | Date.now = () => 0; 228 | 229 | const seal = await sealData({ user: { id: 1 } }, { password, ttl: 60 }); 230 | const req = { 231 | headers: { cookie: `${cookieName}=${seal}` }, 232 | } as IncomingMessage; 233 | 234 | let session = await getSession(req, {} as unknown as ServerResponse, { 235 | cookieName, 236 | password, 237 | }); 238 | deepEqual(session, { user: { id: 1 } }); 239 | 240 | Date.now = () => 120_000; // = ttl + 60s skew 241 | 242 | session = await getSession(req, {} as unknown as ServerResponse, { 243 | cookieName, 244 | password, 245 | }); 246 | deepEqual(session, {}); 247 | 248 | Date.now = real; 249 | }); 250 | 251 | await test("should refresh the session (ttl, max-age) on save", async () => { 252 | const res = { getHeader: mock.fn(), setHeader: mock.fn() }; 253 | const options = { cookieName, password, ttl: 61 }; 254 | 255 | const real = Date.now; 256 | Date.now = () => 0; 257 | 258 | let session = await getSession( 259 | { headers: {} } as IncomingMessage, 260 | res as unknown as ServerResponse, 261 | options, 262 | ); 263 | session.user = { id: 1 }; 264 | await session.save(); 265 | 266 | let cookie = res.setHeader.mock.calls[0]?.arguments[1][0]; 267 | match(cookie, /Max-Age=1;/); 268 | 269 | Date.now = () => 120_000; // < ttl + 60s skew 270 | 271 | session = await getSession( 272 | { headers: { cookie: cookie.split(";")[0] } } as IncomingMessage, 273 | res as unknown as ServerResponse, 274 | options, 275 | ); 276 | deepEqual(session, { user: { id: 1 } }); 277 | 278 | await session.save(); // session is now valid for another ttl + 60s 279 | 280 | cookie = res.setHeader.mock.calls[1]?.arguments[1][0]; 281 | match(cookie, /Max-Age=1;/); // max-age is relative to the current time 282 | 283 | Date.now = () => 240_000; // < earlier time + ttl + 60s skew 284 | 285 | session = await getSession( 286 | { headers: { cookie: cookie.split(";")[0] } } as IncomingMessage, 287 | res as unknown as ServerResponse, 288 | options, 289 | ); 290 | deepEqual(session, { user: { id: 1 } }); // session is still valid 291 | // if ttl wasn't refreshed, session would have been reset to {} 292 | 293 | Date.now = real; 294 | mock.reset(); 295 | }); 296 | 297 | await test("should reset the session if password is changed", async () => { 298 | const firstPassword = password; 299 | const secondPassword = "12345678901234567890123456789012"; 300 | 301 | const seal = await sealData({ user: { id: 1 } }, { password: firstPassword }); 302 | const req = { headers: { cookie: `${cookieName}=${seal}` } }; 303 | 304 | const session = await getSession( 305 | req as IncomingMessage, 306 | {} as unknown as ServerResponse, 307 | { cookieName, password: secondPassword }, 308 | ); 309 | deepEqual(session, {}); 310 | }); 311 | 312 | await test("should decrypt cookie generated from older password", async () => { 313 | const firstPassword = password; 314 | const secondPassword = "12345678901234567890123456789012"; 315 | 316 | const seal = await sealData({ user: { id: 1 } }, { password: firstPassword }); 317 | const req = { headers: { cookie: `${cookieName}=${seal}` } }; 318 | 319 | const passwords = { 2: secondPassword, 1: firstPassword }; // rotation 320 | const session = await getSession( 321 | req as IncomingMessage, 322 | {} as unknown as ServerResponse, 323 | { cookieName, password: passwords }, 324 | ); 325 | deepEqual(session, { user: { id: 1 } }); 326 | }); 327 | 328 | await test("should throw if the cookie length is too big", async () => { 329 | const res = { getHeader: mock.fn(), setHeader: mock.fn() }; 330 | 331 | const session = await getSession( 332 | { headers: {} } as IncomingMessage, 333 | res as unknown as ServerResponse, 334 | { 335 | cookieName, 336 | password, 337 | }, 338 | ); 339 | session.user = { id: 1, meta: "0".repeat(3000) }; 340 | await rejects(session.save(), /Cookie length is too big/); 341 | 342 | mock.reset(); 343 | }); 344 | 345 | await test("should throw if trying to save after headers are sent", async () => { 346 | const session = await getSession( 347 | { headers: {} } as IncomingMessage, 348 | { headersSent: true } as unknown as Response, 349 | { cookieName, password }, 350 | ); 351 | session.user = { id: 1 }; 352 | 353 | await rejects( 354 | session.save(), 355 | /session.save\(\) was called after headers were sent/, 356 | ); 357 | }); 358 | 359 | await test("should keep previously set cookie - single", async () => { 360 | const existingCookie = "existing=cookie"; 361 | const res = { 362 | getHeader: mock.fn(() => existingCookie), 363 | setHeader: mock.fn(), 364 | }; 365 | 366 | const session = await getSession( 367 | { headers: {} } as IncomingMessage, 368 | res as unknown as Response, 369 | { 370 | cookieName, 371 | password, 372 | }, 373 | ); 374 | session.user = { id: 1 }; 375 | await session.save(); 376 | 377 | let cookies = res.setHeader.mock.calls[0]?.arguments[1]; 378 | deepEqual(cookies[0], existingCookie); 379 | deepEqual(cookies.length, 2); 380 | 381 | session.destroy(); 382 | 383 | cookies = res.setHeader.mock.calls[1]?.arguments[1]; 384 | deepEqual(cookies[0], existingCookie); 385 | deepEqual( 386 | cookies[1], 387 | `${cookieName}=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax`, 388 | ); 389 | 390 | mock.reset(); 391 | }); 392 | 393 | await test("should keep previously set cookies - multiple", async () => { 394 | const existingCookies = ["existing=cookie", "existing2=cookie2"]; 395 | const res = { 396 | getHeader: mock.fn(() => existingCookies), 397 | setHeader: mock.fn(), 398 | }; 399 | 400 | const session = await getSession( 401 | { headers: {} } as Request, 402 | res as unknown as Response, 403 | { 404 | cookieName, 405 | password, 406 | }, 407 | ); 408 | session.user = { id: 1 }; 409 | await session.save(); 410 | 411 | let cookies = res.setHeader.mock.calls[0]?.arguments[1]; 412 | deepEqual(cookies[0], existingCookies[0]); 413 | deepEqual(cookies[1], existingCookies[1]); 414 | deepEqual(cookies.length, 3); 415 | 416 | session.destroy(); 417 | 418 | cookies = res.setHeader.mock.calls[1]?.arguments[1]; 419 | deepEqual(cookies[0], existingCookies[0]); 420 | deepEqual(cookies[1], existingCookies[1]); 421 | deepEqual( 422 | cookies[2], 423 | `${cookieName}=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax`, 424 | ); 425 | 426 | mock.reset(); 427 | }); 428 | 429 | await test("should be backwards compatible with older cookie format", async () => { 430 | // this seal is in the old next-iron-session format (generated with ttl: 0) 431 | const cookie = `${cookieName}=Fe26.2*1*1e2bacee1edffaeb4a9ba4a07dc36c2c60d20415a60ac1b901033af1f107ead5*LAC9Fn3BJ9ifKMhVL3pP5w*JHhcByIzk4ThLt9rUW-fDMrOwUT7htHy1uyqeOTIqrVwDJ0Bz7TOAwIz_Cos-ug3**7dfa11868bbcc4f7e118342c0280ff49ba4a7cc84c70395bbc3d821a5f460174*6a8FkHxdg322jyym6PwJf3owz7pd6nq5ZIzyLHGVC0c`; 432 | 433 | const session = await getSession( 434 | { headers: { cookie } } as IncomingMessage, 435 | {} as Response, 436 | { cookieName, password }, 437 | ); 438 | deepEqual(session, { user: { id: 77 } }); 439 | }); 440 | 441 | await test("should prevent reassignment of save/destroy functions", async () => { 442 | const session = await getSession( 443 | { headers: {} } as IncomingMessage, 444 | {} as Response, 445 | { cookieName, password }, 446 | ); 447 | 448 | await rejects(async () => { 449 | // @ts-expect-error Runtime check 450 | session.save = () => {}; 451 | }, /Cannot assign to read only property 'save' of object '#<Object>'/); 452 | 453 | await rejects(async () => { 454 | // @ts-expect-error Runtime check 455 | session.destroy = () => {}; 456 | }, /Cannot assign to read only property 'destroy' of object '#<Object>'/); 457 | }); 458 | 459 | await test("allow to update session configuration", async () => { 460 | const res = { 461 | getHeader: mock.fn(), 462 | setHeader: mock.fn(), 463 | }; 464 | 465 | const session = await getSession( 466 | { headers: {} } as IncomingMessage, 467 | res as unknown as ServerResponse, 468 | { 469 | cookieName, 470 | password, 471 | }, 472 | ); 473 | session.user = { id: 1 }; 474 | 475 | session.updateConfig({ ttl: 61, cookieName: "test2", password: "ok" }); 476 | 477 | await session.save(); 478 | match(res.setHeader.mock.calls[0]?.arguments[1][0], /Max-Age=1;/); 479 | 480 | mock.reset(); 481 | }); 482 | 483 | await test("should work with standard web Request/Response APIs", async () => { 484 | const req = new Request("https://example.com"); 485 | const res = new Response("Hello, world!"); 486 | 487 | let session = await getSession(req, res, { cookieName, password }); 488 | deepEqual(session, {}); 489 | 490 | session.user = { id: 1 }; 491 | await session.save(); 492 | 493 | const cookie = res.headers.get("set-cookie") ?? ""; 494 | match( 495 | cookie, 496 | /^test=.{265}; Max-Age=1209540; Path=\/; HttpOnly; Secure; SameSite=Lax$/, 497 | ); 498 | 499 | req.headers.set("cookie", cookie.split(";")[0] ?? ""); 500 | session = await getSession(req, res, { cookieName, password }); 501 | deepEqual(session, { user: { id: 1 } }); 502 | }); 503 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createGetIronSession, 3 | createSealData, 4 | createUnsealData, 5 | } from "./core.js"; 6 | 7 | import * as crypto from "uncrypto"; 8 | 9 | export type { IronSession, SessionOptions } from "./core.js"; 10 | export const sealData = createSealData(crypto); 11 | export const unsealData = createUnsealData(crypto); 12 | export const getIronSession = createGetIronSession(sealData, unsealData); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "allowUnusedLabels": false, 6 | "baseUrl": "src", 7 | "rootDir": ".", 8 | "declaration": true, 9 | "downlevelIteration": true, 10 | "esModuleInterop": true, 11 | "exactOptionalPropertyTypes": true, 12 | "jsx": "preserve", 13 | "lib": ["ES2022", "dom"], 14 | "module": "NodeNext", 15 | "moduleResolution": "nodenext", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitOverride": true, 18 | "noImplicitReturns": true, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "outDir": "dist", 23 | "skipLibCheck": true, 24 | "strict": true, 25 | // https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping#node-18 26 | "target": "ES2022", 27 | "verbatimModuleSyntax": true 28 | }, 29 | "exclude": ["dist", "examples/next"] 30 | } 31 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["src/index.ts"], 6 | clean: true, 7 | // this setting inlines the types from dependencies we use, like `cookie` 8 | dts: { resolve: true }, 9 | format: ["esm", "cjs"], 10 | treeshake: true, 11 | sourcemap: true, 12 | }, 13 | ]); 14 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "//#build": { 5 | "outputs": [ 6 | "dist/**/*" 7 | ], 8 | "inputs": [ 9 | "version.txt" 10 | ] 11 | }, 12 | "build": { 13 | "dependsOn": [ 14 | "^build", 15 | "//#build" 16 | ], 17 | "outputs": [ 18 | ".next/**", 19 | "!.next/cache/**", 20 | "dist/**" 21 | ] 22 | }, 23 | "start": { 24 | "dependsOn": [ 25 | "^build" 26 | ] 27 | } 28 | } 29 | } 30 | --------------------------------------------------------------------------------