diff --git a/examples/with-passport/README.md b/examples/with-passport/README.md
new file mode 100644
index 0000000000000..db78f88011dfd
--- /dev/null
+++ b/examples/with-passport/README.md
@@ -0,0 +1,48 @@
+# Passport.js Example
+
+This example show how to use [Passport.js](http://www.passportjs.org) with Next.js. The example features cookie based authentication with username and password.
+
+The example shows how to do a login, signup and logout; and to get the user info using a hook with [SWR](https://swr.now.sh).
+
+A DB is not included. You can use any db you want and add it [here](/lib/user.js).
+
+The login cookie is httpOnly, meaning it can only be accessed by the API, and it's encrypted using [@hapi/iron](https://hapi.dev/family/iron) for more security.
+
+## Deploy your own
+
+Deploy the example using [ZEIT Now](https://zeit.co/now):
+
+[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-passport)
+
+## How to use
+
+### Using `create-next-app`
+
+Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
+
+```bash
+npm init next-app --example with-passport with-passport-app
+# or
+yarn create next-app --example with-passport with-passport-app
+```
+
+### Download manually
+
+Download the example [or clone the repo](https://github.com/zeit/next.js):
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-passport
+cd with-passport
+```
+
+Install it and run:
+
+```bash
+npm install
+npm run dev
+# or
+yarn
+yarn dev
+```
+
+Deploy it to the cloud with [ZEIT Now](https://zeit.co/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
diff --git a/examples/with-passport/components/form.js b/examples/with-passport/components/form.js
new file mode 100644
index 0000000000000..2b5ba4b9f04e9
--- /dev/null
+++ b/examples/with-passport/components/form.js
@@ -0,0 +1,82 @@
+import Link from 'next/link'
+
+const Form = ({ isLogin, errorMessage, onSubmit }) => (
+
+)
+
+export default Form
diff --git a/examples/with-passport/components/header.js b/examples/with-passport/components/header.js
new file mode 100644
index 0000000000000..0b605d656739a
--- /dev/null
+++ b/examples/with-passport/components/header.js
@@ -0,0 +1,67 @@
+import Link from 'next/link'
+import { useUser } from '../lib/hooks'
+
+const Header = () => {
+ const user = useUser()
+
+ return (
+
+ )
+}
+
+export default Header
diff --git a/examples/with-passport/components/layout.js b/examples/with-passport/components/layout.js
new file mode 100644
index 0000000000000..174351ec9b608
--- /dev/null
+++ b/examples/with-passport/components/layout.js
@@ -0,0 +1,38 @@
+import Head from 'next/head'
+import Header from './header'
+
+const Layout = props => (
+ <>
+
+ With Cookies
+
+
+
+
+
+ {props.children}
+
+
+
+ >
+)
+
+export default Layout
diff --git a/examples/with-passport/lib/auth-cookies.js b/examples/with-passport/lib/auth-cookies.js
new file mode 100644
index 0000000000000..1d215f3a66423
--- /dev/null
+++ b/examples/with-passport/lib/auth-cookies.js
@@ -0,0 +1,40 @@
+import { serialize, parse } from 'cookie'
+
+const TOKEN_NAME = 'token'
+const MAX_AGE = 60 * 60 * 8 // 8 hours
+
+export function setTokenCookie(res, token) {
+ const cookie = serialize(TOKEN_NAME, token, {
+ maxAge: MAX_AGE,
+ expires: new Date(Date.now() + MAX_AGE * 1000),
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ path: '/',
+ sameSite: 'lax',
+ })
+
+ res.setHeader('Set-Cookie', cookie)
+}
+
+export function removeTokenCookie(res) {
+ const cookie = serialize(TOKEN_NAME, '', {
+ maxAge: -1,
+ path: '/',
+ })
+
+ res.setHeader('Set-Cookie', cookie)
+}
+
+export function parseCookies(req) {
+ // For API Routes we don't need to parse the cookies.
+ if (req.cookies) return req.cookies
+
+ // For pages we do need to parse the cookies.
+ const cookie = req.headers?.cookie
+ return parse(cookie || '')
+}
+
+export function getTokenCookie(req) {
+ const cookies = parseCookies(req)
+ return cookies[TOKEN_NAME]
+}
diff --git a/examples/with-passport/lib/hooks.js b/examples/with-passport/lib/hooks.js
new file mode 100644
index 0000000000000..645364466d3c0
--- /dev/null
+++ b/examples/with-passport/lib/hooks.js
@@ -0,0 +1,31 @@
+import { useEffect } from 'react'
+import Router from 'next/router'
+import useSWR from 'swr'
+
+const fetcher = url =>
+ fetch(url)
+ .then(r => r.json())
+ .then(data => {
+ return { user: data?.user || null }
+ })
+
+export function useUser({ redirectTo, redirectIfFound } = {}) {
+ const { data, error } = useSWR('/api/user', fetcher)
+ const user = data?.user
+ const finished = Boolean(data)
+ const hasUser = Boolean(user)
+
+ useEffect(() => {
+ if (!redirectTo || !finished) return
+ if (
+ // If redirectTo is set, redirect if the user was not found.
+ (redirectTo && !redirectIfFound && !hasUser) ||
+ // If redirectIfFound is also set, redirect if the user was found
+ (redirectIfFound && hasUser)
+ ) {
+ Router.push(redirectTo)
+ }
+ }, [redirectTo, redirectIfFound, finished, hasUser])
+
+ return error ? null : user
+}
diff --git a/examples/with-passport/lib/iron.js b/examples/with-passport/lib/iron.js
new file mode 100644
index 0000000000000..977c4b110dd99
--- /dev/null
+++ b/examples/with-passport/lib/iron.js
@@ -0,0 +1,14 @@
+import Iron from '@hapi/iron'
+import { getTokenCookie } from './auth-cookies'
+
+// Use an environment variable here instead of a hardcoded value for production
+const TOKEN_SECRET = 'this-is-a-secret-value-with-at-least-32-characters'
+
+export function encryptSession(session) {
+ return Iron.seal(session, TOKEN_SECRET, Iron.defaults)
+}
+
+export async function getSession(req) {
+ const token = getTokenCookie(req)
+ return token && Iron.unseal(token, TOKEN_SECRET, Iron.defaults)
+}
diff --git a/examples/with-passport/lib/password-local.js b/examples/with-passport/lib/password-local.js
new file mode 100644
index 0000000000000..1fc07d7d44781
--- /dev/null
+++ b/examples/with-passport/lib/password-local.js
@@ -0,0 +1,16 @@
+import Local from 'passport-local'
+import { findUser } from './user'
+
+export const localStrategy = new Local.Strategy(function(
+ username,
+ password,
+ done
+) {
+ findUser({ username, password })
+ .then(user => {
+ done(null, user)
+ })
+ .catch(error => {
+ done(error)
+ })
+})
diff --git a/examples/with-passport/lib/user.js b/examples/with-passport/lib/user.js
new file mode 100644
index 0000000000000..80276dabd035e
--- /dev/null
+++ b/examples/with-passport/lib/user.js
@@ -0,0 +1,27 @@
+// import crypto from 'crypto'
+
+/**
+ * User methods. The example doesn't contain a DB, but for real applications you must use a
+ * db here, such as MongoDB, Fauna, SQL, etc.
+ */
+
+export async function createUser({ username, password }) {
+ // Here you should create the user and save the salt and hashed password (some dbs may have
+ // authentication methods that will do it for you so you don't have to worry about it):
+ //
+ // const salt = crypto.randomBytes(16).toString('hex')
+ // const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex')
+ // const user = await DB.createUser({ username, salt, hash })
+
+ return { username, createdAt: Date.now() }
+}
+
+export async function findUser({ username, password }) {
+ // Here you should lookup for the user in your DB and compare the password:
+ //
+ // const user = await DB.findUser(...)
+ // const hash = crypto.pbkdf2Sync(password, user.salt, 1000, 64, 'sha512').toString('hex')
+ // const passwordsMatch = user.hash === hash
+
+ return { username, createdAt: Date.now() }
+}
diff --git a/examples/with-passport/package.json b/examples/with-passport/package.json
new file mode 100644
index 0000000000000..5c15d1ca55a64
--- /dev/null
+++ b/examples/with-passport/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "with-passport",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@hapi/iron": "6.0.0",
+ "cookie": "0.4.0",
+ "express": "4.17.1",
+ "next": "latest",
+ "passport": "0.4.1",
+ "passport-local": "1.0.0",
+ "react": "latest",
+ "react-dom": "latest",
+ "swr": "0.1.16"
+ },
+ "license": "ISC"
+}
diff --git a/examples/with-passport/pages/api/login.js b/examples/with-passport/pages/api/login.js
new file mode 100644
index 0000000000000..62fb8be02f204
--- /dev/null
+++ b/examples/with-passport/pages/api/login.js
@@ -0,0 +1,41 @@
+import express from 'express'
+import passport from 'passport'
+import { localStrategy } from '../../lib/password-local'
+import { encryptSession } from '../../lib/iron'
+import { setTokenCookie } from '../../lib/auth-cookies'
+
+const app = express()
+const authenticate = (method, req, res) =>
+ new Promise((resolve, reject) => {
+ passport.authenticate(method, { session: false }, (error, token) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(token)
+ }
+ })(req, res)
+ })
+
+app.disable('x-powered-by')
+
+app.use(passport.initialize())
+
+passport.use(localStrategy)
+
+app.post('/api/login', async (req, res) => {
+ try {
+ const user = await authenticate('local', req, res)
+ // session is the payload to save in the token, it may contain basic info about the user
+ const session = { ...user }
+ // The token is a string with the encrypted session
+ const token = await encryptSession(session)
+
+ setTokenCookie(res, token)
+ res.status(200).send({ done: true })
+ } catch (error) {
+ console.error(error)
+ res.status(401).send(error.message)
+ }
+})
+
+export default app
diff --git a/examples/with-passport/pages/api/logout.js b/examples/with-passport/pages/api/logout.js
new file mode 100644
index 0000000000000..1fe3096cdc014
--- /dev/null
+++ b/examples/with-passport/pages/api/logout.js
@@ -0,0 +1,7 @@
+import { removeTokenCookie } from '../../lib/auth-cookies'
+
+export default async function logout(req, res) {
+ removeTokenCookie(res)
+ res.writeHead(302, { Location: '/' })
+ res.end()
+}
diff --git a/examples/with-passport/pages/api/signup.js b/examples/with-passport/pages/api/signup.js
new file mode 100644
index 0000000000000..7972d47838a74
--- /dev/null
+++ b/examples/with-passport/pages/api/signup.js
@@ -0,0 +1,11 @@
+import { createUser } from '../../lib/user'
+
+export default async function signup(req, res) {
+ try {
+ await createUser(req.body)
+ res.status(200).send({ done: true })
+ } catch (error) {
+ console.error(error)
+ res.status(500).end(error.message)
+ }
+}
diff --git a/examples/with-passport/pages/api/user.js b/examples/with-passport/pages/api/user.js
new file mode 100644
index 0000000000000..dad216c76e9a3
--- /dev/null
+++ b/examples/with-passport/pages/api/user.js
@@ -0,0 +1,9 @@
+import { getSession } from '../../lib/iron'
+
+export default async function user(req, res) {
+ const session = await getSession(req)
+ // After getting the session you may want to fetch for the user instead
+ // of sending the session's payload directly, this example doesn't have a DB
+ // so it won't matter in this case
+ res.status(200).json({ user: session || null })
+}
diff --git a/examples/with-passport/pages/index.js b/examples/with-passport/pages/index.js
new file mode 100644
index 0000000000000..0870d849e3d8c
--- /dev/null
+++ b/examples/with-passport/pages/index.js
@@ -0,0 +1,36 @@
+import { useUser } from '../lib/hooks'
+import Layout from '../components/layout'
+
+const Home = () => {
+ const user = useUser()
+
+ return (
+
+ Passport.js Example
+
+ Steps to test the example:
+
+
+ - Click Login and enter an username and password.
+ -
+ You'll be redirected to Home. Click on Profile, notice how your
+ session is being used through a token stored in a cookie.
+
+ -
+ Click Logout and try to go to Profile again. You'll get redirected to
+ Login.
+
+
+
+ {user && Currently logged in as: {JSON.stringify(user)}
}
+
+
+
+ )
+}
+
+export default Home
diff --git a/examples/with-passport/pages/login.js b/examples/with-passport/pages/login.js
new file mode 100644
index 0000000000000..64ccd37f43558
--- /dev/null
+++ b/examples/with-passport/pages/login.js
@@ -0,0 +1,57 @@
+import { useState } from 'react'
+import Router from 'next/router'
+import { useUser } from '../lib/hooks'
+import Layout from '../components/layout'
+import Form from '../components/form'
+
+const Login = () => {
+ useUser({ redirectTo: '/', redirectIfFound: true })
+
+ const [errorMsg, setErrorMsg] = useState('')
+
+ async function handleSubmit(e) {
+ event.preventDefault()
+
+ if (errorMsg) setErrorMsg('')
+
+ const body = {
+ username: e.currentTarget.username.value,
+ password: e.currentTarget.password.value,
+ }
+
+ try {
+ const res = await fetch('/api/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ })
+ if (res.status === 200) {
+ Router.push('/')
+ } else {
+ throw new Error(await res.text())
+ }
+ } catch (error) {
+ console.error('An unexpected error happened occurred:', error)
+ setErrorMsg(error.message)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default Login
diff --git a/examples/with-passport/pages/profile.js b/examples/with-passport/pages/profile.js
new file mode 100644
index 0000000000000..e864f7480f04b
--- /dev/null
+++ b/examples/with-passport/pages/profile.js
@@ -0,0 +1,15 @@
+import { useUser } from '../lib/hooks'
+import Layout from '../components/layout'
+
+const Profile = () => {
+ const user = useUser({ redirectTo: '/login' })
+
+ return (
+
+ Profile
+ {user && Your session: {JSON.stringify(user)}
}
+
+ )
+}
+
+export default Profile
diff --git a/examples/with-passport/pages/signup.js b/examples/with-passport/pages/signup.js
new file mode 100644
index 0000000000000..a0f31880a8ff6
--- /dev/null
+++ b/examples/with-passport/pages/signup.js
@@ -0,0 +1,62 @@
+import { useState } from 'react'
+import Router from 'next/router'
+import { useUser } from '../lib/hooks'
+import Layout from '../components/layout'
+import Form from '../components/form'
+
+const Signup = () => {
+ useUser({ redirectTo: '/', redirectIfFound: true })
+
+ const [errorMsg, setErrorMsg] = useState('')
+
+ async function handleSubmit(e) {
+ event.preventDefault()
+
+ if (errorMsg) setErrorMsg('')
+
+ const body = {
+ username: e.currentTarget.username.value,
+ password: e.currentTarget.password.value,
+ }
+
+ if (body.password !== e.currentTarget.rpassword.value) {
+ setErrorMsg(`The passwords don't match`)
+ return
+ }
+
+ try {
+ const res = await fetch('/api/signup', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ })
+ if (res.status === 200) {
+ Router.push('/login')
+ } else {
+ throw new Error(await res.text())
+ }
+ } catch (error) {
+ console.error('An unexpected error happened occurred:', error)
+ setErrorMsg(error.message)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default Signup