Comparing version 1.8.0 to 2.0.0
@@ -43,6 +43,8 @@ "use strict"; | ||
exports.authTokenInterceptor = exports.useAuthTokenInterceptor = exports.applyAuthTokenInterceptor = exports.refreshTokenIfNeeded = exports.getAccessToken = exports.getRefreshToken = exports.clearAuthTokens = exports.setAccessToken = exports.setAuthTokens = exports.isLoggedIn = exports.STORAGE_KEY = void 0; | ||
var axios_1 = __importDefault(require("axios")); | ||
var jwt_decode_1 = __importDefault(require("jwt-decode")); | ||
var storage_1 = __importDefault(require("./storage")); | ||
// a little time before expiration to try refresh (seconds) | ||
var EXPIRE_FUDGE = 10; | ||
exports.STORAGE_KEY = "auth-tokens-" + process.env.NODE_ENV; | ||
exports.STORAGE_KEY = "auth-tokens-".concat(process.env.NODE_ENV); | ||
// EXPORTS | ||
@@ -54,3 +56,3 @@ /** | ||
var isLoggedIn = function () { | ||
var token = exports.getRefreshToken(); | ||
var token = (0, exports.getRefreshToken)(); | ||
return !!token; | ||
@@ -63,3 +65,5 @@ }; | ||
*/ | ||
var setAuthTokens = function (tokens) { return localStorage.setItem(exports.STORAGE_KEY, JSON.stringify(tokens)); }; | ||
var setAuthTokens = function (tokens) { | ||
return storage_1.default.setItem(exports.STORAGE_KEY, JSON.stringify(tokens)); | ||
}; | ||
exports.setAuthTokens = setAuthTokens; | ||
@@ -76,3 +80,3 @@ /** | ||
tokens.accessToken = token; | ||
exports.setAuthTokens(tokens); | ||
(0, exports.setAuthTokens)(tokens); | ||
}; | ||
@@ -83,3 +87,3 @@ exports.setAccessToken = setAccessToken; | ||
*/ | ||
var clearAuthTokens = function () { return localStorage.removeItem(exports.STORAGE_KEY); }; | ||
var clearAuthTokens = function () { return storage_1.default.removeItem(exports.STORAGE_KEY); }; | ||
exports.clearAuthTokens = clearAuthTokens; | ||
@@ -119,3 +123,3 @@ /** | ||
case 0: | ||
accessToken = exports.getAccessToken(); | ||
accessToken = (0, exports.getAccessToken)(); | ||
if (!(!accessToken || isTokenExpired(accessToken))) return [3 /*break*/, 2]; | ||
@@ -139,4 +143,4 @@ return [4 /*yield*/, refreshToken(requestRefresh)]; | ||
if (!axios.interceptors) | ||
throw new Error("invalid axios instance: " + axios); | ||
axios.interceptors.request.use(exports.authTokenInterceptor(config)); | ||
throw new Error("invalid axios instance: ".concat(axios)); | ||
axios.interceptors.request.use((0, exports.authTokenInterceptor)(config)); | ||
}; | ||
@@ -154,3 +158,3 @@ exports.applyAuthTokenInterceptor = applyAuthTokenInterceptor; | ||
var getAuthTokens = function () { | ||
var rawTokens = localStorage.getItem(exports.STORAGE_KEY); | ||
var rawTokens = storage_1.default.getItem(exports.STORAGE_KEY); | ||
if (!rawTokens) | ||
@@ -164,3 +168,3 @@ return; | ||
if (error instanceof SyntaxError) { | ||
error.message = "Failed to parse auth tokens: " + rawTokens; | ||
error.message = "Failed to parse auth tokens: ".concat(rawTokens); | ||
throw error; | ||
@@ -189,4 +193,4 @@ } | ||
var getTimestampFromToken = function (token) { | ||
var decoded = jwt_decode_1.default(token); | ||
return decoded === null || decoded === void 0 ? void 0 : decoded.exp; | ||
var decoded = (0, jwt_decode_1.default)(token); | ||
return decoded.exp; | ||
}; | ||
@@ -217,3 +221,3 @@ /** | ||
case 0: | ||
refreshToken = exports.getRefreshToken(); | ||
refreshToken = (0, exports.getRefreshToken)(); | ||
if (!refreshToken) | ||
@@ -229,3 +233,3 @@ throw new Error('No refresh token available'); | ||
if (!(typeof newTokens === 'object' && (newTokens === null || newTokens === void 0 ? void 0 : newTokens.accessToken))) return [3 /*break*/, 4]; | ||
return [4 /*yield*/, exports.setAuthTokens(newTokens)]; | ||
return [4 /*yield*/, (0, exports.setAuthTokens)(newTokens)]; | ||
case 3: | ||
@@ -236,3 +240,3 @@ _b.sent(); | ||
if (!(typeof newTokens === 'string')) return [3 /*break*/, 6]; | ||
return [4 /*yield*/, exports.setAccessToken(newTokens)]; | ||
return [4 /*yield*/, (0, exports.setAccessToken)(newTokens)]; | ||
case 5: | ||
@@ -244,11 +248,17 @@ _b.sent(); | ||
error_1 = _b.sent(); | ||
status_1 = (_a = error_1 === null || error_1 === void 0 ? void 0 : error_1.response) === null || _a === void 0 ? void 0 : _a.status; | ||
if (status_1 === 401 || status_1 === 422) { | ||
// The refresh token is invalid so remove the stored tokens | ||
localStorage.removeItem(exports.STORAGE_KEY); | ||
throw new Error("Got " + status_1 + " on token refresh; clearing both auth tokens"); | ||
// Failed to refresh token | ||
if (axios_1.default.isAxiosError(error_1)) { | ||
status_1 = (_a = error_1.response) === null || _a === void 0 ? void 0 : _a.status; | ||
if (status_1 === 401 || status_1 === 422) { | ||
// The refresh token is invalid so remove the stored tokens | ||
storage_1.default.removeItem(exports.STORAGE_KEY); | ||
throw new Error("Got ".concat(status_1, " on token refresh; clearing both auth tokens")); | ||
} | ||
} | ||
// A different error, probably network error | ||
if (error_1 instanceof Error) { | ||
throw new Error("Failed to refresh auth token: ".concat(error_1.message)); | ||
} | ||
else { | ||
// A different error, probably network error | ||
throw new Error("Failed to refresh auth token: " + error_1.message); | ||
throw new Error('Failed to refresh auth token and failed to parse error'); | ||
} | ||
@@ -280,3 +290,3 @@ return [3 /*break*/, 9]; | ||
// We need refresh token to do any authenticated requests | ||
if (!exports.getRefreshToken()) | ||
if (!(0, exports.getRefreshToken)()) | ||
return [2 /*return*/, requestConfig | ||
@@ -292,3 +302,3 @@ // Queue the request if another refresh request is currently happening | ||
if (requestConfig.headers) { | ||
requestConfig.headers[header] = "" + headerPrefix + token; | ||
requestConfig.headers[header] = "".concat(headerPrefix).concat(token); | ||
} | ||
@@ -302,3 +312,3 @@ return requestConfig; | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, exports.refreshTokenIfNeeded(requestRefresh)]; | ||
return [4 /*yield*/, (0, exports.refreshTokenIfNeeded)(requestRefresh)]; | ||
case 2: | ||
@@ -312,3 +322,3 @@ accessToken = _a.sent(); | ||
declineQueue(error_2); | ||
throw new Error("Unable to refresh access token for request due to token refresh error: " + error_2.message); | ||
throw new Error("Unable to refresh access token for request due to token refresh error: ".concat(error_2.message)); | ||
} | ||
@@ -318,4 +328,5 @@ return [3 /*break*/, 4]; | ||
// add token to headers | ||
if (accessToken && requestConfig.headers) | ||
requestConfig.headers[header] = "" + headerPrefix + accessToken; | ||
if (accessToken && requestConfig.headers) { | ||
requestConfig.headers[header] = "".concat(headerPrefix).concat(accessToken); | ||
} | ||
return [2 /*return*/, requestConfig]; | ||
@@ -322,0 +333,0 @@ } |
{ | ||
"name": "axios-jwt", | ||
"version": "1.8.0", | ||
"version": "2.0.0", | ||
"description": "Axios interceptor to store, use, and refresh tokens for authentication.", | ||
@@ -18,2 +18,3 @@ "main": "dist/index.js", | ||
"@types/jsonwebtoken": "^8.5.1", | ||
"@types/jwt-decode": "^3.1.0", | ||
"@typescript-eslint/eslint-plugin": "^4.21.0", | ||
@@ -26,5 +27,6 @@ "@typescript-eslint/parser": "^4.21.0", | ||
"ts-jest": "^26.5.4", | ||
"typescript": "^4.2.4" | ||
"typescript": "^4.6.4" | ||
}, | ||
"peerDependencies": { | ||
"@react-native-async-storage/async-storage": "1.15.17", | ||
"axios": "^0.27.0" | ||
@@ -31,0 +33,0 @@ }, |
# axios-jwt | ||
Store, clear, transmit and automatically refresh JWT authentication tokens. | ||
Store, clear, transmit and automatically refresh JWT authentication tokens. This library can be used in both web and react-native projects. | ||
@@ -10,3 +10,3 @@ ## What does it do? | ||
The interceptor automatically adds an access token header (default: `Authorization`) to all requests. | ||
It stores `accessToken` and `refreshToken` in `localStorage` and reads them when needed. | ||
It stores `accessToken` and `refreshToken` in `localStorage` (web) or 'AsyncStorage' (React Native) and reads them when needed. | ||
@@ -16,2 +16,27 @@ It parses the expiration time of your access token and checks to see if it is expired before every request. If it has expired, a request to | ||
## Installation instructions | ||
### Install axios-jwt | ||
```bash | ||
npm install --save axios-jwt # or `yarn add axios-jwt` | ||
``` | ||
### Additional steps for React Native projects | ||
You will also need to install react-native-async-storage in order to be able to store and retrieve tokens. | ||
#### Expo | ||
```bash | ||
expo install @react-native-async-storage/async-storage | ||
``` | ||
### React Native | ||
```bash | ||
npm install --save @react-native-async-storage/async-storage # or `yarn add @react-native-async-storage/async-storage` | ||
npx pod-install # installs the native iOS packages | ||
``` | ||
## How do I use it? | ||
@@ -78,3 +103,3 @@ | ||
// 5. Clear the auth tokens from localstorage | ||
// 5. Remove the auth tokens from storage | ||
const logout = () => clearAuthTokens() | ||
@@ -81,0 +106,0 @@ |
121
src/index.ts
@@ -1,3 +0,4 @@ | ||
import { AxiosInstance, AxiosRequestConfig } from 'axios' | ||
import jwtDecode from 'jwt-decode' | ||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' | ||
import jwtDecode, { JwtPayload } from 'jwt-decode' | ||
import Storage from './storage' | ||
@@ -29,3 +30,4 @@ // a little time before expiration to try refresh (seconds) | ||
*/ | ||
export const setAuthTokens = (tokens: IAuthTokens): void => localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens)) | ||
export const setAuthTokens = (tokens: IAuthTokens): void => | ||
Storage.setItem(STORAGE_KEY, JSON.stringify(tokens)) | ||
@@ -49,3 +51,3 @@ /** | ||
*/ | ||
export const clearAuthTokens = (): void => localStorage.removeItem(STORAGE_KEY) | ||
export const clearAuthTokens = (): void => Storage.removeItem(STORAGE_KEY) | ||
@@ -81,3 +83,5 @@ /** | ||
*/ | ||
export const refreshTokenIfNeeded = async (requestRefresh: TokenRefreshRequest): Promise<Token | undefined> => { | ||
export const refreshTokenIfNeeded = async ( | ||
requestRefresh: TokenRefreshRequest | ||
): Promise<Token | undefined> => { | ||
// use access token (if we have it) | ||
@@ -101,3 +105,6 @@ let accessToken = getAccessToken() | ||
*/ | ||
export const applyAuthTokenInterceptor = (axios: AxiosInstance, config: IAuthTokenInterceptorConfig): void => { | ||
export const applyAuthTokenInterceptor = ( | ||
axios: AxiosInstance, | ||
config: IAuthTokenInterceptorConfig | ||
): void => { | ||
if (!axios.interceptors) throw new Error(`invalid axios instance: ${axios}`) | ||
@@ -119,3 +126,3 @@ axios.interceptors.request.use(authTokenInterceptor(config)) | ||
const getAuthTokens = (): IAuthTokens | undefined => { | ||
const rawTokens = localStorage.getItem(STORAGE_KEY) | ||
const rawTokens = Storage.getItem(STORAGE_KEY) | ||
if (!rawTokens) return | ||
@@ -153,5 +160,5 @@ | ||
const getTimestampFromToken = (token: Token): number | undefined => { | ||
const decoded = jwtDecode<{ [key: string]: number }>(token) | ||
const decoded = jwtDecode<JwtPayload>(token) | ||
return decoded?.exp | ||
return decoded.exp | ||
} | ||
@@ -197,13 +204,18 @@ | ||
throw new Error('requestRefresh must either return a string or an object with an accessToken') | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} catch (error: any) { | ||
} catch (error) { | ||
// Failed to refresh token | ||
const status = error?.response?.status | ||
if (status === 401 || status === 422) { | ||
// The refresh token is invalid so remove the stored tokens | ||
localStorage.removeItem(STORAGE_KEY) | ||
throw new Error(`Got ${status} on token refresh; clearing both auth tokens`) | ||
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 { | ||
// A different error, probably network error | ||
throw new Error(`Failed to refresh auth token: ${error.message}`) | ||
throw new Error('Failed to refresh auth token and failed to parse error') | ||
} | ||
@@ -232,41 +244,48 @@ } finally { | ||
*/ | ||
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 | ||
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 | ||
// Queue the request if another refresh request is currently happening | ||
if (isRefreshing) { | ||
return new Promise((resolve, reject) => { | ||
queue.push({ resolve, reject }) | ||
}) | ||
.catch(Promise.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}`) | ||
// 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 | ||
} | ||
// add token to headers | ||
if (accessToken && requestConfig.headers) requestConfig.headers[header] = `${headerPrefix}${accessToken}` | ||
return requestConfig | ||
} | ||
type RequestsQueue = { | ||
@@ -273,0 +292,0 @@ resolve: (value?: unknown) => void |
import { STORAGE_KEY, refreshTokenIfNeeded } from '../src' | ||
import jwt from 'jsonwebtoken' | ||
import { AxiosError } from 'axios' | ||
function makeAxiosErrorWithStatusCode(statusCode: number) { | ||
const error = new AxiosError( | ||
'Server error', | ||
'ECONNABORTED', | ||
{}, | ||
{}, | ||
{ | ||
status: statusCode, | ||
data: {}, | ||
config: {}, | ||
headers: {}, | ||
statusText: '', | ||
} | ||
) | ||
return error | ||
} | ||
describe('refreshTokenIfNeeded', () => { | ||
@@ -54,6 +73,3 @@ it('throws an error if the requestRefresh function threw one', async () => { | ||
const requestRefresh = async () => { | ||
const error: any = new Error('Server error') | ||
error.response = { | ||
status: 401, | ||
} | ||
const error = makeAxiosErrorWithStatusCode(401) | ||
@@ -94,6 +110,3 @@ throw error | ||
const requestRefresh = async () => { | ||
const error: any = new Error('Server error') | ||
error.response = { | ||
status: 422, | ||
} | ||
const error = makeAxiosErrorWithStatusCode(422) | ||
@@ -100,0 +113,0 @@ throw error |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
69992
36
1361
167
3
12