-
Notifications
You must be signed in to change notification settings - Fork 27k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
b4e877c
commit 798ae04
Showing
12 changed files
with
526 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,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. |
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,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) |
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": "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" | ||
} |
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,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) |
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,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" } | ||
] | ||
} |
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,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 |
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 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 |
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 @@ | ||
{ | ||
"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" | ||
} | ||
} |
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,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 |
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,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 |
Oops, something went wrong.