iron-session 2.0.0-alpha.14 → 2.0.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -55
- package/dist/index.cjs +181 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +46 -17
- package/dist/index.js +100 -76
- package/dist/index.js.map +2 -2
- package/express/dist/index.cjs +65 -0
- package/express/dist/index.cjs.map +7 -0
- package/express/dist/index.d.ts +6 -0
- package/express/dist/index.js +37 -0
- package/express/dist/index.js.map +7 -0
- package/next/dist/index.cjs +74 -0
- package/next/dist/index.cjs.map +7 -0
- package/next/dist/index.d.ts +9 -13
- package/next/dist/index.js +30 -33
- package/next/dist/index.js.map +3 -3
- package/package.json +83 -43
- package/dist/index.mjs +0 -102
- package/dist/index.mjs.map +0 -7
- package/next/dist/index.mjs +0 -19
- package/next/dist/index.mjs.map +0 -7
package/README.md
CHANGED
|
@@ -1,103 +1,346 @@
|
|
|
1
|
-
#
|
|
1
|
+
# iron-session [](https://github.com/vvo/iron-session/blob/master/LICENSE) [](https://github.com/vvo/iron-session/actions) [](https://codecov.io/gh/vvo/iron-session)  [](http://npm-stat.com/charts.html?package=iron-session) [](#contributors-)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
_🛠 Node.js stateless session utility using signed and encrypted cookies to store data. Works with Next.js, Express, NestJs, Fastify, and any Node.js HTTP framework._
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The session data is stored in encrypted cookies ("seals"). And only your server can decode the session data. There are no session ids, making iron sessions "stateless" from the server point of view.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This strategy of storing session data is the same technique used by **frameworks like [Ruby On Rails](https://guides.rubyonrails.org/security.html#session-storage)**.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The underlying cryptography library is [iron](https://hapi.dev/module/iron) which was [created by the lead developer of OAuth 2.0](https://hueniversedotcom.wordpress.com/2015/09/19/auth-to-see-the-wizard-or-i-wrote-an-oauth-replacement/).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
<p align="center"><b>Online demo at <a href="https://iron-session.now.sh/">https://iron-session.now.sh/</a> 👀</b></p>
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
_Table of contents:_
|
|
16
|
+
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Usage (Next.js)](#usage-nextjs)
|
|
19
|
+
- [Advanced usage](#advanced-usage)
|
|
20
|
+
- [Express](#express)
|
|
21
|
+
- [Handle password rotation/update the password](#handle-password-rotationupdate-the-password)
|
|
22
|
+
- [API](#api)
|
|
23
|
+
- [withIronSession(handler, { password, cookieName, [ttl], [cookieOptions] })](#withironsessionhandler--password-cookiename-ttl-cookieoptions-)
|
|
24
|
+
- [ironSession({ password, cookieName, [ttl], [cookieOptions] })](#ironsession-password-cookiename-ttl-cookieoptions-)
|
|
25
|
+
- [async applySession(req, res, { password, cookieName, [ttl], [cookieOptions] })](#async-applysessionreq-res--password-cookiename-ttl-cookieoptions-)
|
|
26
|
+
- [req.session.set(name, value)](#reqsessionsetname-value)
|
|
27
|
+
- [req.session.get(name)](#reqsessiongetname)
|
|
28
|
+
- [req.session.unset(name)](#reqsessionunsetname)
|
|
29
|
+
- [req.session.save() => promise](#reqsessionsave--promise)
|
|
30
|
+
- [req.session.destroy()](#reqsessiondestroy)
|
|
31
|
+
- [FAQ](#faq)
|
|
32
|
+
- [Why use pure 🍪 cookies for sessions?](#why-use-pure--cookies-for-sessions)
|
|
33
|
+
- [What are the drawbacks?](#what-are-the-drawbacks)
|
|
34
|
+
- [How is this different from JWT?](#how-is-this-different-from-jwt)
|
|
35
|
+
- [Project status](#project-status)
|
|
36
|
+
- [Credits](#credits)
|
|
37
|
+
- [🤓 References](#-references)
|
|
38
|
+
- [Contributors ✨](#contributors-)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
14
41
|
|
|
15
42
|
```bash
|
|
16
|
-
npm
|
|
43
|
+
npm add iron-session
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage (Next.js)
|
|
47
|
+
|
|
48
|
+
You can find full featured examples (Next.js, Express) in the [examples folder](./examples/).
|
|
49
|
+
|
|
50
|
+
The password is a private key you must pass at runtime and builtime (for getServerSideProps), it has to be at least 32 characters long. You can use https://1password.com/password-generator/ to generate strong passwords.
|
|
51
|
+
|
|
52
|
+
⚠️ Always store passwords in encrypted environment variables on your platform. Vercel does this automatically.
|
|
53
|
+
|
|
54
|
+
**Login:**
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// pages/api/login.ts
|
|
58
|
+
|
|
59
|
+
import { withIronSessionApiRoute } from "iron-session/next";
|
|
60
|
+
|
|
61
|
+
export default withIronSessionApiRoute(
|
|
62
|
+
async function handler(req, res) {
|
|
63
|
+
// get user from database then:
|
|
64
|
+
req.session.set("user", {
|
|
65
|
+
id: 230,
|
|
66
|
+
admin: true,
|
|
67
|
+
});
|
|
68
|
+
await req.session.save();
|
|
69
|
+
res.send("Logged in");
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
cookieName: "myapp_cookiename",
|
|
73
|
+
password: "complex_password_at_least_32_characters_long",
|
|
74
|
+
},
|
|
75
|
+
);
|
|
17
76
|
```
|
|
18
77
|
|
|
19
|
-
|
|
78
|
+
**Read user data:**
|
|
20
79
|
|
|
21
|
-
|
|
80
|
+
```ts
|
|
81
|
+
// pages/api/user.ts
|
|
22
82
|
|
|
23
|
-
|
|
83
|
+
import { withIronSessionApiRoute } from "iron-session/next";
|
|
24
84
|
|
|
25
|
-
|
|
85
|
+
export default withIronSessionApiRoute(
|
|
86
|
+
function handler(req, res, session) {
|
|
87
|
+
const user = req.session.get("user");
|
|
88
|
+
res.send({ user });
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
cookieName: "myapp_cookiename",
|
|
92
|
+
password: "...",
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Logout:**
|
|
26
98
|
|
|
27
|
-
|
|
99
|
+
```ts
|
|
100
|
+
// pages/api/logout.ts
|
|
28
101
|
|
|
29
|
-
|
|
102
|
+
import { withIronSessionApiRoute } from "iron-session/next";
|
|
103
|
+
|
|
104
|
+
export default withIronSessionApiRoute(
|
|
105
|
+
function handler(req, res, session) {
|
|
106
|
+
req.session.destroy();
|
|
107
|
+
res.send("Logged out");
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
cookieName: "myapp_cookiename",
|
|
111
|
+
password: "...",
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
```
|
|
30
115
|
|
|
31
|
-
|
|
116
|
+
**getServerSideProps:**
|
|
32
117
|
|
|
33
|
-
|
|
118
|
+
```ts
|
|
119
|
+
// pages/admin.tsx
|
|
34
120
|
|
|
35
|
-
|
|
121
|
+
import { withIronSessionSsr } from "iron-session/next";
|
|
36
122
|
|
|
37
|
-
|
|
123
|
+
export const getServerSideProps = withIronSessionSsr(
|
|
124
|
+
async function getServerSideProps({ req }) {
|
|
125
|
+
const user = req.session.get("user");
|
|
38
126
|
|
|
39
|
-
|
|
127
|
+
if (user.admin !== true) {
|
|
128
|
+
return {
|
|
129
|
+
notFound: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
40
132
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
133
|
+
return {
|
|
134
|
+
props: {},
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
cookieName: "myapp_cookiename",
|
|
139
|
+
password: "...",
|
|
140
|
+
},
|
|
141
|
+
);
|
|
50
142
|
```
|
|
51
143
|
|
|
52
|
-
|
|
144
|
+
Note: We encourage you to create a `withSession` utility so you do not have to repeat the password and cookie name in every route. You can see how to do that [in the example](./examples/next.js-typescript/lib/session.ts).
|
|
53
145
|
|
|
54
|
-
|
|
146
|
+
## Advanced usage
|
|
55
147
|
|
|
56
|
-
###
|
|
148
|
+
### Express
|
|
57
149
|
|
|
58
|
-
|
|
150
|
+
You can import and use `ironSession` if you want to use `iron-session` in [Express](https://expressjs.com/) and [Connect](https://github.com/senchalabs/connect).
|
|
59
151
|
|
|
60
|
-
|
|
152
|
+
```js
|
|
153
|
+
import { ironSession } from "iron-session";
|
|
154
|
+
|
|
155
|
+
const session = ironSession({
|
|
156
|
+
cookieName: "iron-session/examples/express",
|
|
157
|
+
password: process.env.SECRET_COOKIE_PASSWORD,
|
|
158
|
+
// if your localhost is served on http:// then disable the secure flag
|
|
159
|
+
cookieOptions: {
|
|
160
|
+
secure: process.env.NODE_ENV === "production",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
router.get("/profile", session, async function (req, res) {
|
|
165
|
+
// now you can access all of the req.session.* utilities
|
|
166
|
+
if (req.session.get("user") === undefined) {
|
|
167
|
+
res.redirect("/restricted");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
res.render("profile", {
|
|
172
|
+
title: "Profile",
|
|
173
|
+
userId: req.session.get("user").id,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
A more complete example using Express can be found in the [examples folder](./examples/express).
|
|
179
|
+
|
|
180
|
+
### Handle password rotation/update the password
|
|
61
181
|
|
|
62
|
-
|
|
182
|
+
When you want to:
|
|
63
183
|
|
|
64
|
-
|
|
184
|
+
- rotate passwords for better security every two (or more, or less) weeks
|
|
185
|
+
- change the password you previously used because it leaked somewhere (😱)
|
|
65
186
|
|
|
66
|
-
|
|
67
|
-
- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit)
|
|
187
|
+
Then you can use multiple passwords:
|
|
68
188
|
|
|
69
|
-
|
|
189
|
+
**Week 1**:
|
|
70
190
|
|
|
71
|
-
|
|
191
|
+
```js
|
|
192
|
+
export default withIronSession(handler, {
|
|
193
|
+
password: [
|
|
194
|
+
{
|
|
195
|
+
id: 1,
|
|
196
|
+
password: "complex_password_at_least_32_characters_long",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Week 2**:
|
|
72
203
|
|
|
73
204
|
```js
|
|
74
|
-
|
|
75
|
-
|
|
205
|
+
export default withIronSession(handler, {
|
|
206
|
+
password: [
|
|
207
|
+
{
|
|
208
|
+
id: 2,
|
|
209
|
+
password: "another_password_at_least_32_characters_long",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 1,
|
|
213
|
+
password: "complex_password_at_least_32_characters_long",
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
```
|
|
76
218
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
219
|
+
Notes:
|
|
220
|
+
|
|
221
|
+
- `id` is required so that we do not have to try every password in the list when decrypting (the `id` is part of the cookie value).
|
|
222
|
+
- The password used to encrypt session data (to `seal`) is always the first one in the array, so when rotating to put a new password, it must be first in the array list
|
|
223
|
+
- Even if you do not provide an array at first, you can always move to array based passwords afterwards, knowing that your first password (`string`) was given `{id:1}` automatically.
|
|
224
|
+
|
|
225
|
+
## API
|
|
226
|
+
|
|
227
|
+
### withIronSession(handler, { password, cookieName, [ttl], [cookieOptions] })
|
|
228
|
+
|
|
229
|
+
This can be used to wrap Next.js [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) or [API Routes](https://nextjs.org/docs/api-routes/introduction) so you can then access all `req.session.*` methods.
|
|
230
|
+
|
|
231
|
+
- `password`, **required**: Private key used to encrypt the cookie. It has to be at least 32 characters long. Use https://1password.com/password-generator/ to generate strong passwords. `password` can be either a `string` or an `array` of objects like this: `[{id: 2, password: "..."}, {id: 1, password: "..."}]` to allow for password rotation.
|
|
232
|
+
- `cookieName`, **required**: Name of the cookie to be stored
|
|
233
|
+
- `ttl`, _optional_: In seconds, default to 14 days
|
|
234
|
+
- [`cookieOptions`](https://github.com/jshttp/cookie#cookieserializename-value-options), _optional_: Any option available from [jshttp/cookie#serialize](https://github.com/jshttp/cookie#cookieserializename-value-options). Default to:
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
{
|
|
238
|
+
httpOnly: true,
|
|
239
|
+
secure: true,
|
|
240
|
+
sameSite: "lax",
|
|
241
|
+
// The next line makes sure browser will expire cookies before seals are considered expired by the server. It also allows for clock difference of 60 seconds maximum between server and clients.
|
|
242
|
+
maxAge: (ttl === 0 ? 2147483647 : ttl) - 60,
|
|
243
|
+
path: "/",
|
|
244
|
+
// other options:
|
|
245
|
+
// domain, if you want the cookie to be valid for the whole domain and subdomains, use domain: example.com
|
|
246
|
+
// encode, there should be no need to use this option, encoding is done by iron-session already
|
|
247
|
+
// expires, there should be no need to use this option, maxAge takes precedence
|
|
80
248
|
}
|
|
81
249
|
```
|
|
82
250
|
|
|
83
|
-
|
|
251
|
+
### ironSession({ password, cookieName, [ttl], [cookieOptions] })
|
|
252
|
+
|
|
253
|
+
Connect middleware.
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
import { ironSession } from "iron-session";
|
|
257
|
+
|
|
258
|
+
app.use(ironSession({ ...options }));
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### async applySession(req, res, { password, cookieName, [ttl], [cookieOptions] })
|
|
262
|
+
|
|
263
|
+
Allows you to use this module the way you want as long as you have access to `req` and `res`.
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
import { applySession } from "iron-session";
|
|
267
|
+
|
|
268
|
+
await applySession(req, res, options);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### req.session.set(name, value)
|
|
272
|
+
|
|
273
|
+
### req.session.get(name)
|
|
274
|
+
|
|
275
|
+
### req.session.unset(name)
|
|
276
|
+
|
|
277
|
+
### req.session.save() => promise
|
|
278
|
+
|
|
279
|
+
### req.session.destroy()
|
|
280
|
+
|
|
281
|
+
Note: If you use `req.session.destroy()` in an API route, you need to make sure this route will not be cached. To do so, either call this route via a POST request `fetch("/api/logout", { method: "POST" })` or add `cache-control: no-store, max-age=0` to its response.
|
|
282
|
+
|
|
283
|
+
See https://github.com/vvo/iron-session/issues/274 for more details.
|
|
284
|
+
|
|
285
|
+
## FAQ
|
|
286
|
+
|
|
287
|
+
### Why use pure 🍪 cookies for sessions?
|
|
288
|
+
|
|
289
|
+
This makes your sessions stateless: you do not have to store session data on your server. You do not need another server or service to store session data. This is particularly useful in serverless architectures where you're trying to reduce your backend dependencies.
|
|
290
|
+
|
|
291
|
+
### What are the drawbacks?
|
|
292
|
+
|
|
293
|
+
There are some drawbacks to this approach:
|
|
294
|
+
|
|
295
|
+
- you cannot invalidate a seal when needed because there's no state stored on the server-side about them. We consider that the way the cookie is stored reduces the possibility for this eventuality to happen. Also, in most applications the first thing you do when receiving an authenticated request is to validate the user and their rights in your database, which defeats the case where someone would try to use a token while their account was deactivated/deleted. Now if someone steals a user token you should have a process in place to mitigate that: deactivate the user and force a re-login with a flag in your database for example.
|
|
296
|
+
- application not supporting cookies won't work, but you can use [iron-store](https://github.com/vvo/iron-store/) to implement something similar. In the future, we could allow `iron-session` to accept [basic auth](https://tools.ietf.org/html/rfc7617) or bearer token methods too. Open an issue if you're interested.
|
|
297
|
+
- on most browsers, you're limited to 4,096 bytes per cookie. To give you an idea, a `iron-session` cookie containing `{user: {id: 230, admin: true}}` is 358 bytes signed and encrypted: still plenty of available cookie space in here.
|
|
298
|
+
- performance: crypto on the server-side could be slow, if that's the case let me know. Also, cookies are sent to every request to your website, even images, so this could be an issue
|
|
299
|
+
|
|
300
|
+
Now that you know the drawbacks, you can decide if they are an issue for your application or not.
|
|
301
|
+
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.
|
|
302
|
+
|
|
303
|
+
### How is this different from [JWT](https://jwt.io/)?
|
|
304
|
+
|
|
305
|
+
Not so much:
|
|
306
|
+
|
|
307
|
+
- JWT is a standard, it stores metadata in the JWT token themselves to ensure communication between different systems is flawless.
|
|
308
|
+
- 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.
|
|
309
|
+
- @hapi/iron mechanism is not a standard, it's a way to sign and encrypt data into seals
|
|
310
|
+
|
|
311
|
+
Depending on your own needs and preferences, `iron-session` may or may not fit you.
|
|
312
|
+
|
|
313
|
+
## Project status
|
|
314
|
+
|
|
315
|
+
✅ Production ready and maintained.
|
|
316
|
+
|
|
317
|
+
## Credits
|
|
84
318
|
|
|
85
|
-
|
|
319
|
+
Thanks to [Hoang Vo](https://github.com/hoangvvo) for advice and guidance while building this module. Hoang built [next-connect](https://github.com/hoangvvo/next-connect) and [next-session](https://github.com/hoangvvo/next-session).
|
|
86
320
|
|
|
87
|
-
|
|
321
|
+
Thanks to [hapi](https://hapi.dev/) team for creating [iron](https://github.com/hapijs/iron).
|
|
88
322
|
|
|
89
|
-
|
|
323
|
+
## 🤓 References
|
|
90
324
|
|
|
91
|
-
|
|
325
|
+
- https://owasp.org/www-project-cheat-sheets/cheatsheets/Session_Management_Cheat_Sheet.html#cookies
|
|
326
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#samesite-cookie-attribute
|
|
92
327
|
|
|
93
|
-
|
|
328
|
+
## Contributors ✨
|
|
94
329
|
|
|
95
|
-
|
|
330
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
96
331
|
|
|
97
|
-
|
|
332
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
333
|
+
<!-- prettier-ignore-start -->
|
|
334
|
+
<!-- markdownlint-disable -->
|
|
335
|
+
<table>
|
|
336
|
+
<tr>
|
|
337
|
+
<td align="center"><a href="http://www.afterecon.com/"><img src="https://avatars.githubusercontent.com/u/5559355?v=4?s=100" width="100px;" alt=""/><br /><sub><b>John Vandivier</b></sub></a><br /><a href="https://github.com/vvo/iron-session/commits?author=Vandivier" title="Code">💻</a> <a href="#ideas-Vandivier" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-Vandivier" title="Examples">💡</a></td>
|
|
338
|
+
</tr>
|
|
339
|
+
</table>
|
|
98
340
|
|
|
99
|
-
|
|
341
|
+
<!-- markdownlint-restore -->
|
|
342
|
+
<!-- prettier-ignore-end -->
|
|
100
343
|
|
|
101
|
-
|
|
344
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
102
345
|
|
|
103
|
-
|
|
346
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
__markAsModule(target);
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __reExport = (target, module2, desc) => {
|
|
14
|
+
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(module2))
|
|
16
|
+
if (!__hasOwnProp.call(target, key) && key !== "default")
|
|
17
|
+
__defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return target;
|
|
20
|
+
};
|
|
21
|
+
var __toModule = (module2) => {
|
|
22
|
+
return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/index.ts
|
|
26
|
+
__export(exports, {
|
|
27
|
+
getIronSession: () => getIronSession,
|
|
28
|
+
sealData: () => sealData,
|
|
29
|
+
unsealData: () => unsealData
|
|
30
|
+
});
|
|
31
|
+
var import_iron = __toModule(require("@hapi/iron"));
|
|
32
|
+
var import_cookie = __toModule(require("cookie"));
|
|
33
|
+
var timestampSkewSec = 60;
|
|
34
|
+
var fourteenDaysInSeconds = 15 * 24 * 3600;
|
|
35
|
+
var currentMajorVersion = 2;
|
|
36
|
+
var versionDelimiter = "#";
|
|
37
|
+
var defaultOptions = {
|
|
38
|
+
ttl: fourteenDaysInSeconds,
|
|
39
|
+
cookieOptions: {
|
|
40
|
+
httpOnly: true,
|
|
41
|
+
secure: true,
|
|
42
|
+
sameSite: "lax",
|
|
43
|
+
path: "/"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
async function getIronSession(req, res, userSessionOptions) {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
if (!req || !res || !userSessionOptions || !userSessionOptions.cookieName || !userSessionOptions.password) {
|
|
49
|
+
throw new Error(`iron-session: Bad usage. Minimum usage is const session = await getIronSession(req, res, { cookieName: "...", password: "...". Check the usage here: https://github.com/vvo/iron-session`);
|
|
50
|
+
}
|
|
51
|
+
const passwordsAsMap = normalizeStringPasswordToMap(userSessionOptions.password);
|
|
52
|
+
Object.values(normalizeStringPasswordToMap(userSessionOptions.password)).forEach((password) => {
|
|
53
|
+
if (password.length < 32) {
|
|
54
|
+
throw new Error(`iron-session: Bad usage. Password must be at least 32 characters long.`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const isHttps = req.socket.encrypted === true;
|
|
58
|
+
if (((_a = userSessionOptions.cookieOptions) == null ? void 0 : _a.secure) === true && isHttps === false) {
|
|
59
|
+
throw new Error(`iron-session: Can't use secure cookies when not in https. See usage at https://github.com/vvo/iron-session/`);
|
|
60
|
+
}
|
|
61
|
+
const options = {
|
|
62
|
+
...defaultOptions,
|
|
63
|
+
...userSessionOptions,
|
|
64
|
+
cookieOptions: {
|
|
65
|
+
...defaultOptions.cookieOptions,
|
|
66
|
+
...userSessionOptions.cookieOptions || {}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
if (((_b = userSessionOptions.cookieOptions) == null ? void 0 : _b.secure) === void 0) {
|
|
70
|
+
options.cookieOptions.secure = isHttps;
|
|
71
|
+
}
|
|
72
|
+
if (options.ttl === 0) {
|
|
73
|
+
options.ttl = 2147483647;
|
|
74
|
+
}
|
|
75
|
+
if (userSessionOptions.cookieOptions && "maxAge" in userSessionOptions.cookieOptions) {
|
|
76
|
+
if (userSessionOptions.cookieOptions.maxAge === void 0) {
|
|
77
|
+
options.ttl = 0;
|
|
78
|
+
} else {
|
|
79
|
+
options.cookieOptions.maxAge = computeCookieMaxAge(userSessionOptions.cookieOptions.maxAge);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
options.cookieOptions.maxAge = computeCookieMaxAge(options.ttl);
|
|
83
|
+
}
|
|
84
|
+
const sealFromCookies = (0, import_cookie.parse)(req.headers.cookie || "")[options.cookieName];
|
|
85
|
+
const session = sealFromCookies === void 0 ? {} : await unsealData(sealFromCookies, passwordsAsMap, options.ttl);
|
|
86
|
+
Object.defineProperties(session, {
|
|
87
|
+
save: {
|
|
88
|
+
value: async function save() {
|
|
89
|
+
if (res.headersSent === true) {
|
|
90
|
+
throw new Error(`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()`);
|
|
91
|
+
}
|
|
92
|
+
const seal = await sealData(session, passwordsAsMap, options.ttl);
|
|
93
|
+
const cookieValue = (0, import_cookie.serialize)(options.cookieName, seal, options.cookieOptions);
|
|
94
|
+
if (cookieValue.length > 4096) {
|
|
95
|
+
throw new Error(`iron-session: Cookie length is too big ${cookieValue.length}, browsers will refuse it. Try to remove some data.`);
|
|
96
|
+
}
|
|
97
|
+
addToCookies(cookieValue, res);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
destroy: {
|
|
101
|
+
value: function destroy() {
|
|
102
|
+
Object.keys(session).forEach((key) => {
|
|
103
|
+
delete session[key];
|
|
104
|
+
});
|
|
105
|
+
const cookieValue = (0, import_cookie.serialize)(options.cookieName, "", {
|
|
106
|
+
...options.cookieOptions,
|
|
107
|
+
maxAge: 0
|
|
108
|
+
});
|
|
109
|
+
addToCookies(cookieValue, res);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return session;
|
|
114
|
+
}
|
|
115
|
+
function addToCookies(cookieValue, res) {
|
|
116
|
+
var _a;
|
|
117
|
+
let existingSetCookie = (_a = res.getHeader("set-cookie")) != null ? _a : [];
|
|
118
|
+
if (typeof existingSetCookie === "string") {
|
|
119
|
+
existingSetCookie = [existingSetCookie];
|
|
120
|
+
}
|
|
121
|
+
res.setHeader("set-cookie", [...existingSetCookie, cookieValue]);
|
|
122
|
+
}
|
|
123
|
+
function computeCookieMaxAge(ttl) {
|
|
124
|
+
return ttl - timestampSkewSec;
|
|
125
|
+
}
|
|
126
|
+
async function unsealData(seal, password, ttl = fourteenDaysInSeconds) {
|
|
127
|
+
const passwordsAsMap = normalizeStringPasswordToMap(password);
|
|
128
|
+
const { sealWithoutVersion, tokenVersion } = parseSeal(seal);
|
|
129
|
+
try {
|
|
130
|
+
const data = await import_iron.default.unseal(sealWithoutVersion, passwordsAsMap, {
|
|
131
|
+
...import_iron.default.defaults,
|
|
132
|
+
ttl: ttl * 1e3
|
|
133
|
+
});
|
|
134
|
+
if (tokenVersion === 2) {
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...data.persistent
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof Error) {
|
|
142
|
+
if (error.message === "Expired seal" || error.message === "Bad hmac value" || error.message === "Cannot find password: " || error.message === "Incorrect number of sealed components") {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function parseSeal(seal) {
|
|
150
|
+
if (seal[seal.length - 2] === versionDelimiter) {
|
|
151
|
+
const [sealWithoutVersion, tokenVersionAsString] = seal.split(versionDelimiter);
|
|
152
|
+
return {
|
|
153
|
+
sealWithoutVersion,
|
|
154
|
+
tokenVersion: parseInt(tokenVersionAsString, 10)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { sealWithoutVersion: seal, tokenVersion: null };
|
|
158
|
+
}
|
|
159
|
+
async function sealData(data, password, ttl = fourteenDaysInSeconds) {
|
|
160
|
+
const passwordsAsMap = normalizeStringPasswordToMap(password);
|
|
161
|
+
const mostRecentPasswordId = Math.max(...Object.keys(passwordsAsMap).map((id) => parseInt(id, 10)));
|
|
162
|
+
const passwordForSeal = {
|
|
163
|
+
id: mostRecentPasswordId.toString(),
|
|
164
|
+
secret: passwordsAsMap[mostRecentPasswordId]
|
|
165
|
+
};
|
|
166
|
+
const seal = await import_iron.default.seal(data, passwordForSeal, {
|
|
167
|
+
...import_iron.default.defaults,
|
|
168
|
+
ttl: ttl * 1e3
|
|
169
|
+
});
|
|
170
|
+
return `${seal}${versionDelimiter}${currentMajorVersion}`;
|
|
171
|
+
}
|
|
172
|
+
function normalizeStringPasswordToMap(password) {
|
|
173
|
+
return typeof password === "string" ? { 1: password } : password;
|
|
174
|
+
}
|
|
175
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
176
|
+
0 && (module.exports = {
|
|
177
|
+
getIronSession,
|
|
178
|
+
sealData,
|
|
179
|
+
unsealData
|
|
180
|
+
});
|
|
181
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["import Iron from \"@hapi/iron\";\nimport type { CookieSerializeOptions } from \"cookie\";\nimport { parse as parseCookie, serialize as serializeCookie } from \"cookie\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport type { TLSSocket } from \"tls\";\n\n// default time allowed to check for iron seal validity when ttl passed\n// see https://hapi.dev/family/iron/api/?v=6.0.0#options\nconst timestampSkewSec = 60;\n\ntype passwordsMap = { [id: string]: string };\ntype password = string | passwordsMap;\n\nconst fourteenDaysInSeconds = 15 * 24 * 3600;\n\n// We store a token major version to handle data format changes when any. So that when you upgrade the cookies\n// can be kept alive between upgrades, no need to disconnect everyone.\nconst currentMajorVersion = 2;\nconst versionDelimiter = \"#\";\n\nconst defaultOptions: {\n ttl: number;\n cookieOptions: CookieSerializeOptions;\n} = {\n ttl: fourteenDaysInSeconds,\n cookieOptions: {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n path: \"/\",\n },\n};\n\nexport interface IronSessionOptions {\n /**\n * This is the cookie name that will be used inside the browser. You should make sure it's unique given\n * your application. Example: vercel-session\n */\n cookieName: string;\n\n /**\n * This is the password(s) that will be used to encrypt the cookie. It can be either a string or an object\n * like {1: \"password\", 2: password}.\n *\n * When you provide multiple passwords then all of them will be used to decrypt the cookie and only the most\n * recent (= highest key, 2 in this example) password will be used to encrypt the cookie. This allow you\n * to use password rotation (security)\n */\n password: password;\n\n /**\n * This is the time in seconds that the session will be valid for. This also set the max-age attribute of\n * the cookie automatically (minus 60 seconds so that the cookie always expire before the session).\n */\n ttl?: number;\n\n /**\n * This is the options that will be passed to the cookie library.\n * You can see all of them here: https://github.com/jshttp/cookie#options-1.\n *\n * If you want to use \"session cookies\" (cookies that are deleted when the browser is closed) then you need\n * to pass cookieOptions: { maxAge: undefined }.\n */\n cookieOptions?: CookieSerializeOptions;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface IronSessionData {\n // If we allow for any keys, then there's no more type check on unknown properties which is not good\n // If we allow for any keys, the later delete will work but I prefer to disable the check at this stage and\n // provide good type checking instead\n // [key: string]: unknown;\n}\n\nexport type IronSession = IronSessionData & {\n /**\n * Destroys the session data and removes the cookie.\n */\n destroy: () => void;\n\n /**\n * Encrypts the session data and sets the cookie.\n */\n save: () => Promise<void>;\n};\n\ndeclare module \"http\" {\n interface IncomingMessage {\n session: IronSession;\n }\n}\n\nexport async function getIronSession(\n req: IncomingMessage,\n res: ServerResponse,\n userSessionOptions: IronSessionOptions,\n): Promise<IronSession> {\n if (\n !req ||\n !res ||\n !userSessionOptions ||\n !userSessionOptions.cookieName ||\n !userSessionOptions.password\n ) {\n throw new Error(\n `iron-session: Bad usage. Minimum usage is const session = await getIronSession(req, res, { cookieName: \"...\", password: \"...\". Check the usage here: https://github.com/vvo/iron-session`,\n );\n }\n\n const passwordsAsMap = normalizeStringPasswordToMap(\n userSessionOptions.password,\n );\n\n Object.values(\n normalizeStringPasswordToMap(userSessionOptions.password),\n ).forEach((password) => {\n if (password.length < 32) {\n throw new Error(\n `iron-session: Bad usage. Password must be at least 32 characters long.`,\n );\n }\n });\n\n const isHttps = (req.socket as TLSSocket).encrypted === true;\n\n if (userSessionOptions.cookieOptions?.secure === true && isHttps === false) {\n throw new Error(\n `iron-session: Can't use secure cookies when not in https. See usage at https://github.com/vvo/iron-session/`,\n );\n }\n\n const options: Required<IronSessionOptions> = {\n ...defaultOptions,\n ...userSessionOptions,\n cookieOptions: {\n ...defaultOptions.cookieOptions,\n ...(userSessionOptions.cookieOptions || {}),\n },\n };\n\n // if the user did not set the secure flag themselves, we automatically configure it\n if (userSessionOptions.cookieOptions?.secure === undefined) {\n options.cookieOptions.secure = isHttps;\n }\n\n if (options.ttl === 0) {\n // ttl = 0 means no expiration\n // but in reality cookies have to expire (can't have no max-age)\n // 2147483647 is the max value for max-age in cookies\n // see https://stackoverflow.com/a/11685301/147079\n options.ttl = 2147483647;\n }\n\n if (\n userSessionOptions.cookieOptions &&\n \"maxAge\" in userSessionOptions.cookieOptions\n ) {\n // session cookie, do not set maxAge, consider token as infinite\n if (userSessionOptions.cookieOptions.maxAge === undefined) {\n options.ttl = 0;\n } else {\n options.cookieOptions.maxAge = computeCookieMaxAge(\n userSessionOptions.cookieOptions.maxAge,\n );\n }\n } else {\n options.cookieOptions.maxAge = computeCookieMaxAge(options.ttl);\n }\n\n const sealFromCookies = parseCookie(req.headers.cookie || \"\")[\n options.cookieName\n ];\n\n const session =\n sealFromCookies === undefined\n ? {}\n : await unsealData(sealFromCookies, passwordsAsMap, options.ttl);\n\n Object.defineProperties(session, {\n save: {\n value: async function save() {\n if (res.headersSent === true) {\n throw new Error(\n `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()`,\n );\n }\n const seal = await sealData(session, passwordsAsMap, options.ttl);\n const cookieValue = serializeCookie(\n options.cookieName,\n seal,\n options.cookieOptions,\n );\n\n if (cookieValue.length > 4096) {\n throw new Error(\n `iron-session: Cookie length is too big ${cookieValue.length}, browsers will refuse it. Try to remove some data.`,\n );\n }\n\n addToCookies(cookieValue, res);\n },\n },\n destroy: {\n value: function destroy() {\n Object.keys(session).forEach((key) => {\n // @ts-ignore See comment on the IronSessionData interface\n delete session[key];\n });\n\n const cookieValue = serializeCookie(options.cookieName, \"\", {\n ...options.cookieOptions,\n maxAge: 0,\n });\n addToCookies(cookieValue, res);\n },\n },\n });\n\n return session as IronSession;\n}\n\nfunction addToCookies(cookieValue: string, res: ServerResponse) {\n let existingSetCookie =\n (res.getHeader(\"set-cookie\") as string[] | string) ?? [];\n if (typeof existingSetCookie === \"string\") {\n existingSetCookie = [existingSetCookie];\n }\n res.setHeader(\"set-cookie\", [...existingSetCookie, cookieValue]);\n}\n\nfunction computeCookieMaxAge(ttl: number) {\n // The next line makes sure browser will expire cookies before seals are considered expired by the server.\n // It also allows for clock difference of 60 seconds maximum between server and clients.\n // It also makes sure to expire the cookie immediately when value is 0\n return ttl - timestampSkewSec;\n}\n\nexport async function unsealData(\n seal: string,\n password: password,\n ttl: number = fourteenDaysInSeconds,\n): Promise<IronSessionData> {\n const passwordsAsMap = normalizeStringPasswordToMap(password);\n const { sealWithoutVersion, tokenVersion } = parseSeal(seal);\n\n try {\n const data = await Iron.unseal(sealWithoutVersion, passwordsAsMap, {\n ...Iron.defaults,\n ttl: ttl * 1000,\n });\n\n if (tokenVersion === 2) {\n return data;\n }\n\n return {\n ...data.persistent,\n };\n } catch (error) {\n if (error instanceof Error) {\n if (\n error.message === \"Expired seal\" ||\n error.message === \"Bad hmac value\" ||\n error.message === \"Cannot find password: \" ||\n error.message === \"Incorrect number of sealed components\"\n ) {\n // if seal expired or\n // if seal is not valid (encrypted using a different password, when passwords are badly rotated) or\n // if we can't find back the password in the seal\n // then we just start a new session over\n return {};\n }\n }\n\n throw error;\n }\n}\n\nfunction parseSeal(seal: string): {\n sealWithoutVersion: string;\n tokenVersion: number | null;\n} {\n if (seal[seal.length - 2] === versionDelimiter) {\n const [sealWithoutVersion, tokenVersionAsString] =\n seal.split(versionDelimiter);\n return {\n sealWithoutVersion,\n tokenVersion: parseInt(tokenVersionAsString, 10),\n };\n }\n\n return { sealWithoutVersion: seal, tokenVersion: null };\n}\n\nexport async function sealData(\n data: IronSessionData,\n password: password,\n ttl: number = fourteenDaysInSeconds,\n) {\n const passwordsAsMap = normalizeStringPasswordToMap(password);\n\n const mostRecentPasswordId = Math.max(\n ...Object.keys(passwordsAsMap).map((id) => parseInt(id, 10)),\n );\n\n const passwordForSeal = {\n id: mostRecentPasswordId.toString(),\n secret: passwordsAsMap[mostRecentPasswordId],\n };\n\n const seal = await Iron.seal(data, passwordForSeal, {\n ...Iron.defaults,\n ttl: ttl * 1000,\n });\n\n return `${seal}${versionDelimiter}${currentMajorVersion}`;\n}\n\nfunction normalizeStringPasswordToMap(password: password) {\n return typeof password === \"string\" ? { 1: password } : password;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|