Skip to content

Commit

Permalink
feat(oauth2): Add oauth2 refresh support
Browse files Browse the repository at this point in the history
  • Loading branch information
MathiasCiarlo committed Sep 20, 2019
1 parent b446a76 commit 110fdf3
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 3 deletions.
3 changes: 2 additions & 1 deletion examples/demo/nuxt.config.js
Expand Up @@ -17,7 +17,8 @@ module.exports = {
},
auth: {
redirect: {
callback: '/callback'
callback: '/callback',
logout: '/signed-out'
},
strategies: {
local: {
Expand Down
14 changes: 14 additions & 0 deletions examples/demo/pages/signed-out.vue
@@ -0,0 +1,14 @@
<template>
<div>
<b-alert show variant="info">You have been signed out!</b-alert>
</div>
</template>

<script>
export default {
middleware: ['auth'],
options: {
auth: false
}
}
</script>
22 changes: 22 additions & 0 deletions lib/core/auth.js
Expand Up @@ -229,6 +229,28 @@ export default class Auth {
return this.$storage.syncUniversal(_key)
}

// ---------------------------------------------------------------
// Refresh token helpers
// ---------------------------------------------------------------

getRefreshToken (strategy) {
const _key = this.options.refresh_token.prefix + strategy

return this.$storage.getUniversal(_key)
}

setRefreshToken (strategy, refreshToken) {
const _key = this.options.refresh_token.prefix + strategy

return this.$storage.setUniversal(_key, refreshToken)
}

syncRefreshToken (strategy) {
const _key = this.options.refresh_token.prefix + strategy

return this.$storage.syncUniversal(_key)
}

// ---------------------------------------------------------------
// User helpers
// ---------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions lib/core/utilities.js
Expand Up @@ -83,3 +83,9 @@ export function decodeValue (val) {
// Return as is
return val
}

export class ExpiredSessionError extends Error {
constructor(message) {
super(message)
}
}
9 changes: 9 additions & 0 deletions lib/module/plugin.js
@@ -1,6 +1,7 @@
import Auth from './auth'

import './middleware'
import { ExpiredSessionError } from './utilities'

// Active schemes
<%= options.uniqueSchemes.map(path =>`import ${'scheme_' + hash(path)} from '${path.replace(/\\/g,'/')}'`).join('\n') %>
Expand Down Expand Up @@ -29,6 +30,14 @@ export default function (ctx, inject) {
// Initialize auth
return $auth.init().catch(error => {
if (process.client) {

// Don't console log expired session errors. This error is common, and expected to happen.
// The error happens whenever the user does an ssr request (reload/initial navigation) with an expired refresh
// token. We don't want to log this as an error.
if (error instanceof ExpiredSessionError) {
return
}

console.error('[ERROR] [AUTH]', error)
}
})
Expand Down
89 changes: 87 additions & 2 deletions lib/schemes/oauth2.js
@@ -1,5 +1,6 @@
import { encodeQuery, parseQuery } from '../utilities'
import { encodeQuery, parseQuery, ExpiredSessionError } from '../utilities'
import nanoid from 'nanoid'
import jwtDecode from 'jwt-decode'
const isHttps = process.server ? require('is-https') : null

const DEFAULTS = {
Expand Down Expand Up @@ -42,11 +43,14 @@ export default class Oauth2Scheme {
}

async mounted () {
// Sync token
// Sync tokens
const token = this.$auth.syncToken(this.name)
this.$auth.syncRefreshToken(this.name)

// Set axios token
if (token) {
this._setToken(token)
this.initializeTokenRefreshOnRequest()
}

// Handle callbacks on page load
Expand Down Expand Up @@ -198,4 +202,85 @@ export default class Oauth2Scheme {

return true // True means a redirect happened
}

// ---------------------------------------------------------------
// Watch axios requests for token expiration
// Refresh tokens if token has expired
// ---------------------------------------------------------------

initializeTokenRefreshOnRequest () {
const { $axios } = this.$auth.ctx.app
let isRefreshing = false

$axios.onRequest(async config => {
let token = this.$auth.getToken(this.name)
let refreshToken = this.$auth.getRefreshToken(this.name)

// Token or "refresh token" does not exists
if (!token || !refreshToken || !token.length || !refreshToken.length) {
return config
}

// If already trying to refresh token, do not try again
if (isRefreshing) {
return config
}

// Time variables
let tokenExpiresAt = jwtDecode(token).exp * 1000
let refreshTokenExpiresAt = jwtDecode(refreshToken).exp * 1000
const now = Date.now()

// Give us some slack to help the token from expiring between validation and usage
const timeSlackMillis = 500
tokenExpiresAt -= timeSlackMillis
refreshTokenExpiresAt -= timeSlackMillis


// Return if token has not expired
if (now < tokenExpiresAt) {
return config
}
// "Refresh token" has also expired. There is no way to refresh. Force logout.
if (now > refreshTokenExpiresAt) {
this.logout()

if(process.client) {
// Explicitly redirect to the signed-out page.
// We don't want to redirect a user with an expired token on page reload/first navigation.
// WatchLoggedIn in auth.js->mounted redirects to logout when the state changes,
// but it only works in the client
this.$auth.redirect('logout')
}
// Stop the request from happening. The original caller must catch ExpiredSessionErrors
throw new ExpiredSessionError()
}

// Try to refresh token before processing current request
isRefreshing = true

return $axios.post(this.options.access_token_endpoint,
encodeQuery({
refresh_token: refreshToken.replace(this.options.token_type + ' ', ''),
client_id: this.options.client_id,
grant_type: 'refresh_token'
})
).then(response => {
isRefreshing = false

// Update token and "refresh token"
token = this.options.token_type + ' ' + response.data.access_token
refreshToken = this.options.token_type + ' ' + response.data.refresh_token

this.$auth.setToken(this.name, token)
this.$auth.setRefreshToken(this.name, refreshToken)
$axios.setToken(token)

// Update token for current request and process it
config.headers['Authorization'] = token

return Promise.resolve(config)
})
})
}
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -48,6 +48,7 @@
"dotprop": "^1.2.0",
"is-https": "^1.0.0",
"js-cookie": "^2.2.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.15",
"nanoid": "^2.1.1"
},
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -6073,6 +6073,11 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"

jwt-decode@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=

kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
Expand Down

0 comments on commit 110fdf3

Please sign in to comment.