Skip to content

Commit

Permalink
feat(API): expose ironSession (express), applySession, cookieName req…
Browse files Browse the repository at this point in the history
…uired

New features:
- an Express/Connect middleware: import { ironSession } from "next-iron-session"
  app.use(ironSession(options))
  or router.get("/", ironSession(options), (req, res, next) => {});
- import { applySession } from "next-iron-session":
  await applySession(req, res, options);

Examples:
- moved to examples/ folder
- added an Express example

BREAKING CHANGE:
- you need to import withIronSession as a named export:
before: import withIronSession from "next-iron-session"
after: import { withIronSession } from "next-iron-session"
- cookieName option is now mandatory (#54)

fixes #54
fixes #9
fixes #41
  • Loading branch information
vvo committed May 1, 2020
1 parent c016fec commit 5b7c3d1
Show file tree
Hide file tree
Showing 40 changed files with 1,298 additions and 55 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
example/
examples/next.js/
examples/express/node_modules
dist/
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ node_modules/
# misc
.DS_Store
/dist/
/example/.next/
/examples/next.js/.next/

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

.now
.now
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"editor.formatOnSave": true,
"search.exclude": {
".yarn": true,
"example/.yarn": true,
"examples/next.js/.yarn": true,
"yarn.lock": true
},
"eslint.packageManager": "yarn",
Expand Down
126 changes: 120 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# next-iron-session [![GitHub license](https://img.shields.io/github/license/vvo/next-iron-session?style=flat)](https://github.com/vvo/next-iron-session/blob/master/LICENSE) [![Tests](https://github.com/vvo/next-iron-session/workflows/Tests/badge.svg)](https://github.com/vvo/next-iron-session/actions) [![codecov](https://codecov.io/gh/vvo/next-iron-session/branch/master/graph/badge.svg)](https://codecov.io/gh/vvo/next-iron-session) ![npm](https://img.shields.io/npm/v/next-iron-session)

_🛠 Next.js stateless session utility using signed and encrypted cookies to store data_
_🛠 Next.js and Express (connect middleware) stateless session utility using signed and encrypted cookies to store data_

---

**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.
**This [Next.js](https://nextjs.org/), [Express](https://expressjs.com/) and [Connect](https://github.com/senchalabs/connect) 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.

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 map data on the server-side.

Expand All @@ -23,14 +23,20 @@ This method of storing session data is the same technique used by **frameworks l

**Next.js's** 🗿 [Static generation](https://nextjs.org/docs/basic-features/pages#static-generation-recommended) (SG) and ⚙️ [Server-side Rendering](https://nextjs.org/docs/basic-features/pages#server-side-rendering) (SSG) are both supported.

**There's a Connect middleware available** so you can use this library in any Connect compatible framework like Express.

_Table of contents:_

- [Installation](#installation)
- [Usage](#usage)
- [Examples](#examples)
- [Handle password rotation/update the password](#handle-password-rotationupdate-the-password)
- [Express / Connect middleware: `ironSession`](#express--connect-middleware-ironsession)
- [Usage with `next-connect`](#usage-with-next-connect)
- [API](#api)
- [withIronSession(handler, { password, cookieName, [ttl], [cookieOptions] })](#withironsessionhandler--password-cookiename-ttl-cookieoptions-)
- [ironSession({ password, cookieName, [ttl], [cookieOptions] })](#ironsession-password-cookiename-ttl-cookieoptions-)
- [async applySession(req, res, { password, cookieName, [ttl], [cookieOptions] })](#async-applysessionreq-res--password-cookiename-ttl-cookieoptions-)
- [req.session.set(name, value)](#reqsessionsetname-value)
- [req.session.get(name)](#reqsessiongetname)
- [req.session.setFlash(name, value)](#reqsessionsetflashname-value)
Expand All @@ -40,17 +46,20 @@ _Table of contents:_
- [Why use pure 🍪 cookies for sessions?](#why-use-pure--cookies-for-sessions)
- [How is this different from JWT?](#how-is-this-different-from-jwt)
- [Project status](#project-status)
- [Credits](#credits)
- [🤓 References](#-references)

## Installation

```bash
npm add next-iron-session

yarn add next-iron-session
```

## Usage

You can find a more complete real-world example in the [example folder](./example/).
You can find real-world examples (Next.js, Express) in the [examples folder](./examples/).

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.

Expand All @@ -59,7 +68,7 @@ The password is a private key you must pass at runtime, it has to be at least 32
**pages/api/login.js**:

```js
import withIronSession from "iron-session";
import { withIronSession } from "next-iron-session";

async function handler(req, res) {
// get user from database then:
Expand All @@ -73,13 +82,17 @@ async function handler(req, res) {

export default withIronSession(handler, {
password: "complex_password_at_least_32_characters_long",
// if your localhost is server on http:// then disable the secure flag
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false,
},
});
```

**pages/user.js**:

```js
import withIronSession from "iron-session";
import { withIronSession } from "next-iron-session";

function handler(req, res, session) {
const user = req.session.get("user");
Expand All @@ -88,13 +101,17 @@ function handler(req, res, session) {

export default withIronSession(handler, {
password: "complex_password_at_least_32_characters_long",
// if your localhost is server on http:// then disable the secure flag
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false,
},
});
```

**pages/api/logout.js**:

```js
import withIronSession from "iron-session";
import { withIronSession } from "next-iron-session";

function handler(req, res, session) {
req.session.destroy();
Expand All @@ -103,9 +120,19 @@ function handler(req, res, session) {

export default withIronSession(handler, {
password: "complex_password_at_least_32_characters_long",
// if your localhost is server on http:// then disable the secure flag
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false,
},
});
```

⚠️ Sessions are automatically recreated (empty session though) when:

- they expire
- a wrong password was used
- we can't find back the password id in the current list

## Examples

### Handle password rotation/update the password
Expand Down Expand Up @@ -153,10 +180,71 @@ Notes:
- 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
- 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.

### Express / Connect middleware: `ironSession`

You can import and use `ironSession` if you want to use `next-iron-session` in [Express](https://expressjs.com/) and [Connect](https://github.com/senchalabs/connect).

```js
import { ironSession } from "next-iron-session";

const session = ironSession({
cookieName: "next-iron-session/examples/express",
password: process.env.SECRET_COOKIE_PASSWORD,
// if your localhost is server on http:// then disable the secure flag
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false,
},
});

router.get("/profile", session, async function (req, res) {
// now you can access all of the req.session.* utilities
if (req.session.get("user") === undefined) {
res.redirect("/restricted");
return;
}

res.render("profile", {
title: "Profile",
userId: req.session.get("user").id,
});
});
```

A more complete example using Express can be found in the [examples folder](./examples/express).

### Usage with `next-connect`

Since `ironSession` is an Express / Connect middleware, it means you can use it with [`next-connect`](https://github.com/hoangvvo/next-connect):

```js
import { ironSession } from "next-iron-session";

const session = ironSession({
cookieName: "next-iron-session/examples/express",
password: process.env.SECRET_COOKIE_PASSWORD,
// if your localhost is server on http:// then disable the secure flag
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false,
},
});
import nextConnect from "next-connect";

const handler = nextConnect();

handler.use(session).get((req, res) => {
const user = req.session.get("user");
res.send(`Hello user ${user.id}`);
});

export default handler;
```

## API

### withIronSession(handler, { password, cookieName, [ttl], [cookieOptions] })

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.

- `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.
- `cookieName`, **required**: Name of the cookie to be stored
- `ttl`, _optional_: In seconds, default to 14 days
Expand All @@ -173,6 +261,26 @@ Notes:
}
```

### ironSession({ password, cookieName, [ttl], [cookieOptions] })

Connect middleware.

```js
import { ironSession } from "next-iron-session";

app.use(ironSession({ ...options }));
```

### async applySession(req, res, { password, cookieName, [ttl], [cookieOptions] })

Allows you to use this module the way you want as long as you have access to `req` and `res`.

```js
import { applySession } from "next-session";

await applySession(req, res, options);
```

### req.session.set(name, value)

### req.session.get(name)
Expand Down Expand Up @@ -213,6 +321,12 @@ This is a recent library I authored because I needed it. While @hapi/iron is bat

If you find bugs or have API ideas, [create an issue](https://github.com/vvo/next-iron-session/issues).

## Credits

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).

Thanks to [hapi](https://hapi.dev/) team for creating [iron](https://github.com/hapijs/iron).

## 🤓 References

- https://owasp.org/www-project-cheat-sheets/cheatsheets/Session_Management_Cheat_Sheet.html#cookies
Expand Down
30 changes: 30 additions & 0 deletions examples/express/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Express example application using next-iron-session

This is a small example application generated with the [Express application generator](https://expressjs.com/en/starter/generator.html).

The tl;dr; on how to use `next-iron-session` with Express is this:

```js
import { ironSession } from "next-iron-session";

const session = ironSession({
cookieName: "next-iron-session/examples/express",
password: process.env.SECRET_COOKIE_PASSWORD,
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false,
},
});

router.get("/profile", session, async function (req, res) {
// now you can access all of the req.session.* utilities
if (req.session.get("user") === undefined) {
res.redirect("/restricted");
return;
}

res.render("profile", {
title: "Profile",
userId: req.session.get("user").id,
});
});
```
40 changes: 40 additions & 0 deletions examples/express/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
var path = require("path");

var createError = require("http-errors");
var express = require("express");
var logger = require("morgan");

var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");

var app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "public")));

app.use("/", indexRouter);
app.use("/users", usersRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});

// error handler
app.use(function (err, req, res) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};

// render the error page
res.status(err.status || 500);
res.render("error");
});

module.exports = app;

0 comments on commit 5b7c3d1

Please sign in to comment.