Skip to content

Commit

Permalink
[Example] with-passport (#10529)
Browse files Browse the repository at this point in the history
* Added basic layout with login page

* Updated styles

* Added form component

* Added signup page

* Added login/signup API endpoints

* Bug fixes

* Set the cookie

* Added logout route

* Added more auth

* Updated signup

* Added profile page

* Added useUser

* Fix link

* Updated redirect path

* Renaming some files

* Added README

* Apply suggestions from Shu

Co-Authored-By: Shu Uesugi <shu@chibicode.com>

* Add useUser to the header

Co-authored-by: Shu Uesugi <shu@chibicode.com>
  • Loading branch information
Luis Alvarez D and chibicode committed Mar 19, 2020
1 parent 38e42cd commit 7d42b07
Show file tree
Hide file tree
Showing 18 changed files with 621 additions and 0 deletions.
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.
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"
}

0 comments on commit 7d42b07

Please sign in to comment.