diff --git a/backend/benefit/helsinkibenefit/settings.py b/backend/benefit/helsinkibenefit/settings.py index bb927ad891..b9c5a95b5c 100644 --- a/backend/benefit/helsinkibenefit/settings.py +++ b/backend/benefit/helsinkibenefit/settings.py @@ -292,6 +292,7 @@ CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS") CSRF_COOKIE_NAME = env.str("CSRF_COOKIE_NAME") CSRF_COOKIE_SECURE = True +CSRF_USE_SESSIONS = True # Audit logging AUDIT_LOG_ORIGIN = env.str("AUDIT_LOG_ORIGIN") diff --git a/backend/benefit/helsinkibenefit/urls.py b/backend/benefit/helsinkibenefit/urls.py index 8ad96bff1c..c6751f9790 100644 --- a/backend/benefit/helsinkibenefit/urls.py +++ b/backend/benefit/helsinkibenefit/urls.py @@ -69,7 +69,7 @@ path("v1/company/", GetUsersOrganizationView.as_view()), path("v1/company/search//", SearchOrganisationsView.as_view()), path("v1/company/get//", GetOrganisationByIdView.as_view()), - path("v1/users/me/", CurrentUserView.as_view()), + path("v1/users/me/", CurrentUserView.as_view(), name="users-me"), path("v1/users/options/", UserOptionsView.as_view()), path( "v1/handlerapplications//review/", ReviewStateView.as_view() diff --git a/backend/benefit/users/api/v1/views.py b/backend/benefit/users/api/v1/views.py index 6b23edd1c7..d205a477d3 100644 --- a/backend/benefit/users/api/v1/views.py +++ b/backend/benefit/users/api/v1/views.py @@ -3,6 +3,7 @@ from django.contrib.auth.base_user import AbstractBaseUser from django.core.exceptions import PermissionDenied from django.db import DatabaseError, transaction +from django.middleware.csrf import get_token from django.shortcuts import get_object_or_404 from drf_spectacular.utils import extend_schema from helsinki_gdpr.views import DeletionNotAllowed, DryRunException, GDPRAPIView @@ -30,7 +31,9 @@ def get(self, request): serializer = UserSerializer( self._get_current_user(request), context={"request": request} ) - return Response(serializer.data) + response = serializer.data + response["csrf_token"] = get_token(request) + return Response(response) def _get_current_user(self, request): if not request.user.is_authenticated: diff --git a/backend/benefit/users/tests/conftest.py b/backend/benefit/users/tests/conftest.py index cf687ed857..7b11e94c3a 100644 --- a/backend/benefit/users/tests/conftest.py +++ b/backend/benefit/users/tests/conftest.py @@ -1,10 +1,23 @@ +import factory import pytest from rest_framework.test import APIClient +from applications.tests.factories import ApplicationFactory from common.tests.conftest import * # noqa +from companies.tests.conftest import * # noqa from helsinkibenefit.tests.conftest import * # noqa @pytest.fixture def gdpr_api_client(): return APIClient() + + +@pytest.fixture +def application(mock_get_organisation_roles_and_create_company): + # Application which belongs to logged in user company + with factory.Faker.override_default_locale("fi_FI"): + app = ApplicationFactory() + app.company = mock_get_organisation_roles_and_create_company + app.save() + return app diff --git a/backend/benefit/users/tests/test_user_api.py b/backend/benefit/users/tests/test_user_api.py new file mode 100644 index 0000000000..87e381f526 --- /dev/null +++ b/backend/benefit/users/tests/test_user_api.py @@ -0,0 +1,11 @@ +from rest_framework.reverse import reverse + +from common.tests.conftest import get_client_user + + +def test_applications_unauthorized(api_client, application): + response = api_client.get(reverse("users-me")) + user = get_client_user(api_client) + assert response.status_code == 200 + assert response.data["id"] == str(user.id) + assert len(response.data["csrf_token"]) > 0 diff --git a/frontend/benefit/applicant/src/constants.ts b/frontend/benefit/applicant/src/constants.ts index ad14473e56..99fbb5e75e 100644 --- a/frontend/benefit/applicant/src/constants.ts +++ b/frontend/benefit/applicant/src/constants.ts @@ -113,4 +113,5 @@ export const SUBMITTED_STATUSES = [ export enum LOCAL_STORAGE_KEYS { IS_TERMS_OF_SERVICE_APPROVED = 'isTermsOfServiceApproved', + CSRF_TOKEN = 'csrfToken', } diff --git a/frontend/benefit/applicant/src/hooks/useUserQuery.ts b/frontend/benefit/applicant/src/hooks/useUserQuery.ts index ace1549ef8..c3958671ca 100644 --- a/frontend/benefit/applicant/src/hooks/useUserQuery.ts +++ b/frontend/benefit/applicant/src/hooks/useUserQuery.ts @@ -7,6 +7,7 @@ import { useQuery, UseQueryResult } from 'react-query'; import showErrorToast from 'shared/components/toast/show-error-toast'; import useBackendAPI from 'shared/hooks/useBackendAPI'; import useLocale from 'shared/hooks/useLocale'; +import { setLocalStorageItem } from 'shared/utils/localstorage.utils'; import { LOCAL_STORAGE_KEYS } from '../constants'; @@ -51,9 +52,9 @@ const useUserQuery = ( select: (data) => camelcaseKeys(data, { deep: true }), onError: (error) => handleError(error), onSuccess: (data) => { + setLocalStorageItem(LOCAL_STORAGE_KEYS.CSRF_TOKEN, data.csrfToken); if (data.id && data.termsOfServiceApprovalNeeded) - // eslint-disable-next-line scanjs-rules/identifier_localStorage - localStorage.setItem( + setLocalStorageItem( LOCAL_STORAGE_KEYS.IS_TERMS_OF_SERVICE_APPROVED, 'false' ); diff --git a/frontend/benefit/applicant/src/pages/_app.tsx b/frontend/benefit/applicant/src/pages/_app.tsx index 32aa79de28..e0be75f92d 100644 --- a/frontend/benefit/applicant/src/pages/_app.tsx +++ b/frontend/benefit/applicant/src/pages/_app.tsx @@ -56,6 +56,7 @@ const App: React.FC = (appProps) => { diff --git a/frontend/benefit/applicant/src/pages/login.tsx b/frontend/benefit/applicant/src/pages/login.tsx index d86b1e0012..9be28bea44 100644 --- a/frontend/benefit/applicant/src/pages/login.tsx +++ b/frontend/benefit/applicant/src/pages/login.tsx @@ -23,8 +23,9 @@ import { $GridCell, } from 'shared/components/forms/section/FormSection.sc'; import getServerSideTranslations from 'shared/i18n/get-server-side-translations'; +import { removeLocalStorageItem } from 'shared/utils/localstorage.utils'; -import { IS_CLIENT, LOCAL_STORAGE_KEYS } from '../constants'; +import { LOCAL_STORAGE_KEYS } from '../constants'; type NotificationProps = | (Pick & { @@ -60,9 +61,8 @@ const Login: NextPage = () => { }, [logout, queryClient]); useEffect(() => { - if (IS_CLIENT) - // eslint-disable-next-line scanjs-rules/identifier_localStorage - localStorage.removeItem(LOCAL_STORAGE_KEYS.IS_TERMS_OF_SERVICE_APPROVED); + removeLocalStorageItem(LOCAL_STORAGE_KEYS.IS_TERMS_OF_SERVICE_APPROVED); + removeLocalStorageItem(LOCAL_STORAGE_KEYS.CSRF_TOKEN); }, []); return ( diff --git a/frontend/benefit/handler/src/constants.ts b/frontend/benefit/handler/src/constants.ts index 22a953d207..6e426fd991 100644 --- a/frontend/benefit/handler/src/constants.ts +++ b/frontend/benefit/handler/src/constants.ts @@ -193,3 +193,7 @@ export const ALL_APPLICATION_STATUSES: APPLICATION_STATUSES[] = [ APPLICATION_STATUSES.DRAFT, APPLICATION_STATUSES.REJECTED, ]; + +export enum LOCAL_STORAGE_KEYS { + CSRF_TOKEN = 'csrfToken', +} diff --git a/frontend/benefit/handler/src/hooks/useUserQuery.ts b/frontend/benefit/handler/src/hooks/useUserQuery.ts index fa6e947743..644b942720 100644 --- a/frontend/benefit/handler/src/hooks/useUserQuery.ts +++ b/frontend/benefit/handler/src/hooks/useUserQuery.ts @@ -5,8 +5,9 @@ import { useQuery, UseQueryResult } from 'react-query'; import useBackendAPI from 'shared/hooks/useBackendAPI'; import useLocale from 'shared/hooks/useLocale'; import User from 'shared/types/user'; +import { setLocalStorageItem } from 'shared/utils/localstorage.utils'; -import { ROUTES } from '../constants'; +import { LOCAL_STORAGE_KEYS, ROUTES } from '../constants'; import useLogout from './useLogout'; // check that authentication is still alive in every 5 minutes @@ -46,6 +47,11 @@ const useUserQuery = ( } }; + const onSuccessHandler = (user: User): void => { + checkForStaffStatus(user); + setLocalStorageItem(LOCAL_STORAGE_KEYS.CSRF_TOKEN, user.csrf_token); + }; + return useQuery( `${BackendEndpoint.USER_ME}`, () => handleResponse(axios.get(BackendEndpoint.USER_ME)), @@ -54,7 +60,7 @@ const useUserQuery = ( enabled: !logout, retry: false, select, - onSuccess: checkForStaffStatus, + onSuccess: onSuccessHandler, onError: (error) => handleError(error), } ); diff --git a/frontend/benefit/handler/src/pages/_app.tsx b/frontend/benefit/handler/src/pages/_app.tsx index e38c8213db..d3f9716669 100644 --- a/frontend/benefit/handler/src/pages/_app.tsx +++ b/frontend/benefit/handler/src/pages/_app.tsx @@ -33,6 +33,7 @@ const App: React.FC = (appProps) => { diff --git a/frontend/benefit/handler/src/pages/login.tsx b/frontend/benefit/handler/src/pages/login.tsx index 2ad5971a0d..684ac33a34 100644 --- a/frontend/benefit/handler/src/pages/login.tsx +++ b/frontend/benefit/handler/src/pages/login.tsx @@ -12,8 +12,11 @@ import React, { useEffect } from 'react'; import { useQueryClient } from 'react-query'; import Container from 'shared/components/container/Container'; import getServerSideTranslations from 'shared/i18n/get-server-side-translations'; +import { removeLocalStorageItem } from 'shared/utils/localstorage.utils'; import { useTheme } from 'styled-components'; +import { LOCAL_STORAGE_KEYS } from '../constants'; + type NotificationProps = Pick & { content?: string; }; @@ -61,6 +64,10 @@ const Login: NextPage = () => { } }, [router.query.logout, router.query.userStateError, queryClient]); + useEffect(() => { + removeLocalStorageItem(LOCAL_STORAGE_KEYS.CSRF_TOKEN); + }, []); + return ( = ({ baseURL, headers, + isLocalStorageCsrf = false, children, }): JSX.Element => ( = ({ baseURL, headers: { 'Content-Type': 'application/json', - 'X-CSRFToken': getLastCookieValue('yjdhcsrftoken'), + 'X-CSRFToken': isLocalStorageCsrf + ? getLocalStorageItem('csrfToken') + : getLastCookieValue('yjdhcsrftoken'), ...headers, }, withCredentials: true, diff --git a/frontend/shared/src/types/user.d.ts b/frontend/shared/src/types/user.d.ts index 3b331ada1b..51b05ee2d9 100644 --- a/frontend/shared/src/types/user.d.ts +++ b/frontend/shared/src/types/user.d.ts @@ -6,5 +6,6 @@ type User = { name: string; organization_name?: string; is_staff?: boolean; + csrf_token?: string; }; export default User; diff --git a/frontend/shared/src/utils/localstorage.utils.ts b/frontend/shared/src/utils/localstorage.utils.ts new file mode 100644 index 0000000000..4860b42562 --- /dev/null +++ b/frontend/shared/src/utils/localstorage.utils.ts @@ -0,0 +1,14 @@ +/* eslint-disable scanjs-rules/identifier_localStorage */ + +const IS_CLIENT = typeof window !== 'undefined'; + +export const getLocalStorageItem = (key: string): string => + IS_CLIENT ? localStorage.getItem(key) || '' : ''; + +export const setLocalStorageItem = (key: string, value: string): void => + IS_CLIENT && localStorage.setItem(key, value); + +export const removeLocalStorageItem = (key: string): void => + IS_CLIENT && localStorage.removeItem(key); + +/* eslint-enable scanjs-rules/identifier_localStorage */