├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── _helpers │ ├── auth-header.js │ ├── fake-backend.js │ ├── index.js │ └── router.js ├── _services │ ├── index.js │ └── user.service.js ├── _store │ ├── alert.module.js │ ├── authentication.module.js │ ├── index.js │ └── users.module.js ├── app │ └── App.vue ├── home │ └── HomePage.vue ├── index.html ├── index.js └── login │ └── LoginPage.vue └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | typings 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jason Watmore 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 | # vue-vuex-jwt-authentication-example 2 | 3 | Vue + Vuex - JWT Authentication Tutorial & Example 4 | 5 | To see a demo and further details go to http://jasonwatmore.com/post/2018/07/06/vue-vuex-jwt-authentication-tutorial-example -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-vuex-jwt-authentication-example", 3 | "version": "1.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/cornflourblue/vue-vuex-jwt-authentication-example.git" 7 | }, 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "webpack-dev-server --open" 11 | }, 12 | "dependencies": { 13 | "vue": "^2.5.16", 14 | "vue-router": "^3.0.1", 15 | "vuex": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "babel-core": "^6.26.0", 19 | "babel-loader": "^7.1.5", 20 | "babel-preset-env": "^1.6.1", 21 | "babel-preset-stage-0": "^6.24.1", 22 | "babel-preset-vue": "^2.0.2", 23 | "css-loader": "^3.4.2", 24 | "html-webpack-plugin": "^3.2.0", 25 | "path": "^0.12.7", 26 | "vue-loader": "^14.2.3", 27 | "vue-template-compiler": "^2.5.16", 28 | "webpack": "^4.15.0", 29 | "webpack-cli": "^3.0.8", 30 | "webpack-dev-server": "^3.1.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/_helpers/auth-header.js: -------------------------------------------------------------------------------- 1 | export function authHeader() { 2 | // return authorization header with jwt token 3 | let user = JSON.parse(localStorage.getItem('user')); 4 | 5 | if (user && user.token) { 6 | return { 'Authorization': 'Bearer ' + user.token }; 7 | } else { 8 | return {}; 9 | } 10 | } -------------------------------------------------------------------------------- /src/_helpers/fake-backend.js: -------------------------------------------------------------------------------- 1 | export function configureFakeBackend() { 2 | let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }]; 3 | let realFetch = window.fetch; 4 | window.fetch = function (url, opts) { 5 | return new Promise((resolve, reject) => { 6 | // wrap in timeout to simulate server api call 7 | setTimeout(() => { 8 | 9 | // authenticate 10 | if (url.endsWith('/users/authenticate') && opts.method === 'POST') { 11 | // get parameters from post request 12 | let params = JSON.parse(opts.body); 13 | 14 | // find if any user matches login credentials 15 | let filteredUsers = users.filter(user => { 16 | return user.username === params.username && user.password === params.password; 17 | }); 18 | 19 | if (filteredUsers.length) { 20 | // if login details are valid return user details and fake jwt token 21 | let user = filteredUsers[0]; 22 | let responseJson = { 23 | id: user.id, 24 | username: user.username, 25 | firstName: user.firstName, 26 | lastName: user.lastName, 27 | token: 'fake-jwt-token' 28 | }; 29 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(responseJson)) }); 30 | } else { 31 | // else return error 32 | reject('Username or password is incorrect'); 33 | } 34 | 35 | return; 36 | } 37 | 38 | // get users 39 | if (url.endsWith('/users') && opts.method === 'GET') { 40 | // check for fake auth token in header and return users if valid, this security is implemented server side in a real application 41 | if (opts.headers && opts.headers.Authorization === 'Bearer fake-jwt-token') { 42 | resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(users)) }); 43 | } else { 44 | // return 401 not authorised if token is null or invalid 45 | reject('Unauthorised'); 46 | } 47 | 48 | return; 49 | } 50 | 51 | // pass through any requests not handled above 52 | realFetch(url, opts).then(response => resolve(response)); 53 | 54 | }, 500); 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /src/_helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './fake-backend'; 2 | export * from './router'; 3 | export * from './auth-header'; -------------------------------------------------------------------------------- /src/_helpers/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | import HomePage from '../home/HomePage' 5 | import LoginPage from '../login/LoginPage' 6 | 7 | Vue.use(Router); 8 | 9 | export const router = new Router({ 10 | mode: 'history', 11 | routes: [ 12 | { path: '/', component: HomePage }, 13 | { path: '/login', component: LoginPage }, 14 | 15 | // otherwise redirect to home 16 | { path: '*', redirect: '/' } 17 | ] 18 | }); 19 | 20 | router.beforeEach((to, from, next) => { 21 | // redirect to login page if not logged in and trying to access a restricted page 22 | const publicPages = ['/login']; 23 | const authRequired = !publicPages.includes(to.path); 24 | const loggedIn = localStorage.getItem('user'); 25 | 26 | if (authRequired && !loggedIn) { 27 | return next('/login'); 28 | } 29 | 30 | next(); 31 | }) -------------------------------------------------------------------------------- /src/_services/index.js: -------------------------------------------------------------------------------- 1 | export * from './user.service'; 2 | -------------------------------------------------------------------------------- /src/_services/user.service.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import { authHeader } from '../_helpers'; 3 | 4 | export const userService = { 5 | login, 6 | logout, 7 | getAll 8 | }; 9 | 10 | function login(username, password) { 11 | const requestOptions = { 12 | method: 'POST', 13 | headers: { 'Content-Type': 'application/json' }, 14 | body: JSON.stringify({ username, password }) 15 | }; 16 | 17 | return fetch(`${config.apiUrl}/users/authenticate`, requestOptions) 18 | .then(handleResponse) 19 | .then(user => { 20 | // login successful if there's a jwt token in the response 21 | if (user.token) { 22 | // store user details and jwt token in local storage to keep user logged in between page refreshes 23 | localStorage.setItem('user', JSON.stringify(user)); 24 | } 25 | 26 | return user; 27 | }); 28 | } 29 | 30 | function logout() { 31 | // remove user from local storage to log user out 32 | localStorage.removeItem('user'); 33 | } 34 | 35 | function getAll() { 36 | const requestOptions = { 37 | method: 'GET', 38 | headers: authHeader() 39 | }; 40 | 41 | return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse); 42 | } 43 | 44 | function handleResponse(response) { 45 | return response.text().then(text => { 46 | const data = text && JSON.parse(text); 47 | if (!response.ok) { 48 | if (response.status === 401) { 49 | // auto logout if 401 response returned from api 50 | logout(); 51 | location.reload(true); 52 | } 53 | 54 | const error = (data && data.message) || response.statusText; 55 | return Promise.reject(error); 56 | } 57 | 58 | return data; 59 | }); 60 | } -------------------------------------------------------------------------------- /src/_store/alert.module.js: -------------------------------------------------------------------------------- 1 | export const alert = { 2 | namespaced: true, 3 | state: { 4 | type: null, 5 | message: null 6 | }, 7 | actions: { 8 | success({ commit }, message) { 9 | commit('success', message); 10 | }, 11 | error({ commit }, message) { 12 | commit('error', message); 13 | }, 14 | clear({ commit }) { 15 | commit('clear'); 16 | } 17 | }, 18 | mutations: { 19 | success(state, message) { 20 | state.type = 'alert-success'; 21 | state.message = message; 22 | }, 23 | error(state, message) { 24 | state.type = 'alert-danger'; 25 | state.message = message; 26 | }, 27 | clear(state) { 28 | state.type = null; 29 | state.message = null; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/_store/authentication.module.js: -------------------------------------------------------------------------------- 1 | import { userService } from '../_services'; 2 | import { router } from '../_helpers'; 3 | 4 | const user = JSON.parse(localStorage.getItem('user')); 5 | const initialState = user 6 | ? { status: { loggedIn: true }, user } 7 | : { status: {}, user: null }; 8 | 9 | export const authentication = { 10 | namespaced: true, 11 | state: initialState, 12 | actions: { 13 | login({ dispatch, commit }, { username, password }) { 14 | commit('loginRequest', { username }); 15 | 16 | userService.login(username, password) 17 | .then( 18 | user => { 19 | commit('loginSuccess', user); 20 | router.push('/'); 21 | }, 22 | error => { 23 | commit('loginFailure', error); 24 | dispatch('alert/error', error, { root: true }); 25 | } 26 | ); 27 | }, 28 | logout({ commit }) { 29 | userService.logout(); 30 | commit('logout'); 31 | } 32 | }, 33 | mutations: { 34 | loginRequest(state, user) { 35 | state.status = { loggingIn: true }; 36 | state.user = user; 37 | }, 38 | loginSuccess(state, user) { 39 | state.status = { loggedIn: true }; 40 | state.user = user; 41 | }, 42 | loginFailure(state) { 43 | state.status = {}; 44 | state.user = null; 45 | }, 46 | logout(state) { 47 | state.status = {}; 48 | state.user = null; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/_store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import { alert } from './alert.module'; 5 | import { authentication } from './authentication.module'; 6 | import { users } from './users.module'; 7 | 8 | Vue.use(Vuex); 9 | 10 | export const store = new Vuex.Store({ 11 | modules: { 12 | alert, 13 | authentication, 14 | users 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/_store/users.module.js: -------------------------------------------------------------------------------- 1 | import { userService } from '../_services'; 2 | 3 | export const users = { 4 | namespaced: true, 5 | state: { 6 | all: {} 7 | }, 8 | actions: { 9 | getAll({ commit }) { 10 | commit('getAllRequest'); 11 | 12 | userService.getAll() 13 | .then( 14 | users => commit('getAllSuccess', users), 15 | error => commit('getAllFailure', error) 16 | ); 17 | } 18 | }, 19 | mutations: { 20 | getAllRequest(state) { 21 | state.all = { loading: true }; 22 | }, 23 | getAllSuccess(state, users) { 24 | state.all = { items: users }; 25 | }, 26 | getAllFailure(state, error) { 27 | state.all = { error }; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/home/HomePage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue + Vuex - JWT Authentication Example & Tutorial 6 | 7 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |

17 | Vue + Vuex - JWT Authentication Tutorial & Example 18 |

19 |

20 | JasonWatmore.com 21 |

22 |
23 | 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { store } from './_store'; 4 | import { router } from './_helpers'; 5 | import App from './app/App'; 6 | 7 | // setup fake backend 8 | import { configureFakeBackend } from './_helpers'; 9 | configureFakeBackend(); 10 | 11 | new Vue({ 12 | el: '#app', 13 | router, 14 | store, 15 | render: h => h(App) 16 | }); -------------------------------------------------------------------------------- /src/login/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | resolve: { 7 | extensions: ['.js', '.vue'] 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.vue?$/, 13 | exclude: /(node_modules)/, 14 | use: 'vue-loader' 15 | }, 16 | { 17 | test: /\.js?$/, 18 | exclude: /(node_modules)/, 19 | use: 'babel-loader' 20 | } 21 | ] 22 | }, 23 | plugins: [new HtmlWebpackPlugin({ 24 | template: './src/index.html' 25 | })], 26 | devServer: { 27 | historyApiFallback: true 28 | }, 29 | externals: { 30 | // global app config object 31 | config: JSON.stringify({ 32 | apiUrl: 'http://localhost:4000' 33 | }) 34 | } 35 | } --------------------------------------------------------------------------------