Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: With cookie authentication #3955

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions examples/with-cookie-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[![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

The goal is to authenticate users and store basic info safely in a signed cookie.
Certain pages can only be accessed by a logged-in user.

> Use any email from https://jsonplaceholder.typicode.com/users to login, user website is the password.
> e.g. Lucio_Hettinger@annie.ca / demarco.info
45 changes: 45 additions & 0 deletions examples/with-cookie-auth/components/Layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import Link from 'next/link'
import { processLogout } from '../lib/auth'

const Layout = ({ auth, children }) => {
const { user = {} } = auth || {}
return (
<div>
<span>Welcome, {user.name || 'Guest'}!</span>
<ul>
<li><Link href='/'><a>Home</a></Link></li>
{user.email
? (
<React.Fragment>
<li><a onClick={processLogout}>Logout</a></li>
<li><Link href='/profile'><a>Profile</a></Link></li>
</React.Fragment>
)
: (
<li><Link href='/login'><a>Login</a></Link></li>
)}
</ul>
{children}
<style jsx>{`
ul {
list-style: none;
float: right;
}
ul li {
display: inline;
margin: 0.25em;
}
ul li a {
text-decoration: underline;
cursor: pointer;
font-size: 1em;
color: #333;
border: none;
}
`}</style>
</div>
)
}

export default Layout
50 changes: 50 additions & 0 deletions examples/with-cookie-auth/components/LoginForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import Router from 'next/router'
import { processLogin } from '../lib/auth'

class LoginForm extends React.PureComponent {
state = {
email: '',
password: '',
error: undefined
}

onChange = ({ target: { name, value } }) => {
this.setState((state) => ({
[name]: value
}))
}

onSubmit = (e) => {
e.preventDefault()
const { email, password } = this.state
this.setState({ error: undefined })
processLogin({ email, password })
.then(() => Router.push('/profile'))
.catch(this.setError)
}

setError = (err) => {
console.warn({err})
const error = (err.response && err.response.data) || err.message
this.setState({ error })
}

render () {
const { email, password, error } = this.state
return (
<form onSubmit={this.onSubmit}>
<div><input autoComplete='on' type='text' placeholder='email' name='email' value={email} onChange={this.onChange} /></div>
<div><input autoComplete='on' type='password' name='password' value={password} onChange={this.onChange} /></div>
<div>
<button type='submit'>Submit</button>
</div>
{error && (
<div>{error}</div>
)}
</form>
)
}
}

export default LoginForm
97 changes: 97 additions & 0 deletions examples/with-cookie-auth/lib/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Router from 'next/router'
import axios from 'axios'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use isomorphic-unfetch like the other examples

Copy link
Contributor Author

@malixsys malixsys Nov 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this example is too opinionated to be included. 🤔
It works with Express and Axios because they are widespread and I find them easier to work with, and the reader might, too.
At the time of its creation in March, I could find no other comprehensive yet concise example of how to do proper secure, cookie-based auth...
Not bitter, just debating how willing I am to complexify this example with a separate API server.
Axios IS no biggy to fix.


axios.defaults.withCredentials = true

const WINDOW_USER_SCRIPT_VARIABLE = `__USER__`

const decode = ({token}) => {
if (!token) {
return {}
}
const {email, type} = token || {}
return {user: {email, type}}
}

export const getUserScript = (user) => {
const json = JSON.stringify(user)
return `${WINDOW_USER_SCRIPT_VARIABLE} = ${json};`
}

export const getServerSideToken = (req) => {
const {signedCookies} = req

if (!signedCookies) {
return {}
}
try {
return decode(signedCookies)
} catch (parseError) {
return {}
}
}

export const getClientSideToken = () => {
if (typeof window !== 'undefined') {
const user = window[WINDOW_USER_SCRIPT_VARIABLE] || {}
return {user}
}
return {user: {}}
}

const getRedirectPath = (userType) => {
switch (userType) {
case 'authenticated':
return '/profile'
default:
return '/login'
}
}

const redirect = (res, path) => {
if (res) {
res.redirect(302, path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

res.redirect is not something officially supported, it comes from Express, this should use the location header.

res.finished = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

res.finished is no longer needed if res.end is called

return {}
}
Router.replace(path)
return {}
}

export const authInitialProps = (redirectIfAuth, secured) => async ({req, res}) => {
const auth = req ? getServerSideToken(req) : getClientSideToken()
const current = req ? req.url : window.location.pathname
const user = auth.user
const isAnonymous = !user || user.type !== 'authenticated'
if (secured && isAnonymous && current !== '/login') {
return redirect(res, '/login')
}
if (!isAnonymous && redirectIfAuth) {
const path = getRedirectPath(user.type)
if (current !== path) {
return redirect(res, path)
}
}
return {auth}
}

export const getProfile = async () => {
const response = await axios.get('/api/profile')
return response.data
}

export const processLogin = async ({email, password}) => {
const response = await axios.post('/api/login', {email, password})
const {data} = response
if (typeof window !== 'undefined') {
window[WINDOW_USER_SCRIPT_VARIABLE] = data || {}
}
}

export const processLogout = async () => {
if (typeof window !== 'undefined') {
window[WINDOW_USER_SCRIPT_VARIABLE] = {}
}
await axios.post('/api/logout')
Router.push('/login')
}
18 changes: 18 additions & 0 deletions examples/with-cookie-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "with-cookie-auth",
"version": "1.0.0",
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"axios": "^0.18.0",
"cookie-parser": "1.4.3",
"express": "^4.16.2",
"next": "latest",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"license": "ISC"
}
24 changes: 24 additions & 0 deletions examples/with-cookie-auth/pages/_document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Document, { Head, Main, NextScript } from 'next/document'
import { getServerSideToken, getUserScript } from '../lib/auth'

export default class extends Document {
static async getInitialProps (ctx) {
const props = await Document.getInitialProps(ctx)
const info = getServerSideToken(ctx.req)
return { ...props, ...info }
}

render () {
const { user = {} } = this.props
return (
<html lang='en'>
<Head />
<body>
<Main />
<script dangerouslySetInnerHTML={{ __html: getUserScript(user) }} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, you can use _app.js and then assign to window.__USER__ in constructor of _app.js

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then _document.js can also be removed I guess

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, no longer needed... This was written before _app I think... :)

<NextScript />
</body>
</html>
)
}
}
20 changes: 20 additions & 0 deletions examples/with-cookie-auth/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'
import Link from 'next/link'
import Layout from '../components/Layout'
import { authInitialProps } from '../lib/auth'

export default class Home extends React.PureComponent {
render () {
return (
<Layout {...this.props}>
<h1>Home</h1>
Try:
<ul>
<li><Link href='/profile'><a>Profile</a></Link></li>
</ul>
</Layout>
)
}
}

Home.getInitialProps = authInitialProps()
17 changes: 17 additions & 0 deletions examples/with-cookie-auth/pages/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'
import Layout from '../components/Layout'
import { authInitialProps } from '../lib/auth'
import LoginForm from '../components/LoginForm'

export default class Login extends React.PureComponent {
render () {
return (
<Layout {...this.props}>
<h1>Login</h1>
<LoginForm />
</Layout>
)
}
}

Login.getInitialProps = authInitialProps(true)
23 changes: 23 additions & 0 deletions examples/with-cookie-auth/pages/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import Layout from '../components/Layout'
import { authInitialProps, getProfile } from '../lib/auth'

export default class Profile extends React.PureComponent {
state = { user: 'Loading...' }

componentDidMount () {
// to test withCredentials
getProfile().then(user => this.setState({ user }))
}

render () {
return (
<Layout {...this.props}>
<h1>Profile</h1>
<pre>{JSON.stringify(this.state.user, 0, 2)}</pre>
</Layout>
)
}
}

Profile.getInitialProps = authInitialProps(false, true)
Loading