├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── pages ├── index.js └── login.js ├── server.js └── utils └── CookieUtils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel", 4 | "es2015", 5 | "stage-3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nhan Tran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Demo](https://nextjs-simple-authentication-with-jwt-dgjcfwbaax.now.sh) 2 | 3 | # The idea behind 4 | The very basic auththentication implemetation for next.js with Jsonwebtoken. Basically, we use expressjs to customize the server side, by adding an authentication middleware, this middleware will check the `x-access-token` in every request comming to server. 5 |

6 | We also have a endpoint to generate jwt token and this token will be stored in cookies. I find this way is simple and work very well, especially in the time next.js haven't had an official example for this yet. 7 | 8 | # How to use 9 | 10 | 1. Clone or download 11 | 2. `npm install` 12 | 3. `npm run build` 13 | 4. `npm start` 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next.js-simple-authentication-with-jwt", 3 | "version": "1.0.0", 4 | "description": "Simple authentication with Jsonwebtoken for nextjs", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "next build && babel ./ --ignore ./node_modules,./dist -d dist", 8 | "dev": "babel-node server.js", 9 | "start": "NODE_ENV=production node dist/server.js" 10 | }, 11 | "author": "nhantran", 12 | "license": "MIT", 13 | "dependencies": { 14 | "axios": "^0.16.2", 15 | "body-parser": "^1.17.2", 16 | "cookie-parser": "^1.4.3", 17 | "crypto": "0.0.3", 18 | "express": "^4.15.3", 19 | "jsonwebtoken": "^7.4.1", 20 | "jwt-decode": "^2.2.0", 21 | "next": "^3.0.1-beta.8", 22 | "react": "^15.6.1", 23 | "react-dom": "^15.6.1" 24 | }, 25 | "devDependencies": { 26 | "babel-cli": "^6.24.1", 27 | "babel-preset-es2015": "^6.24.1", 28 | "babel-preset-stage-3": "^6.24.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Router from 'next/router' 3 | import jwtDecode from 'jwt-decode'; 4 | import { setCookie, getCookie } from '../utils/CookieUtils' 5 | import axios from 'axios' 6 | 7 | export default class Index extends Component { 8 | constructor (props) { 9 | super(props) 10 | this.state = { 11 | message: '' 12 | } 13 | this.logout = (e) => this._logout() 14 | this.testCSRF = (e) => this._testCSRF() 15 | 16 | } 17 | _logout () { 18 | setCookie('x-access-token', '') 19 | Router.push({ 20 | pathname: '/login' 21 | }) 22 | } 23 | // Use javascript to get jwt-token in cookies which can only 24 | // done by JavaScript that runs on your domain can read the cookie 25 | // You can override axios request config for general solution 26 | async _testCSRF () { 27 | const token = getCookie('x-access-token') 28 | const decoded = jwtDecode(token) 29 | try { 30 | const res = await axios.post(window.location.origin + '/api/preventCRSF', { 31 | example: 'data' 32 | }, { 33 | headers: { 34 | 'X-XSRF-TOKEN': decoded.xsrfToken 35 | } 36 | }) 37 | if (res.data.success) { 38 | this.setState({ 39 | message: res.data.message 40 | }) 41 | } 42 | } catch (error) { 43 | this.setState({ 44 | message: error.response.data.message 45 | }) 46 | } 47 | } 48 | render () { 49 | return ( 50 |
51 |

Authenticated

52 |
53 |
54 |
55 | {this.state.message} 56 |
57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pages/login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Link from 'next/link' 3 | import Router from 'next/router' 4 | import Head from 'next/head' 5 | import axios from 'axios' 6 | import { setCookie } from '../utils/CookieUtils' 7 | 8 | class Login extends Component { 9 | constructor (props) { 10 | super(props) 11 | this.state = { 12 | username: 'test', 13 | password: 'test', 14 | errorMessage: '' 15 | } 16 | this.changeUsername = e => this._changeUsername(e.target.value) 17 | this.changePassword = e => this._changePassword(e.target.value) 18 | this.login = e => this._login() 19 | } 20 | _changeUsername (value) { 21 | this.setState({ 22 | username: value 23 | }) 24 | } 25 | _changePassword (value) { 26 | this.setState({ 27 | password: value 28 | }) 29 | } 30 | async _login () { 31 | const { username, password } = this.state 32 | if (!username || !password) return 33 | try { 34 | const res = await axios.post(window.location.origin + '/authenticate', this.state) 35 | if (res.data.success) { 36 | setCookie('x-access-token', res.data.token) 37 | Router.push({ 38 | pathname: '/' 39 | }) 40 | } 41 | } catch (error) { 42 | this.setState({ 43 | errorMessage: error.response.data.message 44 | }) 45 | } 46 | } 47 | render () { 48 | const { username, password , errorMessage } = this.state 49 | return ( 50 |
51 |
52 |
Username: (test)
53 | 58 |
59 |
60 |
Password: (test)
61 | 66 |
67 | 73 |

{errorMessage}

74 |

You can not access index page without login

75 |

You can try by changing the url

76 |
77 | ) 78 | } 79 | } 80 | 81 | const styles = { 82 | login: { 83 | width: '50%', 84 | margin: 'auto', 85 | marginTop: '20px' 86 | }, 87 | button: { 88 | marginTop: '10px', 89 | }, 90 | user: { 91 | marginTop: '10px' 92 | }, 93 | password: { 94 | marginTop: '10px' 95 | } 96 | } 97 | 98 | export default Login 99 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import next from 'next' 3 | import bodyParser from 'body-parser' 4 | import cookieParser from 'cookie-parser' 5 | import jwt from 'jsonwebtoken' 6 | import crypto from 'crypto' 7 | 8 | const dev = process.env.NODE_ENV !== 'production' 9 | const app = next({ dev }) 10 | const handle = app.getRequestHandler() 11 | 12 | app.prepare().then(() => { 13 | const server = express() 14 | 15 | // Request body parsing middleware should be above methodOverride 16 | server.use(bodyParser.json()); 17 | server.use(bodyParser.urlencoded({ extended: false })); 18 | 19 | server.use(cookieParser()) 20 | 21 | // Verify username and password, if passed, we return jwt token for client 22 | // We also include xsrfToken for client, which will be used to prevent CSRF attack 23 | // and, you should use random complicated key (JWT Secret) to make brute forcing token very hard 24 | server.post('/authenticate', (req, res) => { 25 | const { username, password } = req.body 26 | if (username === 'test' || password === 'test') { 27 | var token = jwt.sign({ 28 | username: username, 29 | xsrfToken: crypto.createHash('md5').update(username).digest('hex') 30 | }, 'jwtSecret', { 31 | expiresIn: 60*60 32 | }); 33 | res.status(200).json({ 34 | success: true, 35 | message: 'Enjoy your token', 36 | token: token 37 | }) 38 | } else { 39 | res.status(400).json({ 40 | success: false, 41 | message: 'Authentication failed' 42 | }) 43 | } 44 | }) 45 | 46 | // Authenticate middleware 47 | // We will apply this middleware to every route except '/login' and '/_next' 48 | server.use(unless(['/login', '/_next'], (req, res, next) => { 49 | const token = req.cookies['x-access-token']; 50 | if (token) { 51 | jwt.verify(token, 'jwtSecret', (err, decoded) => { 52 | if (err) { 53 | res.redirect('/login'); 54 | } else { 55 | // if everything is good, save to request for use in other routes 56 | req.decoded = decoded; 57 | next(); 58 | } 59 | }) 60 | } else { 61 | res.redirect('/login'); 62 | } 63 | })) 64 | 65 | // Api example to prevent CRSF attack 66 | server.post('/api/preventCRSF', (req, res, next) => { 67 | if (req.decoded.xsrfToken === req.get('X-XSRF-TOKEN')) { 68 | res.status(200).json({ 69 | success: true, 70 | message: 'Yes, this api is protected by CRSF attack' 71 | }) 72 | } else { 73 | res.status(400).json({ 74 | success: false, 75 | message: 'CRSF attack is useless' 76 | }) 77 | } 78 | }) 79 | 80 | server.get('*', (req, res) => { 81 | return handle(req, res) 82 | }) 83 | 84 | server.listen(3000, (err) => { 85 | if (err) throw err 86 | console.log('> Ready on http://localhost:3000') 87 | }) 88 | }) 89 | 90 | function unless (paths, middleware) { 91 | return function(req, res, next) { 92 | let isHave = false 93 | paths.forEach((path) => { 94 | if (path === req.path || req.path.includes(path)) { 95 | isHave = true 96 | return 97 | } 98 | }) 99 | if (isHave) { 100 | return next() 101 | } else { 102 | return middleware(req, res, next) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /utils/CookieUtils.js: -------------------------------------------------------------------------------- 1 | export function setCookie(cname, cvalue, exdays) { 2 | var d = new Date(); 3 | d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); 4 | var expires = "expires=" + d.toUTCString(); 5 | document.cookie = cname + "=" + cvalue + ";" + expires + "HttpOnly;" + ";path=/"; 6 | } 7 | 8 | export function getCookie(cname) { 9 | var name = cname + "="; 10 | var ca = document.cookie.split(';'); 11 | for(var i = 0; i < ca.length; i++) { 12 | var c = ca[i]; 13 | while (c.charAt(0) === ' ') { 14 | c = c.substring(1); 15 | } 16 | if (c.indexOf(name) === 0) { 17 | return c.substring(name.length, c.length); 18 | } 19 | } 20 | return ""; 21 | } 22 | --------------------------------------------------------------------------------