forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Example] with-passport (vercel#10529)
* 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 <[email protected]> * Add useUser to the header Co-authored-by: Shu Uesugi <[email protected]>
- Loading branch information
Showing
18 changed files
with
621 additions
and
0 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 |
---|---|---|
@@ -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)). |
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 |
---|---|---|
@@ -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 |
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 |
---|---|---|
@@ -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 |
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 |
---|---|---|
@@ -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 |
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 |
---|---|---|
@@ -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] | ||
} |
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 |
---|---|---|
@@ -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 | ||
} |
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 |
---|---|---|
@@ -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) | ||
} |
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 |
---|---|---|
@@ -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) | ||
}) | ||
}) |
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 |
---|---|---|
@@ -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() } | ||
} |
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 |
---|---|---|
@@ -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" | ||
} |
Oops, something went wrong.