Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Example] with-passport #10529

Merged
merged 19 commits into from Mar 19, 2020
48 changes: 48 additions & 0 deletions 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)).
82 changes: 82 additions & 0 deletions examples/with-passport/components/form.js
@@ -0,0 +1,82 @@
import Link from 'next/link'

const Form = ({ isLogin, errorMessage, onSubmit }) => (
<form onSubmit={onSubmit}>
<label>
<span>Username</span>
<input type="text" name="username" required />
</label>
<label>
<span>Password</span>
<input type="password" name="password" required />
</label>
{!isLogin && (
<label>
<span>Repeat password</span>
<input type="password" name="rpassword" required />
</label>
)}

<div className="submit">
{isLogin ? (
<>
<Link href="/signup">
<a>I don't have an account</a>
</Link>
<button type="submit">Login</button>
</>
) : (
<>
<Link href="/login">
<a>I already have an account</a>
</Link>
<button type="submit">Signup</button>
</>
)}
</div>

{errorMessage && <p className="error">{errorMessage}</p>}

<style jsx>{`
form,
label {
display: flex;
flex-flow: column;
}
label > span {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit {
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
}
.submit > a {
text-decoration: none;
}
.submit > button {
padding: 0.5rem 1rem;
cursor: pointer;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit > button:hover {
border-color: #888;
}
.error {
color: brown;
margin: 1rem 0 0;
}
`}</style>
</form>
)

export default Form
67 changes: 67 additions & 0 deletions 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 (
<header>
<nav>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
{user ? (
<>
<li>
<Link href="/profile">
<a>Profile</a>
</Link>
</li>
<li>
<a href="/api/logout">Logout</a>
</li>
</>
) : (
<li>
<Link href="/login">
<a>Login</a>
</Link>
</li>
)}
</ul>
</nav>
<style jsx>{`
nav {
max-width: 42rem;
margin: 0 auto;
padding: 0.2rem 1.25rem;
}
ul {
display: flex;
list-style: none;
margin-left: 0;
padding-left: 0;
}
li {
margin-right: 1rem;
}
li:first-child {
margin-left: auto;
}
a {
color: #fff;
text-decoration: none;
}
header {
color: #fff;
background-color: #333;
}
`}</style>
</header>
)
}

export default Header
38 changes: 38 additions & 0 deletions examples/with-passport/components/layout.js
@@ -0,0 +1,38 @@
import Head from 'next/head'
import Header from './header'

const Layout = props => (
<>
<Head>
<title>With Cookies</title>
</Head>

<Header />

<main>
<div className="container">{props.children}</div>
</main>

<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.container {
max-width: 42rem;
margin: 0 auto;
padding: 2rem 1.25rem;
}
`}</style>
</>
)

export default Layout
40 changes: 40 additions & 0 deletions 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.
lfades marked this conversation as resolved.
Show resolved Hide resolved
const cookie = req.headers?.cookie
return parse(cookie || '')
}

export function getTokenCookie(req) {
const cookies = parseCookies(req)
return cookies[TOKEN_NAME]
}
31 changes: 31 additions & 0 deletions 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
}
14 changes: 14 additions & 0 deletions 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)
}
16 changes: 16 additions & 0 deletions 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)
})
})
27 changes: 27 additions & 0 deletions 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() }
}
20 changes: 20 additions & 0 deletions 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"
}