* fix: eslint operator-linebreak issue * fix: eslint quotes issue * fix: react jsx indent and props issues * fix: eslint trailing spaces issues * fix: eslint line around directives issue * fix: eslint semi rule * fix: eslint newline per chain rule * fix: eslint space infix ops rule * fix: eslint space-in-parens issue * fix: eslint space before function paren issue * fix: eslint space before blocks issue * fix: eslint arrow body style issue * fix: eslint dot-location issue * fix: eslint quotes issue * fix: eslint quote props issue * fix: eslint operator assignment issue * fix: eslint new line after import issue * fix: indent issues * fix: operator assignment issue * fix: all autofixable eslint issues * fix: all react related fixable issues * fix: autofixable eslint issues * chore: remove all template literals * fix: remaining autofixable issues * chore: apply amnesty on all existing issues * fix: failing xss-lint issues * refactor: apply amnesty on remaining issues * refactor: apply amnesty on new issues * fix: remove file level suppressions * refactor: apply amnesty on new issues
127 lines
4.5 KiB
JavaScript
127 lines
4.5 KiB
JavaScript
/**
|
|
* Service class to support JWT Token Authentication.
|
|
*
|
|
* Temporarily copied from the edx/frontend-platform
|
|
*/
|
|
import Cookies from 'universal-cookie';
|
|
import jwtDecode from 'jwt-decode';
|
|
import axios from 'axios';
|
|
import createRetryInterceptor from './interceptors/createRetryInterceptor';
|
|
import { processAxiosErrorAndThrow } from './utils';
|
|
|
|
export default class AxiosJwtTokenService {
|
|
static isTokenExpired(token) {
|
|
return !token || token.exp < Date.now() / 1000;
|
|
}
|
|
|
|
constructor(tokenCookieName, tokenRefreshEndpoint) {
|
|
this.tokenCookieName = tokenCookieName;
|
|
this.tokenRefreshEndpoint = tokenRefreshEndpoint;
|
|
|
|
this.httpClient = axios.create();
|
|
// Set withCredentials to true. Enables cross-site Access-Control requests
|
|
// to be made using cookies, authorization headers or TLS client
|
|
// certificates. More on MDN:
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
|
|
this.httpClient.defaults.withCredentials = true;
|
|
// Add retries to this axios instance
|
|
this.httpClient.interceptors.response.use(
|
|
response => response,
|
|
createRetryInterceptor({ httpClient: this.httpClient }),
|
|
);
|
|
|
|
this.cookies = new Cookies();
|
|
this.refreshRequestPromises = {};
|
|
}
|
|
|
|
getHttpClient() {
|
|
return this.httpClient;
|
|
}
|
|
|
|
decodeJwtCookie() {
|
|
const cookieValue = this.cookies.get(this.tokenCookieName);
|
|
|
|
if (cookieValue) {
|
|
try {
|
|
return jwtDecode(cookieValue);
|
|
} catch (e) {
|
|
const error = Object.create(e);
|
|
error.message = 'Error decoding JWT token';
|
|
error.customAttributes = { cookieValue };
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
refresh() {
|
|
if (this.refreshRequestPromises[this.tokenCookieName] === undefined) {
|
|
const makeRefreshRequest = async () => {
|
|
let axiosResponse;
|
|
try {
|
|
try {
|
|
axiosResponse = await this.httpClient.post(this.tokenRefreshEndpoint);
|
|
} catch (error) {
|
|
processAxiosErrorAndThrow(error);
|
|
}
|
|
} catch (error) {
|
|
const userIsUnauthenticated = error.response && error.response.status === 401;
|
|
if (userIsUnauthenticated) {
|
|
// Clean up the cookie if it exists to eliminate any situation
|
|
// where the cookie is not expired but the jwt is expired.
|
|
this.cookies.remove(this.tokenCookieName);
|
|
const decodedJwtToken = null;
|
|
return decodedJwtToken;
|
|
}
|
|
|
|
// TODO: Network timeouts and other problems will end up in
|
|
// this block of code. We could add logic for retrying token
|
|
// refreshes if we wanted to.
|
|
throw error;
|
|
}
|
|
|
|
const decodedJwtToken = this.decodeJwtCookie();
|
|
|
|
if (!decodedJwtToken) {
|
|
// This is an unexpected case. The refresh endpoint should
|
|
// set the cookie that is needed. See ARCH-948 for more
|
|
// information on a similar situation that was happening
|
|
// prior to this refactor in Oct 2019.
|
|
const error = new Error('Access token is still null after successful refresh.');
|
|
error.customAttributes = { axiosResponse };
|
|
throw error;
|
|
}
|
|
|
|
return decodedJwtToken;
|
|
};
|
|
|
|
this.refreshRequestPromises[this.tokenCookieName] = makeRefreshRequest().finally(() => {
|
|
delete this.refreshRequestPromises[this.tokenCookieName];
|
|
});
|
|
}
|
|
|
|
return this.refreshRequestPromises[this.tokenCookieName];
|
|
}
|
|
|
|
async getJwtToken() {
|
|
// eslint-disable-next-line no-useless-catch
|
|
try {
|
|
const decodedJwtToken = this.decodeJwtCookie(this.tokenCookieName);
|
|
if (!AxiosJwtTokenService.isTokenExpired(decodedJwtToken)) {
|
|
return decodedJwtToken;
|
|
}
|
|
} catch (e) {
|
|
// Log unexpected error and continue with attempt to refresh it.
|
|
throw e;
|
|
}
|
|
|
|
// eslint-disable-next-line no-useless-catch
|
|
try {
|
|
return await this.refresh();
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|