Skip to content

Commit

Permalink
Add an example for Fauna using cookie based auth (round 2) (#9986)
Browse files Browse the repository at this point in the history
* Add an example for Fauna using cookie based auth.

* Update example to use more secure method of non-js cookie and all authed access via backend api calls.

* Updated README

* Updated files and added prettier

* Remove unused import to fix lint issue.

* Improve documentation on how to setup fauna. Remove client key to simplify setup.

* Remove semicolons

* Lint fix

* Updated readme instructions and deployment

* Fixed client side redirect issue with /profile

* Simplified login code

* Simplified signup code

* Removed isomorphic-unfetch

* Simplified logout

* Removed get-host file

* Removed the custom getInitialProps from withAuthSync

* Removed user email from localStorage

Co-authored-by: Luis Alvarez D. <luis@zeit.co>
  • Loading branch information
vimota and Luis Alvarez D. committed Jan 15, 2020
1 parent b595dd6 commit d672167
Show file tree
Hide file tree
Showing 17 changed files with 695 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/with-cookie-auth-fauna/.env
@@ -0,0 +1 @@
FAUNA_SERVER_KEY="<ENTER YOUR FAUNA SERVER KEY>"
70 changes: 70 additions & 0 deletions examples/with-cookie-auth-fauna/README.md
@@ -0,0 +1,70 @@
# With Cookie Auth and Fauna

## How to use

### Using `create-next-app`

Download [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) to bootstrap the example:

```bash
npm i -g create-next-app
create-next-app --example with-cookie-auth-fauna with-cookie-auth-fauna-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-cookie-auth-fauna
cd with-cookie-auth-fauna
```

### Run locally

First, you'll need to create an account on [Fauna](https://fauna.com/), then follow these steps:

1. In the [FaunaDB Console](https://dashboard.fauna.com/), click "New Database". Name it whatever you like and click "Save".
2. Click "New Collection", name it `User`, leave "Create collection index" checked, and click "Save".
3. Now go to "Indexes" in the left sidebar, and click "New Index". Select the `User` collection, call it `users_by_email`, and in the "terms" field type `data.email`. Select the "Unique" checkbox and click "Save". This will create an index that allows looking up users by their email, which we will use to log a user in.
4. Next, go to "Security" in the sidebar, then click "New Key". Create a new key with the `Server` role, call it `server-key`, and click "Save". Your key's secret will be displayed, copy that value and paste it as the value for `FAUNA_SERVER_KEY` in the `.env` file at the project root. Keep this key safely as it has privileged access to your database.

> For more information, read the [User Authentication Tutorial in Fauna](https://app.fauna.com/tutorials/authentication).
> **Add `.env` to `.gitignore`**, files with secrets should never be in the cloud, we have it here for the sake of the example.
Now, install it and run:

```bash
npm install
npm run dev
# or
yarn
yarn dev
```

### Deploy

We'll use [now](https://zeit.co/now) to deploy our app, first we need to add the server key as a secret using [now secrets](https://zeit.co/docs/v2/serverless-functions/env-and-secrets/?query=secrets#adding-secrets), like so:

```bash
now secrets add fauna-secret-key "ENTER YOUR FAUNA SERVER KEY"
```

Then deploy it to the cloud:

```bash
now
```

## The idea behind the example

In this example, we authenticate users and store a token in a secure (non-JS) cookie. The example only shows how the user session works, keeping a user logged in between pages.

This example uses [Fauna](https://fauna.com/) as the auth service and DB.

The repo includes a minimal auth backend built with the new [API Routes support](https://github.com/zeit/next.js/pull/7296) (`pages/api`), [Micro](https://www.npmjs.com/package/micro), [Fauna for Auth](https://app.fauna.com/tutorials/authentication) and [dotenv](https://github.com/zeit/next.js/tree/canary/examples/with-dotenv) for environment variables. The backend allows the user to create an account (a User document), login, and see their user id (User ref id).

Session is synchronized across tabs. If you logout your session gets removed on all the windows as well. We use the HOC `withAuthSync` for this.

The helper function `auth` helps to retrieve the token across pages and redirects the user if not token was found.
63 changes: 63 additions & 0 deletions examples/with-cookie-auth-fauna/components/header.js
@@ -0,0 +1,63 @@
import Link from 'next/link'
import { logout } from '../utils/auth'

const Header = props => (
<header>
<nav>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
<li>
<Link href="/login">
<a>Login</a>
</Link>
</li>
<li>
<Link href="/signup">
<a>Signup</a>
</Link>
</li>
<li>
<Link href="/profile">
<a>Profile</a>
</Link>
</li>
<li>
<button onClick={logout}>Logout</button>
</li>
</ul>
</nav>
<style jsx>{`
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 {
padding: 0.2rem;
color: #fff;
background-color: #333;
}
`}</style>
</header>
)

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

const Layout = props => (
<>
<Head>
<title>With Cookies</title>
</Head>
<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: 65rem;
margin: 1.5rem auto;
padding-left: 1rem;
padding-right: 1rem;
}
`}</style>
<Header />

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

export default Layout
8 changes: 8 additions & 0 deletions examples/with-cookie-auth-fauna/next.config.js
@@ -0,0 +1,8 @@
require('dotenv').config()

module.exports = {
env: {
// Set the fauna server key in the .env file and make it available at Build Time.
FAUNA_SERVER_KEY: process.env.FAUNA_SERVER_KEY,
},
}
7 changes: 7 additions & 0 deletions examples/with-cookie-auth-fauna/now.json
@@ -0,0 +1,7 @@
{
"build": {
"env": {
"FAUNA_SERVER_KEY": "@fauna-secret-key"
}
}
}
17 changes: 17 additions & 0 deletions examples/with-cookie-auth-fauna/package.json
@@ -0,0 +1,17 @@
{
"name": "with-cookie-auth",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"cookie": "^0.4.0",
"dotenv": "8.2.0",
"faunadb": "2.10.0",
"js-cookie": "^2.2.0",
"next": "^9.0.1",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}
29 changes: 29 additions & 0 deletions examples/with-cookie-auth-fauna/pages/api/login.js
@@ -0,0 +1,29 @@
import { query as q } from 'faunadb'
import { serverClient, serializeFaunaCookie } from '../../utils/fauna-auth'

export default async (req, res) => {
const { email, password } = await req.body

try {
if (!email || !password) {
throw new Error('Email and password must be provided.')
}

const loginRes = await serverClient.query(
q.Login(q.Match(q.Index('users_by_email'), email), {
password,
})
)

if (!loginRes.secret) {
throw new Error('No secret present in login query response.')
}

const cookieSerialized = serializeFaunaCookie(loginRes.secret)

res.setHeader('Set-Cookie', cookieSerialized)
res.status(200).end()
} catch (error) {
res.status(400).send(error.message)
}
}
24 changes: 24 additions & 0 deletions examples/with-cookie-auth-fauna/pages/api/logout.js
@@ -0,0 +1,24 @@
import { query as q } from 'faunadb'
import cookie from 'cookie'
import { faunaClient, FAUNA_SECRET_COOKIE } from '../../utils/fauna-auth'

export default async (req, res) => {
const cookies = cookie.parse(req.headers.cookie ?? '')
const faunaSecret = cookies[FAUNA_SECRET_COOKIE]
if (!faunaSecret) {
// Already logged out.
return res.status(200).end()
}
// Invalidate secret (ie. logout from Fauna).
await faunaClient(faunaSecret).query(q.Logout(false))
// Clear cookie.
const cookieSerialized = cookie.serialize(FAUNA_SECRET_COOKIE, '', {
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: -1,
httpOnly: true,
path: '/',
})
res.setHeader('Set-Cookie', cookieSerialized)
res.status(200).end()
}
19 changes: 19 additions & 0 deletions examples/with-cookie-auth-fauna/pages/api/profile.js
@@ -0,0 +1,19 @@
import { query as q } from 'faunadb'
import cookie from 'cookie'
import { faunaClient, FAUNA_SECRET_COOKIE } from '../../utils/fauna-auth'

export const profileApi = async faunaSecret => {
const ref = await faunaClient(faunaSecret).query(q.Identity())
return ref.id
}

export default async (req, res) => {
const cookies = cookie.parse(req.headers.cookie ?? '')
const faunaSecret = cookies[FAUNA_SECRET_COOKIE]

if (!faunaSecret) {
return res.status(401).send('Auth cookie missing.')
}

res.status(200).json({ userId: await profileApi(faunaSecret) })
}
48 changes: 48 additions & 0 deletions examples/with-cookie-auth-fauna/pages/api/signup.js
@@ -0,0 +1,48 @@
import { query as q } from 'faunadb'
import { serverClient, serializeFaunaCookie } from '../../utils/fauna-auth'

export default async (req, res) => {
const { email, password } = await req.body

try {
if (!email || !password) {
throw new Error('Email and password must be provided.')
}
console.log(`email: ${email} trying to create user.`)

let user

try {
user = await serverClient.query(
q.Create(q.Collection('User'), {
credentials: { password },
data: { email },
})
)
} catch (error) {
console.error('Fauna create user error:', error)
throw new Error('User already exists.')
}

if (!user.ref) {
throw new Error('No ref present in create query response.')
}

const loginRes = await serverClient.query(
q.Login(user.ref, {
password,
})
)

if (!loginRes.secret) {
throw new Error('No secret present in login query response.')
}

const cookieSerialized = serializeFaunaCookie(loginRes.secret)

res.setHeader('Set-Cookie', cookieSerialized)
res.status(200).end()
} catch (error) {
res.status(400).send(error.message)
}
}
29 changes: 29 additions & 0 deletions examples/with-cookie-auth-fauna/pages/index.js
@@ -0,0 +1,29 @@
import React from 'react'
import Layout from '../components/layout'

const Home = () => (
<Layout>
<h1>Cookie-based authentication example</h1>

<p>Steps to test the functionality:</p>

<ol>
<li>Click signup and create an account, this will also log you in.</li>
<li>
Click home and click profile again, notice how your session is being
used through a token stored in a cookie.
</li>
<li>
Click logout and try to go to profile again. You'll get redirected to
the `/login` route.
</li>
</ol>
<style jsx>{`
li {
margin-bottom: 0.5rem;
}
`}</style>
</Layout>
)

export default Home

0 comments on commit d672167

Please sign in to comment.