Skip to content

Commit

Permalink
feat(refresh scheme): add request interceptor and refresh token expir…
Browse files Browse the repository at this point in the history
…ation
  • Loading branch information
JoaoPedroAS51 committed Jun 11, 2019
1 parent ee82e15 commit 35744ea
Showing 1 changed file with 68 additions and 52 deletions.
120 changes: 68 additions & 52 deletions lib/schemes/refresh.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,53 @@ export default class RefreshScheme extends LocalScheme {

this.refreshInterval = undefined
this.isRefreshing = false
this.failedRequestQueue = []
this.hasRefreshTokenChanged = false
}

// ---------------------------------------------------------------
// Token Expiration helpers
// ---------------------------------------------------------------

_getExpiration () {
_getTokenExpiration () {
const _key = this.options.tokenExpirationPrefix + this.name

return this.$auth.$storage.getUniversal(_key)
}

_setExpiration (token) {
_setTokenExpiration (token) {
const _key = this.options.tokenExpirationPrefix + this.name

return this.$auth.$storage.setUniversal(_key, token)
}

_syncExpiration () {
_syncTokenExpiration () {
const _key = this.options.tokenExpirationPrefix + this.name

return this.$auth.$storage.syncUniversal(_key)
}

// ---------------------------------------------------------------
// Refresh Token Expiration helpers
// ---------------------------------------------------------------

_getRefreshTokenExpiration () {
const _key = this.options.refreshTokenExpirationPrefix + this.name

return this.$auth.$storage.getUniversal(_key)
}

_setRefreshTokenExpiration (token) {
const _key = this.options.refreshTokenExpirationPrefix + this.name

return this.$auth.$storage.setUniversal(_key, token)
}

_syncRefreshTokenExpiration () {
const _key = this.options.refreshTokenExpirationPrefix + this.name

return this.$auth.$storage.syncUniversal(_key)
}

// ---------------------------------------------------------------
// Refresh token helpers
// ---------------------------------------------------------------
Expand Down Expand Up @@ -69,12 +91,19 @@ export default class RefreshScheme extends LocalScheme {
// Update refresh token and register refresh-logic with axios
const refreshToken = getProp(result, this.options.refreshToken.property)
if (refreshToken !== undefined) {
this.hasRefreshTokenChanged = refreshToken === this._getRefreshToken()
this._setRefreshToken(refreshToken)

const _tokenCreatedAt = getProp(result, this.options.createdAt) || Date.now()
const _tokenTTL = getProp(result, this.options.expiresIn) || this.options.token.maxAge
const tokenExpiration = _tokenCreatedAt + (_tokenTTL * 1000)
this._setExpiration(tokenExpiration)
this._setTokenExpiration(tokenExpiration)

// Update refresh token expiration
if (!this._getRefreshTokenExpiration() || this.hasRefreshTokenChanged) {
this._setRefreshTokenExpiration(Date.now() + (this.options.refreshToken.maxAge * 1000))
this.hasRefreshTokenChanged = false
}
}

// Update client id
Expand All @@ -83,18 +112,6 @@ export default class RefreshScheme extends LocalScheme {
this.$auth.setClientId(this.name, clientId)
}

// If have failed requests in the queue
if (this.failedRequestQueue.length) {
// Initialize a local queue for axios requests for each config in failedRequestQueue
const failedRequestQueue = this.failedRequestQueue.map(config => this.$auth.requestWith(this.name, config))

// Clear global queue
this.failedRequestQueue = []

// Retry all failed requests
await Promise.all(failedRequestQueue)
}

// End the refresh
this.isRefreshing = false
}
Expand Down Expand Up @@ -151,7 +168,7 @@ export default class RefreshScheme extends LocalScheme {
// If auto refresh is disabled, bail
if (!this.options.autoRefresh.enable) return

let intervalDuration = (this._getExpiration() - Date.now()) * 0.75
let intervalDuration = (this._getTokenExpiration() - Date.now()) * 0.75
if (intervalDuration < 1000) {
// in case you misconfigured refreshing this will save your auth-server from a self-induced DDoS-Attack
intervalDuration = 1000
Expand All @@ -162,42 +179,38 @@ export default class RefreshScheme extends LocalScheme {
}, intervalDuration)
}

_initializeErrorInterceptor () {
this.$auth.ctx.app.$axios.onError(error => {
// If error has config and response and error status is 401
if (error.config && error.response && error.response.status === 401) {
const token = this.$auth.getToken(this.name)
const headerToken = error.config.headers[this.options.tokenName]

// If current token is equal to header token
if (token && headerToken && headerToken === token) {
// Add the failed request config to failedRequestQueue
if (!error.config.__isRetryRequest) {
error.config.__isRetryRequest = true
this.failedRequestQueue.push(error.config)
}
_initializeRequestInterceptor () {
this.$auth.ctx.app.$axios.onRequest(async config => {
// Sync tokens
const token = this.$auth.syncToken(this.name)
const refreshToken = this._syncRefreshToken()
this._setToken(token)

// If is not refreshing the token
if (!this.isRefreshing) {
// Refresh it
// If no token or no refresh token, bail
if (!token || !refreshToken) return config

if (!this.isRefreshing) {
const tokenExpiration = this._getTokenExpiration()
const refreshTokenExpiration = this._getRefreshTokenExpiration()

if (Date.now() >= tokenExpiration) {
if (Date.now() < refreshTokenExpiration) {
// Refresh the token
return this._tokenRefresh().then(() => {
this._syncExpiration()
this._syncTokenExpiration()
this._scheduleTokenRefresh()
})

// Update Authorization header
config.headers[this.options.tokenName] = this.$auth.getToken(this.name)
return Promise.resolve(config)
}).catch(error => Promise.reject(error))
} else {
// Or resolve Promise
return Promise.resolve()
this._logoutLocally()
}
} else if (token && headerToken && headerToken !== token && !error.config.__isRetryRequest) {
// Or just retry the request if token is not equal to header token
error.config.__isRetryRequest = true
error.config.headers[this.options.tokenName] = token
return this.$auth.ctx.app.$axios.request(error.config)
}
}

// Otherwise, reject Promise
return Promise.reject(error)
return config
})
}

Expand All @@ -206,8 +219,9 @@ export default class RefreshScheme extends LocalScheme {
const token = this.$auth.syncToken(this.name)
this._setToken(token)
this._syncRefreshToken()
this._syncRefreshTokenExpiration()

const expiration = this._syncExpiration()
const expiration = this._syncTokenExpiration()
if (Date.now() >= expiration) {
await this._logoutLocally()
}
Expand All @@ -216,14 +230,14 @@ export default class RefreshScheme extends LocalScheme {
}

return this.$auth.fetchUserOnce().then((response) => {
// Initialize axios error interceptor
this._initializeErrorInterceptor()
// Initialize axios request interceptor
this._initializeRequestInterceptor()

// Only refresh token if user is logged in and is client side
if (process.client && this.$auth.loggedIn && this.options.autoRefresh.enable) {
setTimeout(() => {
this._tokenRefresh().then(() => {
this._syncExpiration()
this._syncTokenExpiration()
this._scheduleTokenRefresh()
})
}, 1000)
Expand Down Expand Up @@ -301,10 +315,11 @@ export default class RefreshScheme extends LocalScheme {
clearInterval(this.refreshInterval)

this.isRefreshing = false
this.failedRequestQueue = []
this.hasRefreshTokenChanged = false

this._setRefreshToken(false)
this._setExpiration(false)
this._setTokenExpiration(false)
this._setRefreshTokenExpiration(false)

return this.$auth.reset()
}
Expand All @@ -326,6 +341,7 @@ const DEFAULTS = {
dataClientId: 'client_id',
dataGrantType: 'grant_type',
tokenExpirationPrefix: '_token_expires_at.',
refreshTokenExpirationPrefix: '_refresh_token_expires_at.',
refreshTokenPrefix: '_refresh_token.',
endpoints: {
refresh: {
Expand Down

0 comments on commit 35744ea

Please sign in to comment.