-
Notifications
You must be signed in to change notification settings - Fork 27k
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
Changes from all commits
bca7542
25c92d2
061f36a
653b798
c23b499
0a3f187
77c744c
3b8068d
397152d
e1b5414
2821cd3
13e1d89
60f782c
c4ddf4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import Router from 'next/router' | ||
import axios from 'axios' | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
res.finished = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. res.finished is no longer needed if |
||
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') | ||
} |
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" | ||
} |
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) }} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not needed, you can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then _document.js can also be removed I guess There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
) | ||
} | ||
} |
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() |
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) |
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.