Skip to content

Commit

Permalink
Example with cookie auth (#5821)
Browse files Browse the repository at this point in the history
Fixes #153

This is my attempt at #153

Following @rauchg instructions:

- it uses an authentication helper across pages which returns a token if there's one
- it has session synchronization across tabs
- <strike>I deployed a passwordless backend on `now.sh` (https://with-cookie-api.now.sh, [src](https://github.com/j0lv3r4/next.js-with-cookies-api))</strike> The backend is included in the repository and you can deploy everything together by running `now`

Also, from reviewing other PRs, I made sure to:

- use [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch).
- use [next-cookies](https://www.npmjs.com/package/next-cookies).

Here's a little demo:

![GIF](https://i.imgur.com/067Ph56.gif)
  • Loading branch information
j0lvera authored and timneutkens committed Dec 14, 2018
1 parent b4e877c commit 798ae04
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 0 deletions.
47 changes: 47 additions & 0 deletions examples/with-cookie-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-cookie-auth)
# Example app utilizing cookie-based authentication

## How to use

### Using `create-next-app`

Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:

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

Install it and run:

```bash
npm install
npm run dev
```

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))

```bash
now
```

## The idea behind the example

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

This example is backend agnostic and uses [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) to do the API calls on the client and the server.

The repo includes a minimal passwordless backend built with [Micro](https://www.npmjs.com/package/micro) and it logs the user in with a GitHub username and saves the user id from the API call as token.

Session is syncronized across tabs. If you logout your session gets logged out 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.
21 changes: 21 additions & 0 deletions examples/with-cookie-auth/api/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { json, send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')

const login = async (req, res) => {
const { username } = await json(req)
const url = `https://api.github.com/users/${username}`

try {
const response = await fetch(url)
if (response.ok) {
const { id } = await response.json()
send(res, 200, { token: id })
} else {
send(res, response.status, response.statusText)
}
} catch (error) {
throw createError(error.statusCode, error.statusText)
}
}

module.exports = (req, res) => run(req, res, login)
20 changes: 20 additions & 0 deletions examples/with-cookie-auth/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"isomorphic-unfetch": "^3.0.0",
"micro": "^9.3.3"
},
"devDependencies": {
"micro-dev": "^3.0.0"
},
"scripts": {
"start": "micro",
"dev": "micro-dev"
},
"keywords": [],
"author": "",
"license": "ISC"
}
29 changes: 29 additions & 0 deletions examples/with-cookie-auth/api/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')

const profile = async (req, res) => {
if (!('authorization' in req.headers)) {
throw createError(401, 'Authorization header missing')
}

const auth = await req.headers.authorization
const { token } = JSON.parse(auth)
const url = `https://api.github.com/user/${token}`

try {
const response = await fetch(url)

if (response.ok) {
const js = await response.json()
// Need camelcase in the frontend
const data = Object.assign({}, { avatarUrl: js.avatar_url }, js)
send(res, 200, { data })
} else {
send(res, response.status, response.statusText)
}
} catch (error) {
throw createError(error.statusCode, error.statusText)
}
}

module.exports = (req, res) => run(req, res, profile)
12 changes: 12 additions & 0 deletions examples/with-cookie-auth/now.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"name": "with-cookie-auth",
"builds": [
{ "src": "www/package.json", "use": "@now/next" },
{ "src": "api/*.js", "use": "@now/node" }
],
"routes": [
{ "src": "/api/(.*)", "dest": "/api/$1" },
{ "src": "/(.*)", "dest": "/www/$1" }
]
}
58 changes: 58 additions & 0 deletions examples/with-cookie-auth/www/components/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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='/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
40 changes: 40 additions & 0 deletions examples/with-cookie-auth/www/components/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'
import Head from 'next/head'
import Header from './header'

const Layout = props => (
<React.Fragment>
<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>
</React.Fragment>
)

export default Layout
16 changes: 16 additions & 0 deletions examples/with-cookie-auth/www/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "with-cookie-auth",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"isomorphic-unfetch": "^3.0.0",
"js-cookie": "^2.2.0",
"next": "^7.0.2",
"next-cookies": "^1.0.4",
"react": "^16.6.3",
"react-dom": "^16.6.3"
}
}
29 changes: 29 additions & 0 deletions examples/with-cookie-auth/www/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import Layout from '../components/layout'

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

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

<ol>
<li>Click login and enter your GitHub username.</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
97 changes: 97 additions & 0 deletions examples/with-cookie-auth/www/pages/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Component } from 'react'
import Layout from '../components/layout'
import { login } from '../utils/auth'

class Login extends Component {
static getInitialProps ({ req }) {
const apiUrl = process.browser
? `https://${window.location.host}/api/login.js`
: `https://${req.headers.host}/api/login.js`

return { apiUrl }
}

constructor (props) {
super(props)

this.state = { username: '', error: '' }
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}

handleChange (event) {
this.setState({ username: event.target.value })
}

async handleSubmit (event) {
event.preventDefault()
const username = this.state.username
const url = this.props.apiUrl
login({ username, url }).catch(() =>
this.setState({ error: 'Login failed.' })
)
}

render () {
return (
<Layout>
<div className='login'>
<form onSubmit={this.handleSubmit}>
<label htmlFor='username'>GitHub username</label>

<input
type='text'
id='username'
name='username'
value={this.state.username}
onChange={this.handleChange}
/>

<button type='submit'>Login</button>

<p className={`error ${this.state.error && 'show'}`}>
{this.state.error && `Error: ${this.state.error}`}
</p>
</form>
</div>
<style jsx>{`
.login {
max-width: 340px;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
form {
display: flex;
flex-flow: column;
}
label {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
margin: 0.5rem 0 0;
display: none;
color: brown;
}
.error.show {
display: block;
}
`}</style>
</Layout>
)
}
}

export default Login
Loading

0 comments on commit 798ae04

Please sign in to comment.