Skip to content

Instantly share code, notes, and snippets.

@fabioanderegg
Last active July 25, 2018 16:02
Show Gist options
  • Save fabioanderegg/5080f0415a077344e07decf16a53787d to your computer and use it in GitHub Desktop.
Save fabioanderegg/5080f0415a077344e07decf16a53787d to your computer and use it in GitHub Desktop.
Vue.js JWT authentication with Django REST Framework Simple JWT as backend
import axios from 'axios'
import { Message } from 'element-ui'
import jwtDecode from 'jwt-decode'
import endsWith from 'lodash.endswith'
import config from '../config'
import store from '@/store'
import router from '@/router'
const LOGIN_URL = 'token/'
const REFRESH_URL = 'token/refresh/'
const http = axios.create({
baseURL: config.API_URL
})
export async function login (username, password) {
let response = null
try {
response = await http.post('token/', {
username: username,
password: password
})
} catch (error) {
if (error.response.status === 400) {
// user/password is wrong
return false
} else {
// some other error (e.g. 500) occurred
throw new Error('Login request error')
}
}
await store.dispatch('auth/login', { accessToken: response.data.access, refreshToken: response.data.refresh })
return true
}
export async function refreshToken () {
let response = null
try {
response = await http.post(REFRESH_URL, {
refresh: store.state.auth.refreshToken
})
} catch (error) {
if (error.response.status === 401) {
// refresh token is not valid
return false
} else {
// some other error (e.g. 500) occurred
throw new Error('Refresh token request error')
}
}
await store.dispatch('auth/login', { accessToken: response.data.access, refreshToken: response.data.refresh })
return true
}
export function isJWTValid (token) {
if (!token) {
return false
}
let data = null
try {
data = jwtDecode(token)
const tokenDate = new Date(data.exp * 1000)
const now = new Date()
return tokenDate > now
} catch (e) {
return false
}
}
const LOGIN_ERROR_MESSAGE = 'login'
export function handleAPIError (error) {
// do not show error message when error comes from a redirect to login
if (error.message && error.message === LOGIN_ERROR_MESSAGE) {
return
}
Message.error('An error has occurred, please try again later')
}
const tryTokensRefresh = async () => {
if (store.getters['auth/refreshTokenValid']) {
let success = null
try {
success = await refreshToken()
} catch (e) {
// some kind of server error occurred, cancel the request
throw new axios.Cancel('Token refresh error')
}
if (!success) {
// refreshing failed (probably because it is expired), user has to login again
router.push({name: 'Login'})
throw new axios.Cancel(LOGIN_ERROR_MESSAGE)
}
} else {
// refresh token is expired, user has to login again
router.push({ name: 'Login' })
throw new axios.Cancel(LOGIN_ERROR_MESSAGE)
}
}
// interceptor to handle case when access token is no longer valid
http.interceptors.request.use(async (config) => {
// login/refresh urls do not require valid tokens
if (config.url === LOGIN_URL || config.url === REFRESH_URL) {
return config
}
// if access token is no longer valid (expired), try to refresh it with the refresh token
if (!store.getters['auth/accessTokenValid']) {
await tryTokensRefresh()
// if the refresh is successful, the store now has a valid access token and we can use it to do the request
// if not, the user will be redirected to the login view
}
config.headers.Authorization = 'Bearer ' + store.state.auth.accessToken
return config
}, (error) => {
return Promise.reject(error)
})
// interceptor to handle case when we considered the access token still valid
// but the server disagrees and sends back a 401 error
http.interceptors.response.use((config) => {
return config
}, async (error) => {
if (!error.config ||
endsWith(error.config.url, LOGIN_URL) ||
endsWith(error.config.url, REFRESH_URL) ||
!error.response ||
error.response.status !== 401) {
return Promise.reject(error)
}
// if response code is something else than token invalid (e.g. user was deleted/set inactive), go to login
if (error.response.data.code !== 'token_not_valid') {
router.push({ name: 'Login' })
throw new axios.Cancel(LOGIN_ERROR_MESSAGE)
}
await tryTokensRefresh()
// if successful, the store now has a valid access token and we can retry to request with it
// if not, the user has been redirected to the login view
return http.request(error.config)
})
export default http
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
import pytest
import factory
from django.contrib.auth.models import User
from pytest_factoryboy import register
from rest_framework.test import APIClient
# enable database for all tests
@pytest.fixture(autouse=True)
def enable_db(db):
pass
DEFAULT_USER_PASSWORD = 'password'
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
username = 'user'
password = factory.PostGenerationMethodCall('set_password', DEFAULT_USER_PASSWORD)
register(UserFactory)
@pytest.fixture
def api_client(user_factory):
user = user_factory()
client = APIClient()
response = client.post('/api/token/', {
'username': user.username,
'password': DEFAULT_USER_PASSWORD,
}, format='json')
client.credentials(HTTP_AUTHORIZATION='Bearer ' + response.data['access'])
return client
vue
vuex
axios
jwt-decode
lodash.endswith
vuex-persistedstate
element-ui
<template>
<div class="login-container">
<div class="login">
<h1 class="heading">Login</h1>
<el-alert title="Invalid username or password" :closable="false" type="error" v-if="error" />
<el-form ref="form" :model="form" :rules="rules">
<el-form-item label="Username" prop="username">
<el-input v-model="form.username" @keyup.enter.native="submit"></el-input>
</el-form-item>
<el-form-item label="Password" prop="password">
<el-input v-model="form.password" type="password" @keyup.enter.native="submit"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">Login</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import {createNamespacedHelpers} from 'vuex'
import { login, handleAPIError } from '@/api'
import router from '@/router'
const {mapActions} = createNamespacedHelpers('auth')
export default {
data () {
return {
form: {
username: '',
password: ''
},
rules: {
username: [{ required: true, message: 'This field is required', trigger: 'change' }],
password: [{ required: true, message: 'This field is required', trigger: 'change' }]
},
error: false
}
},
methods: {
...mapActions(['login']),
async submit () {
this.error = false
try {
await this.$refs.form.validate()
} catch (e) {
return
}
let success = null
try {
success = await login(this.form.username, this.form.password)
} catch (e) {
handleAPIError(e)
}
if (success) {
router.push({name: 'CustomerList'})
} else {
this.error = true
}
}
}
}
</script>
<style lang="sass" scoped>
.heading
text-align: center
margin-top: 30px
.login-container
display: flex
flex-direction: column
flex-grow: 1
justify-content: center
align-items: center
.login
width: 300px
</style>
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/login',
name: 'Login',
component: Login
}
]
})
# dependency: djangorestframework-simplejwt
from datetime import timedelta
REST_FRAMEWORK = {
# all api reviews require authentication by default
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
# generate a new refresh token when the access token is refreshed
# this way a user newer gets logged out if logs in at least every
# REFRESH_TOKEN_LIFETIME days
'ROTATE_REFRESH_TOKENS': True,
}
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import auth from './store_auth'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
modules: {
auth
},
plugins: [createPersistedState({
paths: ['auth.accessToken', 'auth.refreshToken']
})],
strict: debug
})
import { isJWTValid } from '@/api'
export default {
namespaced: true,
state: {
accessToken: null,
refreshToken: null
},
getters: {
accessTokenValid (state) {
return isJWTValid(state.accessToken)
},
refreshTokenValid (state) {
return isJWTValid(state.refreshToken)
}
},
actions: {
login ({ commit }, data) {
commit('loginMutation', data)
}
},
mutations: {
loginMutation (state, { accessToken, refreshToken }) {
state.accessToken = accessToken
state.refreshToken = refreshToken
}
}
}
<template>
<div>
<span v-if="accessTokenValid">logged in</span>
<span v-else>not logged in</span>
</template>
<script>
import { createNamespacedHelpers } from 'vuex'
import HTTP, { handleAPIError } from '@/api'
const { mapGetters } = createNamespacedHelpers('auth')
export default {
computed: {
...mapGetters(['accessTokenValid'])
},
mounted() {
try {
response = await HTTP.get(`something`)
} catch (e) {
// use this function to not show an error message when the user is not logged in and gets redirected to login page
handleAPIError(e)
return
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment