From 35bbf068a615fd3865738269c54adb7accd05c0f Mon Sep 17 00:00:00 2001 From: Thomas Tracy Date: Wed, 16 Sep 2020 15:12:07 -0400 Subject: [PATCH] Add CSRF code to demographics modal (#24998) * Add CSRF tokens to demographics modal PATCH We have temporarilly copied over the CSRF code from frontend-platform to use with the demographics modal. This code is most likely temporary and is not maintained like frontend-platform. --- .../DemographicsCollectionModal.jsx | 8 ++- .../js/jwt_auth/AxiosCsrfTokenService.js | 64 +++++++++++++++++++ themes/edx.org/lms/templates/dashboard.html | 1 + 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 lms/static/js/jwt_auth/AxiosCsrfTokenService.js diff --git a/lms/static/js/demographics_collection/DemographicsCollectionModal.jsx b/lms/static/js/demographics_collection/DemographicsCollectionModal.jsx index eba65a77ae..fac3f2804e 100644 --- a/lms/static/js/demographics_collection/DemographicsCollectionModal.jsx +++ b/lms/static/js/demographics_collection/DemographicsCollectionModal.jsx @@ -7,6 +7,7 @@ import { SelectWithInput } from './SelectWithInput' import { MultiselectDropdown } from './MultiselectDropdown'; import AxiosJwtTokenService from '../jwt_auth/AxiosJwtTokenService'; import StringUtils from 'edx-ui-toolkit/js/utils/string-utils'; +import AxiosCsrfTokenService from '../jwt_auth/AxiosCsrfTokenService'; const FIELD_NAMES = { CURRENT_WORK: "current_work_sector", @@ -51,6 +52,7 @@ class DemographicsCollectionModal extends React.Component { accessToken, refreshUrl, ); + this.csrfTokenService = new AxiosCsrfTokenService(this.props.csrfTokenPath) } async componentDidMount() { @@ -59,7 +61,6 @@ class DemographicsCollectionModal extends React.Component { credentials: 'include', headers: { 'Content-Type': 'application/json', - 'X-CSRFTOKEN': Cookies.get('demographics_csrftoken'), 'USE-JWT-COOKIE': true }, }; @@ -135,6 +136,7 @@ class DemographicsCollectionModal extends React.Component { } async handleSelectChange(e) { + const url = `${this.props.demographicsBaseUrl}/demographics/api/v1/demographics/${this.props.user}/`; const name = e.target.name; const value = e.target.value; const options = { @@ -142,7 +144,6 @@ class DemographicsCollectionModal extends React.Component { credentials: 'include', headers: { 'Content-Type': 'application/json', - 'X-CSRFTOKEN': Cookies.get('demographics_csrftoken'), 'USE-JWT-COOKIE': true }, body: JSON.stringify({ @@ -152,7 +153,8 @@ class DemographicsCollectionModal extends React.Component { try { await this.jwtTokenService.getJwtToken(); - await fetch(`${this.props.demographicsBaseUrl}/demographics/api/v1/demographics/${this.props.user}/`, options) + options.headers['X-CSRFToken'] = await this.csrfTokenService.getCsrfToken(url); + await fetch(url, options) } catch (error) { this.setState({ loading: false, fieldError: true, errorMessage: error }); } diff --git a/lms/static/js/jwt_auth/AxiosCsrfTokenService.js b/lms/static/js/jwt_auth/AxiosCsrfTokenService.js new file mode 100644 index 0000000000..1e83b519a2 --- /dev/null +++ b/lms/static/js/jwt_auth/AxiosCsrfTokenService.js @@ -0,0 +1,64 @@ +/** + * Service class to support CSRF. + * + * Temporarily copied from the edx/frontend-platform + */ +import axios from 'axios'; +import { getUrlParts, processAxiosErrorAndThrow } from './utils'; + +export default class AxiosCsrfTokenService { + constructor(csrfTokenApiPath) { + this.csrfTokenApiPath = csrfTokenApiPath; + 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; + this.httpClient.defaults.headers.common['USE-JWT-COOKIE'] = true; + + this.csrfTokenCache = {}; + this.csrfTokenRequestPromises = {}; + } + + async getCsrfToken(url) { + let urlParts; + try { + urlParts = getUrlParts(url); + } catch (e) { + // If the url is not parsable it's likely because a relative + // path was supplied as the url. This is acceptable and in + // this case we should use the current origin of the page. + urlParts = getUrlParts(global.location.origin); + } + const { protocol, domain } = urlParts; + const csrfToken = this.csrfTokenCache[domain]; + + if (csrfToken) { + return csrfToken; + } + + if (!this.csrfTokenRequestPromises[domain]) { + this.csrfTokenRequestPromises[domain] = this.httpClient + .get(`${protocol}://${domain}${this.csrfTokenApiPath}`) + .then((response) => { + this.csrfTokenCache[domain] = response.data.csrfToken; + return this.csrfTokenCache[domain]; + }) + .catch(processAxiosErrorAndThrow) + .finally(() => { + delete this.csrfTokenRequestPromises[domain]; + }); + } + + return this.csrfTokenRequestPromises[domain]; + } + + clearCsrfTokenCache() { + this.csrfTokenCache = {}; + } + + getHttpClient() { + return this.httpClient; + } +} diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html index 5bc6628408..485da327d9 100644 --- a/themes/edx.org/lms/templates/dashboard.html +++ b/themes/edx.org/lms/templates/dashboard.html @@ -212,6 +212,7 @@ from student.models import CourseEnrollment "demographicsBaseUrl": getattr(settings, 'DEMOGRAPHICS_BASE_URL', ''), "marketingSiteBaseUrl": getattr(settings, 'MKTG_URLS', {}).get('ROOT', ''), "jwtAuthToken": getattr(settings, 'JWT_AUTH', {}).get('JWT_AUTH_COOKIE_HEADER_PAYLOAD', ''), + "csrfTokenPath": getattr(settings, 'DEMOGRAPHICS_CSRF_TOKEN_API_PATH', ''), "bannerLogo": bannerLogoPath }, )}