Comparing version 2.1.1 to 2.3.0
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'jsdom', | ||
setupFilesAfterEnv: ['./jest.setup.ts'] | ||
} |
{ | ||
"name": "axios-jwt", | ||
"version": "2.1.1", | ||
"version": "2.3.0", | ||
"description": "Axios interceptor to store, use, and refresh tokens for authentication.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -53,3 +53,3 @@ # axios-jwt | ||
import { IAuthTokens, TokenRefreshRequest, applyAuthTokenInterceptor } from 'axios-jwt' | ||
import { IAuthTokens, TokenRefreshRequest, applyAuthTokenInterceptor, getBrowserLocalStorage } from 'axios-jwt' | ||
import axios from 'axios' | ||
@@ -81,2 +81,8 @@ | ||
applyAuthTokenInterceptor(axiosInstance, { requestRefresh }) | ||
// New to 2.2.0+: initialize with storage: localStorage/sessionStorage/nativeStorage. Helpers: getBrowserLocalStorage, getBrowserSessionStorage | ||
const getStorage = getBrowserLocalStorage | ||
// You can create you own storage, it has to comply with type StorageType | ||
applyAuthTokenInterceptor(axiosInstance, { requestRefresh, getStorage }) | ||
``` | ||
@@ -83,0 +89,0 @@ |
318
src/index.ts
@@ -1,113 +0,6 @@ | ||
import axios from 'axios' | ||
import type { AxiosInstance, AxiosRequestConfig } from 'axios' | ||
import jwtDecode from 'jwt-decode' | ||
import type { JwtPayload } from 'jwt-decode' | ||
import Storage from './storage' | ||
import { applyAuthTokenInterceptor } from './applyAuthTokenInterceptor' | ||
// a little time before expiration to try refresh (seconds) | ||
const EXPIRE_FUDGE = 10 | ||
export const STORAGE_KEY = `auth-tokens-${process.env.NODE_ENV}` | ||
type Token = string | ||
export interface IAuthTokens { | ||
accessToken: Token | ||
refreshToken: Token | ||
} | ||
// EXPORTS | ||
/** | ||
* Checks if refresh tokens are stored | ||
* @returns Whether the user is logged in or not | ||
*/ | ||
export const isLoggedIn = (): boolean => { | ||
const token = getRefreshToken() | ||
return !!token | ||
} | ||
/** | ||
* Sets the access and refresh tokens | ||
* @param {IAuthTokens} tokens - Access and Refresh tokens | ||
*/ | ||
export const setAuthTokens = (tokens: IAuthTokens): void => | ||
Storage.setItem(STORAGE_KEY, JSON.stringify(tokens)) | ||
/** | ||
* Sets the access token | ||
* @param {string} token - Access token | ||
*/ | ||
export const setAccessToken = (token: Token): void => { | ||
const tokens = getAuthTokens() | ||
if (!tokens) { | ||
throw new Error('Unable to update access token since there are not tokens currently stored') | ||
} | ||
tokens.accessToken = token | ||
setAuthTokens(tokens) | ||
} | ||
/** | ||
* Clears both tokens | ||
*/ | ||
export const clearAuthTokens = (): void => Storage.removeItem(STORAGE_KEY) | ||
/** | ||
* Returns the stored refresh token | ||
* @returns {string} Refresh token | ||
*/ | ||
export const getRefreshToken = (): Token | undefined => { | ||
const tokens = getAuthTokens() | ||
return tokens ? tokens.refreshToken : undefined | ||
} | ||
/** | ||
* Returns the stored access token | ||
* @returns {string} Access token | ||
*/ | ||
export const getAccessToken = (): Token | undefined => { | ||
const tokens = getAuthTokens() | ||
return tokens ? tokens.accessToken : undefined | ||
} | ||
/** | ||
* @callback requestRefresh | ||
* @param {string} refreshToken - Token that is sent to the backend | ||
* @returns {Promise} Promise that resolves in an access token | ||
*/ | ||
/** | ||
* Gets the current access token, exchanges it with a new one if it's expired and then returns the token. | ||
* @param {requestRefresh} requestRefresh - Function that is used to get a new access token | ||
* @returns {string} Access token | ||
*/ | ||
export const refreshTokenIfNeeded = async ( | ||
requestRefresh: TokenRefreshRequest | ||
): Promise<Token | undefined> => { | ||
// use access token (if we have it) | ||
let accessToken = getAccessToken() | ||
// check if access token is expired | ||
if (!accessToken || isTokenExpired(accessToken)) { | ||
// do refresh | ||
accessToken = await refreshToken(requestRefresh) | ||
} | ||
return accessToken | ||
} | ||
/** | ||
* | ||
* @param {Axios} axios - Axios instance to apply the interceptor to | ||
* @param {IAuthTokenInterceptorConfig} config - Configuration for the interceptor | ||
*/ | ||
export const applyAuthTokenInterceptor = ( | ||
axios: AxiosInstance, | ||
config: IAuthTokenInterceptorConfig | ||
): void => { | ||
if (!axios.interceptors) throw new Error(`invalid axios instance: ${axios}`) | ||
axios.interceptors.request.use(authTokenInterceptor(config)) | ||
} | ||
/** | ||
* @deprecated This method has been renamed to applyAuthTokenInterceptor and will be removed in a future release. | ||
@@ -117,200 +10,11 @@ */ | ||
// PRIVATE | ||
/** | ||
* Returns the refresh and access tokens | ||
* @returns {IAuthTokens} Object containing refresh and access tokens | ||
*/ | ||
const getAuthTokens = (): IAuthTokens | undefined => { | ||
const rawTokens = Storage.getItem(STORAGE_KEY) | ||
if (!rawTokens) return | ||
try { | ||
// parse stored tokens JSON | ||
return JSON.parse(rawTokens) | ||
} catch (error: unknown) { | ||
if (error instanceof SyntaxError) { | ||
error.message = `Failed to parse auth tokens: ${rawTokens}` | ||
throw error | ||
} | ||
} | ||
} | ||
/** | ||
* Checks if the token is undefined, has expired or is about the expire | ||
* | ||
* @param {string} token - Access token | ||
* @returns Whether or not the token is undefined, has expired or is about the expire | ||
*/ | ||
const isTokenExpired = (token: Token): boolean => { | ||
if (!token) return true | ||
const expiresIn = getExpiresIn(token) | ||
return !expiresIn || expiresIn <= EXPIRE_FUDGE | ||
} | ||
/** | ||
* Gets the unix timestamp from an access token | ||
* | ||
* @param {string} token - Access token | ||
* @returns {string} Unix timestamp | ||
*/ | ||
const getTimestampFromToken = (token: Token): number | undefined => { | ||
const decoded = jwtDecode<JwtPayload>(token) | ||
return decoded.exp | ||
} | ||
/** | ||
* Returns the number of seconds before the access token expires or -1 if it already has | ||
* | ||
* @param {string} token - Access token | ||
* @returns {number} Number of seconds before the access token expires | ||
*/ | ||
const getExpiresIn = (token: Token): number => { | ||
const expiration = getTimestampFromToken(token) | ||
if (!expiration) return -1 | ||
return expiration - Date.now() / 1000 | ||
} | ||
/** | ||
* Refreshes the access token using the provided function | ||
* | ||
* @param {requestRefresh} requestRefresh - Function that is used to get a new access token | ||
* @returns {string} - Fresh access token | ||
*/ | ||
const refreshToken = async (requestRefresh: TokenRefreshRequest): Promise<Token> => { | ||
const refreshToken = getRefreshToken() | ||
if (!refreshToken) throw new Error('No refresh token available') | ||
try { | ||
isRefreshing = true | ||
// Refresh and store access token using the supplied refresh function | ||
const newTokens = await requestRefresh(refreshToken) | ||
if (typeof newTokens === 'object' && newTokens?.accessToken) { | ||
await setAuthTokens(newTokens) | ||
return newTokens.accessToken | ||
} else if (typeof newTokens === 'string') { | ||
await setAccessToken(newTokens) | ||
return newTokens | ||
} | ||
throw new Error('requestRefresh must either return a string or an object with an accessToken') | ||
} catch (error) { | ||
// Failed to refresh token | ||
if (axios.isAxiosError(error)) { | ||
const status = error.response?.status | ||
if (status === 401 || status === 422) { | ||
// The refresh token is invalid so remove the stored tokens | ||
Storage.removeItem(STORAGE_KEY) | ||
throw new Error(`Got ${status} on token refresh; clearing both auth tokens`) | ||
} | ||
} | ||
// A different error, probably network error | ||
if (error instanceof Error) { | ||
throw new Error(`Failed to refresh auth token: ${error.message}`) | ||
} else { | ||
throw new Error('Failed to refresh auth token and failed to parse error') | ||
} | ||
} finally { | ||
isRefreshing = false | ||
} | ||
} | ||
export type TokenRefreshRequest = (refreshToken: Token) => Promise<Token | IAuthTokens> | ||
export interface IAuthTokenInterceptorConfig { | ||
header?: string | ||
headerPrefix?: string | ||
requestRefresh: TokenRefreshRequest | ||
} | ||
/** | ||
* Function that returns an Axios Intercepter that: | ||
* - Applies that right auth header to requests | ||
* - Refreshes the access token when needed | ||
* - Puts subsequent requests in a queue and executes them in order after the access token has been refreshed. | ||
* | ||
* @param {IAuthTokenInterceptorConfig} config - Configuration for the interceptor | ||
* @returns {Promise} Promise that resolves in the supplied requestConfig | ||
*/ | ||
export const authTokenInterceptor = | ||
({ | ||
header = 'Authorization', | ||
headerPrefix = 'Bearer ', | ||
requestRefresh, | ||
}: IAuthTokenInterceptorConfig) => | ||
async (requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> => { | ||
// We need refresh token to do any authenticated requests | ||
if (!getRefreshToken()) return requestConfig | ||
// Queue the request if another refresh request is currently happening | ||
if (isRefreshing) { | ||
return new Promise((resolve, reject) => { | ||
queue.push({ resolve, reject }) | ||
}) | ||
.then((token) => { | ||
if (requestConfig.headers) { | ||
requestConfig.headers[header] = `${headerPrefix}${token}` | ||
} | ||
return requestConfig | ||
}) | ||
.catch(Promise.reject) | ||
} | ||
// Do refresh if needed | ||
let accessToken | ||
try { | ||
accessToken = await refreshTokenIfNeeded(requestRefresh) | ||
resolveQueue(accessToken) | ||
} catch (error: unknown) { | ||
if (error instanceof Error) { | ||
declineQueue(error) | ||
throw new Error( | ||
`Unable to refresh access token for request due to token refresh error: ${error.message}` | ||
) | ||
} | ||
} | ||
// add token to headers | ||
if (accessToken && requestConfig.headers) { | ||
requestConfig.headers[header] = `${headerPrefix}${accessToken}` | ||
} | ||
return requestConfig | ||
} | ||
type RequestsQueue = { | ||
resolve: (value?: unknown) => void | ||
reject: (reason?: unknown) => void | ||
}[] | ||
let isRefreshing = false | ||
let queue: RequestsQueue = [] | ||
/** | ||
* Function that resolves all items in the queue with the provided token | ||
* @param token New access token | ||
*/ | ||
const resolveQueue = (token?: Token) => { | ||
queue.forEach((p) => { | ||
p.resolve(token) | ||
}) | ||
queue = [] | ||
} | ||
/** | ||
* Function that declines all items in the queue with the provided error | ||
* @param error Error | ||
*/ | ||
const declineQueue = (error: Error) => { | ||
queue.forEach((p) => { | ||
p.reject(error) | ||
}) | ||
queue = [] | ||
} | ||
export * from './tokensUtils' | ||
export * from './authTokenInterceptor' | ||
export * from './setAuthTokens' | ||
export * from './applyAuthTokenInterceptor' | ||
export * from './getBrowserSessionStorage' | ||
export * from './getBrowserLocalStorage' | ||
export * from './IAuthTokens' | ||
export * from './TokenRefreshRequest' | ||
export * from './setAuthTokens' | ||
export * from './StorageType' |
@@ -1,2 +0,2 @@ | ||
import axios from 'axios' | ||
import axios from 'axios'; | ||
import { applyAuthTokenInterceptor } from '../src' | ||
@@ -3,0 +3,0 @@ |
@@ -1,6 +0,5 @@ | ||
import { AxiosRequestConfig } from 'axios' | ||
import jwt from 'jsonwebtoken' | ||
import { AxiosRequestConfig } from 'axios'; | ||
import jwt from 'jsonwebtoken'; | ||
import { authTokenInterceptor } from '../src'; | ||
import { authTokenInterceptor } from '../src' | ||
describe('authTokenInterceptor', () => { | ||
@@ -7,0 +6,0 @@ it('returns the original request config if refresh token is not set', async () => { |
@@ -1,3 +0,5 @@ | ||
import { STORAGE_KEY, clearAuthTokens } from '../src' | ||
import { STORAGE_KEY } from '../src/StorageKey'; | ||
import { clearAuthTokens } from '../src'; | ||
describe('clearAuthTokens', () => { | ||
@@ -4,0 +6,0 @@ it('removes the tokens from localstorage', () => { |
@@ -1,32 +0,78 @@ | ||
import { STORAGE_KEY, getAccessToken } from '../src' | ||
import { getAccessToken, authTokenInterceptor, getBrowserSessionStorage } from '../src'; | ||
import { STORAGE_KEY } from '../src/StorageKey'; | ||
describe('getAccessToken', () => { | ||
it('returns undefined if tokens are not set', () => { | ||
// GIVEN | ||
// localStorage is empty | ||
localStorage.removeItem(STORAGE_KEY) | ||
beforeEach(function () { | ||
window.localStorage.clear() | ||
window.sessionStorage.clear() | ||
}) | ||
// WHEN | ||
// I call getAccessToken | ||
const result = getAccessToken() | ||
describe('for localStorage', function () { | ||
it('returns undefined if tokens are not set', () => { | ||
// GIVEN | ||
// localStorage is empty | ||
localStorage.removeItem(STORAGE_KEY) | ||
// THEN | ||
// I expect the result to be undefined | ||
expect(result).toEqual(undefined) | ||
}) | ||
// WHEN | ||
// I call getAccessToken | ||
const result = getAccessToken() | ||
it('returns the access token is it is set', () => { | ||
// GIVEN | ||
// Both tokens are stored in localstorage | ||
const tokens = { accessToken: 'accesstoken', refreshToken: 'refreshtoken' } | ||
localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens)) | ||
// THEN | ||
// I expect the result to be undefined | ||
expect(result).toEqual(undefined) | ||
}) | ||
// WHEN | ||
// I call getAccessToken | ||
const result = getAccessToken() | ||
it('returns the access token is it is set', () => { | ||
// GIVEN | ||
// Both tokens are stored in localstorage | ||
const tokens = { accessToken: 'accesstoken', refreshToken: 'refreshtoken' } | ||
localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens)) | ||
// THEN | ||
// I expect the result to be the supplied access token | ||
expect(result).toEqual('accesstoken') | ||
// WHEN | ||
// I call getAccessToken | ||
const result = getAccessToken() | ||
// THEN | ||
// I expect the result to be the supplied access token | ||
expect(result).toEqual('accesstoken') | ||
}) | ||
}); | ||
describe('for sessionStorage', function () { | ||
beforeEach( () => { | ||
const getStorage = getBrowserSessionStorage | ||
const requestRefresh = jest.fn() | ||
authTokenInterceptor({getStorage, requestRefresh }) | ||
}) | ||
it('returns undefined if tokens are not set', () => { | ||
// GIVEN | ||
// localStorage is empty | ||
sessionStorage.removeItem(STORAGE_KEY) | ||
// WHEN | ||
// I call getAccessToken | ||
const result = getAccessToken() | ||
// THEN | ||
// I expect the result to be undefined | ||
expect(result).toEqual(undefined) | ||
}) | ||
it('returns the access token is it is set', () => { | ||
// GIVEN | ||
// Both tokens are stored in localstorage | ||
const tokens = { accessToken: 'accesstoken_session', refreshToken: 'refreshtoken_session' } | ||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(tokens)) | ||
// WHEN | ||
// I call getAccessToken | ||
const result = getAccessToken() | ||
// THEN | ||
// I expect the result to be the supplied access token | ||
expect(result).toEqual('accesstoken_session') | ||
}) | ||
}) | ||
}) |
@@ -1,2 +0,3 @@ | ||
import { STORAGE_KEY, getRefreshToken } from '../src' | ||
import { getRefreshToken } from '../src'; | ||
import { STORAGE_KEY } from '../src/StorageKey'; | ||
@@ -3,0 +4,0 @@ describe('getRefreshToken', () => { |
@@ -1,2 +0,3 @@ | ||
import { STORAGE_KEY, isLoggedIn } from '../src' | ||
import { STORAGE_KEY } from '../src/StorageKey'; | ||
import { isLoggedIn } from '../src'; | ||
@@ -3,0 +4,0 @@ describe('isLoggedIn', () => { |
@@ -1,4 +0,5 @@ | ||
import { STORAGE_KEY, refreshTokenIfNeeded } from '../src' | ||
import jwt from 'jsonwebtoken' | ||
import { refreshTokenIfNeeded } from '../src'; | ||
import jwt from 'jsonwebtoken'; | ||
import { AxiosError } from 'axios' | ||
import { STORAGE_KEY } from '../src/StorageKey'; | ||
@@ -5,0 +6,0 @@ function makeAxiosErrorWithStatusCode(statusCode: number) { |
@@ -1,2 +0,3 @@ | ||
import { STORAGE_KEY, setAccessToken } from '../src' | ||
import { setAccessToken } from '../src'; | ||
import { STORAGE_KEY } from '../src/StorageKey'; | ||
@@ -3,0 +4,0 @@ describe('setAccessToken', () => { |
@@ -1,2 +0,3 @@ | ||
import { STORAGE_KEY, setAuthTokens } from '../src' | ||
import { setAuthTokens } from '../src'; | ||
import { STORAGE_KEY } from '../src/StorageKey'; | ||
@@ -3,0 +4,0 @@ describe('setAuthTokens', () => { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
118420
127
2111
173