-
-
Notifications
You must be signed in to change notification settings - Fork 252
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(lib): new major version complete rewrite using iron-store changed the API to ease usage on Next.js BREAKING CHANGES * fix linting * update readme * update readme and pkg description
- Loading branch information
Showing
6 changed files
with
316 additions
and
279 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
/.history/ | ||
/.yarn/ | ||
/.vscode/ | ||
/dist/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,114 +1,121 @@ | ||
# iron-session [](https://github.com/vvo/iron-session/blob/master/LICENSE)  [](https://codecov.io/gh/vvo/iron-session)  | ||
# next-iron-session [](https://github.com/vvo/next-iron-session/blob/master/LICENSE)  [](https://codecov.io/gh/vvo/next-iron-session)  | ||
|
||
**This JavaScript backend utility** allows you to create a session to then be stored in browser cookies via a signed and encrypted token value. This provides client sessions that are ⚒️ iron-strong. | ||
_🛠 Next.js stateless session utility using signed and encrypted cookies to store data_ | ||
|
||
The token stored on the client contains the session data, not your server, making it a "stateless" session from the server point of view. The token is signed and encrypted using [@hapi/iron](https://github.com/hapijs/iron). | ||
--- | ||
|
||
**⚡️ Flash session data is supported**. It means you can store some data which will be deleted when read. This is useful for temporary tokens, redirects or notices on your UI. | ||
**This [Next.js](https://nextjs.org/) backend utility** allows you to create a session to then be stored in browser cookies via a signed and encrypted seal. This provides client sessions that are ⚒️ iron-strong. | ||
|
||
**By default the cookie has an ⏰ expiration time of 15 days**, set via [`maxAge`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives). After that, even if someone tries to reuse the cookie, @hapi/iron will not accept the underlying token. Because the expiration is also part of the token value. See https://hapi.dev/family/iron for more information on @hapi/iron mechanisms. | ||
The seal stored on the client contains the session data, not your server, making it a "stateless" session from the server point of view. This is a different take than [next-session](https://github.com/hoangvvo/next-session/) where the cookie contains a session ID to then be used to identity data on the server-side. | ||
|
||
**Why use pure 🍪 cookies for sessions?** This makes your sessions stateless: you do not have to store session data on your server. This is particularly useful in serverless architectures. Still, there are some drawbacks to this approach: | ||
The seal is signed and encrypted using [@hapi/iron](https://github.com/hapijs/iron), [iron-store](https://github.com/vvo/iron-store/) is used behind the scenes. | ||
|
||
- you cannot invalidate a cookie when needed because there's no state stored on the server-side about the tokens. We consider that the way the cookie is stored reduces the possibility for this eventuality to happen. | ||
- application not supporting cookies won't work, this could be solved in the future by exposing the underlying token instead of signed and encrypted cookies. Open an issue if you're interested. | ||
- on most browsers, you're limited to 4,096 bytes per cookie. To give you an idea, an `iron-session` containing `{user: {id: 230, admin: true}}` is 358 bytes signed and encrypted: still plenty of available cookie space in here. | ||
**⚡️ Flash session data is supported**. It means you can store some data which will be deleted when read. This is useful for temporary data, redirects or notices on your UI. | ||
|
||
Now that you know the drawbacks, you can decide if they are an issue for your application or not. | ||
|
||
**🤓 References:** | ||
|
||
- https://owasp.org/www-project-cheat-sheets/cheatsheets/Session_Management_Cheat_Sheet.html#cookies | ||
- https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#encryption-based-token-pattern | ||
|
||
## How is this different from [JWT](https://jwt.io/)? | ||
**By default the cookie has an ⏰ expiration time of 15 days**, set via [`maxAge`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives). After that, even if someone tries to reuse the cookie, `next-iron-session` will not accept the underlying seal because the expiration is part of the seal value. See https://hapi.dev/family/iron for more information on @hapi/iron mechanisms. | ||
|
||
Not so much: | ||
|
||
- JWT is a standard, it stores metadata in the JWT token themselves to ensure communication between different systems is flawless. | ||
- JWT tokens are not encrypted, the payload is visible by customers if they manage to inspect the token. You would have to use [JWE](https://tools.ietf.org/html/rfc7516) to achieve the same. | ||
- @hapi/iron mechanism is not a standard, it's a way to sign and encrypt data into tokens | ||
|
||
Depending on your own needs and preferences, `iron-session-cookie` may or may not fit you. | ||
|
||
## Instalation | ||
## Installation | ||
|
||
```bash | ||
npm add iron-session | ||
npm add next-iron-session | ||
``` | ||
|
||
## Usage | ||
|
||
The examples are using a user login flow: login, verify, log out. But you can use `iron-session` for any other session need. | ||
The password is a private key you must pass at runtime, it has to be at least 32 characters long. Use https://1password.com/password-generator/ to generate strong passwords. | ||
|
||
The password is a private key you must pass at runtime, it has to be at least 32 characters long. https://1password.com/password-generator/ is a good way to generate a strong password. | ||
Store passwords in secret environment variables on your platform. | ||
|
||
### When the user logs in | ||
**login.js**: | ||
|
||
```js | ||
import { createSession } from "iron-session"; | ||
export default async (req, res) => { | ||
// when user successfully logs in using email/password, oauth, ... then we create a session | ||
// const user = ... | ||
import withIronSession from "iron-session"; | ||
|
||
const session = await createSession({ | ||
password: process.env.SECRET_SESSION_PASSWORD | ||
}); | ||
|
||
session.set({ name: "user", value: { id: 230, admin: true } }); | ||
session.set({ name: "message", value: "Login success", flash: true }); | ||
|
||
res.writeHead(200, { | ||
"set-cookie": await session.serializeCookie() | ||
async function handler(req, res, session) { | ||
session.set("user", { | ||
id: 230, | ||
admin: true | ||
}); | ||
await session.save(); | ||
res.send("Logged in"); | ||
} | ||
|
||
res.end("ok"); | ||
}; | ||
export default withIronSession(handler, { | ||
password: "complex_password_at_least_32_characters_long" | ||
}); | ||
``` | ||
|
||
`serializeCookie` accepts all the options from https://github.com/jshttp/cookie#cookieserializename-value-options, merged with `iron-session` defaults. The defaults are: | ||
**user.js**: | ||
|
||
```js | ||
{ | ||
httpOnly: true, | ||
secure: true, | ||
sameSite: "lax", | ||
maxAge: (ttl === 0 ? 2147483647 : ttl) - 60, // For Iron, ttl 0 means it will never expire. For browser cookies, maxAge 0 means it will expire immediately. WhilCookie must expire before the seal, otherwise you could have expired seals stored in a cookie | ||
import withIronSession from "iron-session"; | ||
|
||
function handler(req, res, session) { | ||
const user = session.get("user"); | ||
res.send({ user }); | ||
} | ||
|
||
export default withIronSession(handler, { | ||
password: "complex_password_at_least_32_characters_long" | ||
}); | ||
``` | ||
|
||
### Checking if the user is logged in | ||
**logout.js**: | ||
|
||
```js | ||
import { getSession, parseCookie } from "iron-session"; | ||
export default async (req, res) => { | ||
const session = await getSession({ | ||
password: process.env.SECRET_SESSION_PASSWORD, | ||
sealed: parseCookie({ cookie: req.getHeader("cookie") }) | ||
}); | ||
import withIronSession from "iron-session"; | ||
|
||
const user = session.get({ name: "user" }); | ||
const flashMessage = session.get({ name: "message" }); | ||
function handler(req, res, session) { | ||
session.destroy(); | ||
res.send("Logged out"); | ||
} | ||
|
||
res.end("ok"); | ||
}; | ||
export default withIronSession(handler, { | ||
password: "complex_password_at_least_32_characters_long" | ||
}); | ||
``` | ||
|
||
### When the user logs out | ||
## API | ||
|
||
```js | ||
import { deleteCookie } from "iron-session"; | ||
export default async (req, res) => { | ||
res.writeHead(200, { | ||
"set-cookie": deleteCookie() | ||
}); | ||
### withIronSession(handler, {password, ttl, cookieName, cookieOptions}) | ||
|
||
res.end("ok"); | ||
}; | ||
``` | ||
### session.set | ||
|
||
### session.get | ||
|
||
### session.setFlash | ||
|
||
### session.destroy | ||
|
||
## FAQ | ||
|
||
### Why use pure 🍪 cookies for sessions? | ||
|
||
This makes your sessions stateless: you do not have to store session data on your server. This is particularly useful in serverless architectures. Still, there are some drawbacks to this approach: | ||
|
||
- 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. | ||
- 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 `next-iron-session` to accept [basic auth](https://tools.ietf.org/html/rfc7617) or bearer token methods too. Open an issue if you're interested. | ||
- on most browsers, you're limited to 4,096 bytes per cookie. To give you an idea, a `next-iron-session` cookie containing `{user: {id: 230, admin: true}}` is 358 bytes signed and encrypted: still plenty of available cookie space in here. | ||
|
||
Now that you know the drawbacks, you can decide if they are an issue for your application or not. | ||
|
||
### How is this different from [JWT](https://jwt.io/)? | ||
|
||
Not so much: | ||
|
||
- JWT is a standard, it stores metadata in the JWT seal themselves to ensure communication between different systems is flawless. | ||
- JWT seals 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. | ||
- @hapi/iron mechanism is not a standard, it's a way to sign and encrypt data into seals | ||
|
||
Depending on your own needs and preferences, `next-iron-session-cookie` may or may not fit you. | ||
|
||
## Project status | ||
|
||
This is a recent library I authored because I needed it. While @hapi/iron is battle-tested and [used in production on a lot of websites](https://hapi.dev/), this library is not. Please use it at your own risk. | ||
|
||
If you find bugs or have API ideas, create an issue. | ||
|
||
## 🤓 References | ||
|
||
- https://owasp.org/www-project-cheat-sheets/cheatsheets/Session_Management_Cheat_Sheet.html#cookies | ||
- https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#encryption-based-seal-pattern |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,67 @@ | ||
import Iron from "@hapi/iron"; | ||
import ironStore from "iron-store"; | ||
import cookie from "cookie"; | ||
import clone from "clone"; | ||
|
||
const defaultTtl = 15 * 24 * 3600; | ||
const cookieName = "__ironSession"; | ||
// TODO: warn on no session usage | ||
// TODO: warn when session saved after end | ||
// TODO: warn when session not saved after set and req ended | ||
|
||
export async function createSession({ password, ttl = defaultTtl }) { | ||
return getSession({ password, ttl }); | ||
// default time allowed to check for iron seal validity when ttl passed | ||
// see https://hapi.dev/family/iron/api/?v=6.0.0#options | ||
const timestampSkewSec = 60; | ||
|
||
function throwOnNoPassword() { | ||
throw new Error("next-iron-sesion: Missing parameter `password`"); | ||
} | ||
|
||
export async function getSession({ sealed, password, ttl = defaultTtl }) { | ||
const options = { ...Iron.defaults, ttl }; | ||
const store = | ||
sealed !== undefined | ||
? await Iron.unseal(sealed, password, options) | ||
: { persistent: {}, flash: {} }; | ||
function computeCookieMaxAge(ttl) { | ||
return (ttl === 0 ? 2147483647 : ttl) - timestampSkewSec; | ||
} | ||
|
||
return { | ||
set({ name, value, flash = false }) { | ||
if (flash === true) { | ||
store.flash[name] = clone(value); | ||
} else { | ||
store.persistent[name] = clone(value); | ||
} | ||
}, | ||
get({ name = undefined } = {}) { | ||
if (name === undefined) { | ||
const flash = store.flash; | ||
store.flash = {}; | ||
return clone({ | ||
...flash, | ||
...store.persistent | ||
}); | ||
} | ||
const defaultCookieOptions = { | ||
httpOnly: true, | ||
secure: true, | ||
sameSite: "lax" | ||
}; | ||
|
||
if (store.flash[name] !== undefined) { | ||
const value = store.flash[name]; | ||
delete store.flash[name]; | ||
return value; // no need to clone, we removed the reference from the flash store | ||
} else { | ||
return clone(store.persistent[name]); | ||
} | ||
}, | ||
async serializeCookie(cookieOptions = {}) { | ||
return cookie.serialize( | ||
cookieName, | ||
await Iron.seal(store, password, options), | ||
{ | ||
httpOnly: true, | ||
secure: true, | ||
sameSite: "lax", | ||
maxAge: (ttl === 0 ? 2147483647 : ttl) - 60, // For Iron, ttl 0 means it will never expire. For browser cookies, maxAge 0 means it will expire immediately. WhilCookie must expire before the seal, otherwise you could have expired seals stored in a cookie | ||
...cookieOptions | ||
} | ||
); | ||
} | ||
export default function withIronSession( | ||
withIronSessionWrapperHandler, | ||
{ | ||
ttl = 15 * 24 * 3600, | ||
cookieName = "__ironSession", | ||
password = throwOnNoPassword(), | ||
cookieOptions: userCookieOptions = {} | ||
} = {} | ||
) { | ||
const cookieOptions = { | ||
...defaultCookieOptions, | ||
...userCookieOptions, | ||
maxAge: userCookieOptions.maxAge || computeCookieMaxAge(ttl) | ||
}; | ||
} | ||
|
||
export function parseCookie({ cookie: cookieValue }) { | ||
return cookie.parse(cookieValue)[cookieName]; | ||
} | ||
return async function withIronSessionHandler(req, res) { | ||
const store = await ironStore({ | ||
sealed: req.cookies[cookieName], | ||
password, | ||
ttl | ||
}); | ||
|
||
const session = { | ||
set: store.set, | ||
get: store.get, | ||
setFlash: store.setFlash, | ||
async save() { | ||
const seal = await store.seal(); | ||
const cookieValue = cookie.serialize(cookieName, seal, cookieOptions); | ||
res.setHeader("set-cookie", [cookieValue]); | ||
}, | ||
destroy() { | ||
const cookieValue = cookie.serialize(cookieName, "", { | ||
maxAge: 0 | ||
}); | ||
res.setHeader("set-cookie", [cookieValue]); | ||
} | ||
}; | ||
|
||
export function deleteCookie() { | ||
return cookie.serialize(cookieName, "", { | ||
maxAge: 0 | ||
}); | ||
return withIronSessionWrapperHandler(req, res, session); | ||
}; | ||
} |
Oops, something went wrong.