From c6dfcec5956c38363fa79d25a67db85f0a749c81 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Thu, 7 Jul 2022 13:10:58 -0400 Subject: [PATCH 01/15] First pass at offline authentication --- .../src/api/local-forage-encrypted-wrapper.ts | 10 ++-- .../src/components/forms/LoginForm/index.tsx | 33 +++++++++--- .../src/hooks/api/useAuth/useLoginCallback.ts | 14 ++++- .../hooks/api/useAuth/useLogoutCallback.ts | 6 +-- packages/webapp/src/utils/localCrypto.ts | 53 +++++++++++++------ 5 files changed, 81 insertions(+), 35 deletions(-) diff --git a/packages/webapp/src/api/local-forage-encrypted-wrapper.ts b/packages/webapp/src/api/local-forage-encrypted-wrapper.ts index aa0cfc366..6f56d7178 100644 --- a/packages/webapp/src/api/local-forage-encrypted-wrapper.ts +++ b/packages/webapp/src/api/local-forage-encrypted-wrapper.ts @@ -5,7 +5,7 @@ import { LocalForageWrapper } from 'apollo3-cache-persist' import * as CryptoJS from 'crypto-js' import { currentUserStore } from '~utils/current-user-store' -import { checkSalt, setPwdHash, setCurrentUser, getCurrentUser } from '~utils/localCrypto' +import { checkSalt, setPwdHash, setCurrentUserId, getCurrentUserId } from '~utils/localCrypto' export class LocalForageWrapperEncrypted extends LocalForageWrapper { constructor( @@ -16,11 +16,11 @@ export class LocalForageWrapperEncrypted extends LocalForageWrapper { super(storage) checkSalt(user) setPwdHash(user, passwd) - setCurrentUser(user) + setCurrentUserId(user) } getItem(key: string): Promise { - const currentUid = getCurrentUser() + const currentUid = getCurrentUserId() return super.getItem(currentUid.concat('-', key)).then((item) => { if (item != null && item.length > 0) { return this.decrypt(item, currentUid) @@ -30,12 +30,12 @@ export class LocalForageWrapperEncrypted extends LocalForageWrapper { } removeItem(key: string): Promise { - const currentUid = getCurrentUser() + const currentUid = getCurrentUserId() return super.removeItem(currentUid.concat('-', key)) } setItem(key: string, value: string | object | null): Promise { - const currentUid = getCurrentUser() + const currentUid = getCurrentUserId() const secData = this.encrypt(value, currentUid) return super.setItem(currentUid.concat('-', key), secData) } diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index 887cea8df..4f7d9a93d 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -10,7 +10,10 @@ import { FormikField } from '~ui/FormikField' import { Formik, Form } from 'formik' import cx from 'classnames' import { useAuthUser } from '~hooks/api/useAuth' +import { useRecoilState } from 'recoil' import { useCallback, useState } from 'react' +import { currentUserState } from '~store' +import type { User } from '@cbosuite/schema/dist/client-types' import { Namespace, useTranslation } from '~hooks/useTranslation' import { FormSectionTitle } from '~components/ui/FormSectionTitle' import { wrap } from '~utils/appinsights' @@ -21,10 +24,12 @@ import { ApplicationRoute } from '~types/ApplicationRoute' import { clearUser, testPassword, - setCurrentUser, + setCurrentUserId, checkSalt, APOLLO_KEY, - setPwdHash + setPwdHash, + getAccessToken, + getUser } from '~utils/localCrypto' import { createLogger } from '~utils/createLogger' import localforage from 'localforage' @@ -32,6 +37,10 @@ import { config } from '~utils/config' import { useStore } from 'react-stores' import { currentUserStore } from '~utils/current-user-store' import * as CryptoJS from 'crypto-js' +import { StatusType } from '~hooks/api' +import { storeAccessToken } from '~utils/localStorage' +import { useOffline } from '~hooks/useOffline' + const logger = createLogger('authenticate') interface LoginFormProps { @@ -48,6 +57,8 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ const { t } = useTranslation(Namespace.Login) const { login } = useAuthUser() const [acceptedAgreement, setAcceptedAgreement] = useState(false) + const isOffline = useOffline() + const [, setCurrentUser] = useRecoilState(currentUserState) const handleLoginClick = useCallback( async (values) => { @@ -55,9 +66,9 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ if (isDurableCacheEnabled) { const onlineAuthStatus = resp.status === 'SUCCESS' - const offlineAuthStatus = testPassword(values.username, values.password) + const offlineAuthStatus = testPassword(values.username, values.password) && isOffline localUserStore.username = values.username - setCurrentUser(values.username) + setCurrentUserId(values.username) if (onlineAuthStatus && offlineAuthStatus) { localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex @@ -78,9 +89,15 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ localUserStore.sessionPassword = CryptoJS.SHA512(values.username).toString( CryptoJS.enc.Hex ) - logger( - 'Handle offline auth success: WIP/TBD, need to check offline status and data availability' - ) + + const userJsonString = getUser(values.username) + const user = JSON.parse(userJsonString) + setCurrentUser(user) + const accessToken = getAccessToken(values.username) + storeAccessToken(accessToken) + resp.status = StatusType.Success + + logger('Offline authentication successful') } else if (!onlineAuthStatus && !offlineAuthStatus) { logger('Handle offline login failure: WIP/TBD, limited retry?') } else { @@ -90,7 +107,7 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ onLoginClick(resp.status) }, - [login, onLoginClick, isDurableCacheEnabled, localUserStore] + [login, onLoginClick, isDurableCacheEnabled, localUserStore, isOffline, setCurrentUser] ) const handlePasswordResetClick = useNavCallback(ApplicationRoute.PasswordReset) diff --git a/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts b/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts index 4ff9115ff..068f5401c 100644 --- a/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts +++ b/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts @@ -13,10 +13,13 @@ import { currentUserState } from '~store' import { CurrentUserFields } from '../fragments' import { useToasts } from '~hooks/useToasts' import { useTranslation } from '~hooks/useTranslation' +import { useOffline } from '~hooks/useOffline' import type { MessageResponse } from '../types' import { useCallback } from 'react' import { storeAccessToken } from '~utils/localStorage' import { handleGraphqlResponse } from '~utils/handleGraphqlResponse' +import { StatusType } from '~hooks/api' +import { setUser, setAccessToken } from '~utils/localCrypto' const AUTHENTICATE_USER = gql` ${CurrentUserFields} @@ -37,21 +40,30 @@ export type BasicAuthCallback = (username: string, password: string) => Promise< export function useLoginCallback(): BasicAuthCallback { const { c } = useTranslation() const toast = useToasts() + const isOffline = useOffline() const [authenticate] = useMutation(AUTHENTICATE_USER) const [, setCurrentUser] = useRecoilState(currentUserState) return useCallback( async (username: string, password: string) => { + if (isOffline) { + return Promise.resolve({ + status: StatusType.Failed, + message: 'Application is offline, cannot authenticate' + }) + } return handleGraphqlResponse(authenticate({ variables: { username, password } }), { toast, failureToast: c('hooks.useAuth.loginFailed'), onSuccess: ({ authenticate }: { authenticate: AuthenticationResponse }) => { storeAccessToken(authenticate.accessToken) setCurrentUser(authenticate.user) + setUser(username, authenticate.user) + setAccessToken(username, authenticate.accessToken) return authenticate.message } }) }, - [c, toast, authenticate, setCurrentUser] + [c, toast, authenticate, setCurrentUser, isOffline] ) } diff --git a/packages/webapp/src/hooks/api/useAuth/useLogoutCallback.ts b/packages/webapp/src/hooks/api/useAuth/useLogoutCallback.ts index 431b41f67..32d01d9b0 100644 --- a/packages/webapp/src/hooks/api/useAuth/useLogoutCallback.ts +++ b/packages/webapp/src/hooks/api/useAuth/useLogoutCallback.ts @@ -13,7 +13,6 @@ import { selectedReportTypeState } from '~store' import { useCallback } from 'react' -import { useApolloClient } from '@apollo/client' export type LogoutCallback = () => void @@ -25,7 +24,6 @@ export function useLogoutCallback(): LogoutCallback { const resetCurrentUser = useResetRecoilState(currentUserState) const resetHiddenFields = useResetRecoilState(hiddenReportFieldsState) const resetReportType = useResetRecoilState(selectedReportTypeState) - const apolloClient = useApolloClient() return useCallback(() => { resetCurrentUser() @@ -35,7 +33,6 @@ export function useLogoutCallback(): LogoutCallback { resetInactiveEngagement() resetHiddenFields() resetReportType() - apolloClient.clearStore() // Reset the Apollo cache entirely }, [ resetEngagement, resetMyEngagement, @@ -43,7 +40,6 @@ export function useLogoutCallback(): LogoutCallback { resetCurrentUser, resetOrg, resetHiddenFields, - resetReportType, - apolloClient + resetReportType ]) } diff --git a/packages/webapp/src/utils/localCrypto.ts b/packages/webapp/src/utils/localCrypto.ts index 52ad4347c..6df95c995 100644 --- a/packages/webapp/src/utils/localCrypto.ts +++ b/packages/webapp/src/utils/localCrypto.ts @@ -4,6 +4,7 @@ */ import * as bcrypt from 'bcryptjs' import * as CryptoJS from 'crypto-js' +import type { User } from '@cbosuite/schema/dist/client-types' const APOLLO_KEY = '-apollo-cache-persist' const SALT_KEY = '-hash-salt' @@ -12,6 +13,9 @@ const HASH_PWD_KEY = '-hash-pwd' const CURRENT_USER_KEY = 'current-user' const VERIFY_TEXT = 'DECRYPT ME' const VERIFY_TEXT_KEY = '-verify' +const USER_KEY = '-user' +const ACCESS_TOKEN_KEY = '-access-token' + /** * Check if a salt value has been stored for the given user. Each user will need a salt value to generate an encrypted * password that will be stored in the session to allow decryption of the apollo persistent cache. @@ -56,43 +60,58 @@ const setPwdHash = (uid: string, pwd: string): boolean => { return true } +const getPwdHash = (uid: string): string => { + return window.localStorage.getItem(uid.concat(HASH_PWD_KEY)) +} + const testPassword = (uid: string, passwd: string) => { const currentPwdHash = getPwdHash(uid) - const edata = window.localStorage.getItem(uid.concat(VERIFY_TEXT_KEY)) - if (!currentPwdHash || !edata) { - return false - } - - const dataBytes = CryptoJS.AES.decrypt(edata, currentPwdHash) - const data = dataBytes.toString(CryptoJS.enc.Utf8) - if (data !== VERIFY_TEXT) { + const salt = getSalt(uid.concat(SALT_KEY)) + if (!currentPwdHash || !salt) { return false } - return true -} + const encryptedPasswd = bcrypt.hashSync(passwd, salt) -const getPwdHash = (uid: string): string => { - return window.localStorage.getItem(uid.concat(HASH_PWD_KEY)) + return encryptedPasswd === currentPwdHash } -const getCurrentUser = (): string => { +const getCurrentUserId = (): string => { return window.localStorage.getItem(CURRENT_USER_KEY) } -const setCurrentUser = (uid: string) => { +const setCurrentUserId = (uid: string) => { window.localStorage.setItem(CURRENT_USER_KEY, uid) } +const getUser = (userId: string): string => { + return window.localStorage.getItem(userId.concat(USER_KEY)) +} + +const setUser = (userId: string, user: User) => { + window.localStorage.setItem(userId.concat(USER_KEY), JSON.stringify(user)) +} + +const getAccessToken = (userId: string): string => { + return window.localStorage.getItem(userId.concat(ACCESS_TOKEN_KEY)) +} + +const setAccessToken = (userId: string, accessToken: string) => { + window.localStorage.setItem(userId.concat(ACCESS_TOKEN_KEY), accessToken) +} + const clearUser = (uid: string): void => { window.localStorage.removeItem(uid.concat(VERIFY_TEXT_KEY)) window.localStorage.removeItem(uid.concat(HASH_PWD_KEY)) window.localStorage.removeItem(uid.concat(SALT_KEY)) + //TODO: remove CURRENT_USER_KEY? } export { - setCurrentUser, - getCurrentUser, + setCurrentUserId, + getCurrentUserId, + getUser, + setUser, checkSalt, setSalt, getSalt, @@ -100,5 +119,7 @@ export { getPwdHash, testPassword, clearUser, + getAccessToken, + setAccessToken, APOLLO_KEY } From f878ec3f9a0fdb2a7f46515136afcaeb0c6e2118 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Thu, 7 Jul 2022 15:26:12 -0400 Subject: [PATCH 02/15] change fetch policy so that we get organization from cache if possible. This is needed so clients and other things are available after logging in offline. --- packages/webapp/src/hooks/api/useOrganization.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/hooks/api/useOrganization.ts b/packages/webapp/src/hooks/api/useOrganization.ts index 94da63d24..edc8793f3 100644 --- a/packages/webapp/src/hooks/api/useOrganization.ts +++ b/packages/webapp/src/hooks/api/useOrganization.ts @@ -40,10 +40,12 @@ export function useOrganization(orgId?: string): UseOranizationReturn { * * */ const [load, { loading, error }] = useLazyQuery(GET_ORGANIZATION, { - fetchPolicy: 'cache-and-network', + fetchPolicy: 'cache-first', onCompleted: (data) => { if (data?.organization) { - setOrg(data.organization) + setTimeout(() => { + setOrg(data.organization) + }) } }, onError: (error) => { From cb06dbf653e3a2d424beb7e5a98a1ff26e1f6cea Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Thu, 7 Jul 2022 15:40:13 -0400 Subject: [PATCH 03/15] added comment to explain setTimeout --- packages/webapp/src/hooks/api/useOrganization.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/hooks/api/useOrganization.ts b/packages/webapp/src/hooks/api/useOrganization.ts index edc8793f3..31f0d1cff 100644 --- a/packages/webapp/src/hooks/api/useOrganization.ts +++ b/packages/webapp/src/hooks/api/useOrganization.ts @@ -32,6 +32,9 @@ export function useOrganization(orgId?: string): UseOranizationReturn { // Common translations const { c } = useTranslation() + // Recoil state used to store and return the cached organization + const [organization, setOrg] = useRecoilState(organizationState) + /** * Lazy graphql query. * @params @@ -43,6 +46,11 @@ export function useOrganization(orgId?: string): UseOranizationReturn { fetchPolicy: 'cache-first', onCompleted: (data) => { if (data?.organization) { + // Use a setTimeout here to avoid an error: "Cannot update a component (`Notifications2`) while rendering a + // different component (`ContainerLayout2`). To locate the bad setState() call inside `ContainerLayout2`" + // when toggling online/offline. This error appeared after switching the fetch policy from cache-and-network + // to cache-first so now the load function returns immediately if data is present in the cache. This hook + // likely needs a refactor. Perhaps a useQuery is more appropriate. setTimeout(() => { setOrg(data.organization) }) @@ -53,9 +61,6 @@ export function useOrganization(orgId?: string): UseOranizationReturn { } }) - // Recoil state used to store and return the cached organization - const [organization, setOrg] = useRecoilState(organizationState) - // If an orgId was passed execute the load function immediately // Otherwise, just return the organization and do NOT make a graph query useEffect(() => { From 01e1f7e34fcf342463a45a588437c6712e0b4d62 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Fri, 8 Jul 2022 11:07:45 -0400 Subject: [PATCH 04/15] move isOffline check to else so that cache gets cleared when online auth works but offline auth fails --- packages/webapp/src/components/forms/LoginForm/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index 4f7d9a93d..3ac455083 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -66,7 +66,7 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ if (isDurableCacheEnabled) { const onlineAuthStatus = resp.status === 'SUCCESS' - const offlineAuthStatus = testPassword(values.username, values.password) && isOffline + const offlineAuthStatus = testPassword(values.username, values.password) localUserStore.username = values.username setCurrentUserId(values.username) if (onlineAuthStatus && offlineAuthStatus) { @@ -85,7 +85,7 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ .removeItem(values.username.concat(APOLLO_KEY)) .then(() => logger(`Apollo persistent storage has been cleared.`)) logger('Password seems to have changed, clearing stored encrypted data.') - } else if (!onlineAuthStatus && offlineAuthStatus) { + } else if (!onlineAuthStatus && offlineAuthStatus && isOffline) { localUserStore.sessionPassword = CryptoJS.SHA512(values.username).toString( CryptoJS.enc.Hex ) From 4e8db1b27bc907ee35e052d54c330605d18be7b0 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Fri, 8 Jul 2022 14:46:56 -0400 Subject: [PATCH 05/15] Added error message on offline auth failure and added login form offline notification --- packages/webapp/src/api/createErrorLink.ts | 2 +- packages/webapp/src/api/index.ts | 4 +++- .../src/components/forms/LoginForm/index.tsx | 12 ++++++++++-- .../ui/OfflineEntityCreationNotice/index.tsx | 19 ++++++++++--------- packages/webapp/src/locales/en-US/common.json | 2 ++ 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/webapp/src/api/createErrorLink.ts b/packages/webapp/src/api/createErrorLink.ts index 6a9c3bfa5..5e440d209 100644 --- a/packages/webapp/src/api/createErrorLink.ts +++ b/packages/webapp/src/api/createErrorLink.ts @@ -36,6 +36,6 @@ export function createErrorLink(history: History) { }) } -const UNAUTHENTICATED = 'UNAUTHENTICATED' +export const UNAUTHENTICATED = 'UNAUTHENTICATED' const TOKEN_EXPIRED = 'TOKEN_EXPIRED' const TOKEN_EXPIRED_ERROR = 'TokenExpiredError' diff --git a/packages/webapp/src/api/index.ts b/packages/webapp/src/api/index.ts index 2167df916..1f90d7be5 100644 --- a/packages/webapp/src/api/index.ts +++ b/packages/webapp/src/api/index.ts @@ -10,7 +10,7 @@ import type { History } from 'history' import { createHttpLink } from './createHttpLink' import { createWebSocketLink } from './createWebSocketLink' import { createErrorLink } from './createErrorLink' -import type QueueLink from '../utils/queueLink' +import type QueueLink from '~utils/queueLink' /** * Configures and creates the Apollo Client. @@ -54,3 +54,5 @@ function isSubscriptionOperation({ query }: Operation) { const definition = getMainDefinition(query) return definition.kind === 'OperationDefinition' && definition.operation === 'subscription' } + +export { UNAUTHENTICATED } from './createErrorLink' diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index 3ac455083..923f21ee7 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -12,6 +12,7 @@ import cx from 'classnames' import { useAuthUser } from '~hooks/api/useAuth' import { useRecoilState } from 'recoil' import { useCallback, useState } from 'react' +import { useHistory } from 'react-router-dom' import { currentUserState } from '~store' import type { User } from '@cbosuite/schema/dist/client-types' import { Namespace, useTranslation } from '~hooks/useTranslation' @@ -40,6 +41,9 @@ import * as CryptoJS from 'crypto-js' import { StatusType } from '~hooks/api' import { storeAccessToken } from '~utils/localStorage' import { useOffline } from '~hooks/useOffline' +import { navigate } from '~utils/navigate' +import { OfflineEntityCreationNotice } from '~components/ui/OfflineEntityCreationNotice' +import { UNAUTHENTICATED } from '~api' const logger = createLogger('authenticate') @@ -59,6 +63,7 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ const [acceptedAgreement, setAcceptedAgreement] = useState(false) const isOffline = useOffline() const [, setCurrentUser] = useRecoilState(currentUserState) + const history = useHistory() const handleLoginClick = useCallback( async (values) => { @@ -98,7 +103,9 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ resp.status = StatusType.Success logger('Offline authentication successful') - } else if (!onlineAuthStatus && !offlineAuthStatus) { + } else if (!offlineAuthStatus && isOffline) { + navigate(history, ApplicationRoute.Login, { error: UNAUTHENTICATED }) + logger('Handle offline login failure: WIP/TBD, limited retry?') } else { logger('Durable cache authentication problem.') @@ -107,12 +114,13 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ onLoginClick(resp.status) }, - [login, onLoginClick, isDurableCacheEnabled, localUserStore, isOffline, setCurrentUser] + [login, onLoginClick, isDurableCacheEnabled, localUserStore, isOffline, setCurrentUser, history] ) const handlePasswordResetClick = useNavCallback(ApplicationRoute.PasswordReset) return ( <> +

{t('login.title')}

diff --git a/packages/webapp/src/components/ui/OfflineEntityCreationNotice/index.tsx b/packages/webapp/src/components/ui/OfflineEntityCreationNotice/index.tsx index b82ef1eed..22cb6b47f 100644 --- a/packages/webapp/src/components/ui/OfflineEntityCreationNotice/index.tsx +++ b/packages/webapp/src/components/ui/OfflineEntityCreationNotice/index.tsx @@ -2,19 +2,20 @@ * Copyright (c) Microsoft. All rights reserved. * Licensed under the MIT license. See LICENSE file in the project. */ +import type { FC } from 'react' import { wrap } from '~utils/appinsights' import { useOffline } from '~hooks/useOffline' import { useTranslation } from '~hooks/useTranslation' import styles from './index.module.scss' import cx from 'classnames' -export const OfflineEntityCreationNotice = wrap(function OfflineEntityCreationNotice() { - const isOffline = useOffline() - const { c } = useTranslation() +export const OfflineEntityCreationNotice: FC<{ isEntityCreation?: boolean }> = wrap( + function OfflineEntityCreationNotice({ isEntityCreation = true }) { + const isOffline = useOffline() + const { c } = useTranslation() - return ( - <> - {isOffline &&
{c('offline.entityCreationNotice')}
} - - ) -}) + const notice = isEntityCreation ? c('offline.entityCreationNotice') : c('offline.generalNotice') + + return <>{isOffline &&
{notice}
} + } +) diff --git a/packages/webapp/src/locales/en-US/common.json b/packages/webapp/src/locales/en-US/common.json index c2e89eccb..7f4de252e 100644 --- a/packages/webapp/src/locales/en-US/common.json +++ b/packages/webapp/src/locales/en-US/common.json @@ -287,6 +287,8 @@ "_notApplicable.comment": "Not Applicable" }, "offline": { + "generalNotice": "You are currently offline. Any records created while offline will be stored on your device and sync automatically when you connect to the internet.", + "_generalNotice.comment": "Used when the user is offline as a general notification", "entityCreationNotice": "You are currently offline. This record will be stored on your device and sync automatically when you connect to the internet.", "_entityCreationNotice.comment": "Used when the user is offline and the form to create a record is open", "connectToTheInternet": "Connect to the Internet", From 74586674c357c8ca77718ec7f8696b728d033201 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Mon, 11 Jul 2022 19:32:55 -0400 Subject: [PATCH 06/15] Encrypt the access token. I had to refactor some things inorder to do that. --- packages/webapp/src/api/getHeaders.ts | 6 +++-- .../src/components/forms/LoginForm/index.tsx | 19 ++------------ .../src/hooks/api/useAuth/useLoginCallback.ts | 6 ++--- packages/webapp/src/utils/localCrypto.ts | 25 +++++++++++++++---- packages/webapp/src/utils/localStorage.ts | 8 ------ 5 files changed, 29 insertions(+), 35 deletions(-) diff --git a/packages/webapp/src/api/getHeaders.ts b/packages/webapp/src/api/getHeaders.ts index 77a7fe43c..1e7d8a5cf 100644 --- a/packages/webapp/src/api/getHeaders.ts +++ b/packages/webapp/src/api/getHeaders.ts @@ -3,7 +3,8 @@ * Licensed under the MIT license. See LICENSE file in the project. */ -import { retrieveAccessToken, retrieveLocale } from '~utils/localStorage' +import { retrieveLocale } from '~utils/localStorage' +import { getAccessToken, getCurrentUserId } from '~utils/localCrypto' export interface RequestHeaders { authorization?: string @@ -21,7 +22,8 @@ export function getHeaders(): RequestHeaders { if (typeof window === 'undefined') return {} // Get values from recoil local store - const accessToken = retrieveAccessToken() + const currentUserId = getCurrentUserId() + const accessToken = getAccessToken(currentUserId) const accept_language = retrieveLocale() // Return node friendly headers diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index 923f21ee7..10909e9e4 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -22,16 +22,7 @@ import { Checkbox } from '@fluentui/react' import { noop } from '~utils/noop' import { useNavCallback } from '~hooks/useNavCallback' import { ApplicationRoute } from '~types/ApplicationRoute' -import { - clearUser, - testPassword, - setCurrentUserId, - checkSalt, - APOLLO_KEY, - setPwdHash, - getAccessToken, - getUser -} from '~utils/localCrypto' +import { testPassword, setCurrentUserId, APOLLO_KEY, getUser } from '~utils/localCrypto' import { createLogger } from '~utils/createLogger' import localforage from 'localforage' import { config } from '~utils/config' @@ -39,7 +30,6 @@ import { useStore } from 'react-stores' import { currentUserStore } from '~utils/current-user-store' import * as CryptoJS from 'crypto-js' import { StatusType } from '~hooks/api' -import { storeAccessToken } from '~utils/localStorage' import { useOffline } from '~hooks/useOffline' import { navigate } from '~utils/navigate' import { OfflineEntityCreationNotice } from '~components/ui/OfflineEntityCreationNotice' @@ -80,26 +70,21 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ ) logger('Online and offline authentication successful!') } else if (onlineAuthStatus && !offlineAuthStatus) { - clearUser(values.username) localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) - checkSalt(values.username) // will create new salt if none found - setPwdHash(values.username, values.password) localforage .removeItem(values.username.concat(APOLLO_KEY)) .then(() => logger(`Apollo persistent storage has been cleared.`)) logger('Password seems to have changed, clearing stored encrypted data.') } else if (!onlineAuthStatus && offlineAuthStatus && isOffline) { - localUserStore.sessionPassword = CryptoJS.SHA512(values.username).toString( + localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) const userJsonString = getUser(values.username) const user = JSON.parse(userJsonString) setCurrentUser(user) - const accessToken = getAccessToken(values.username) - storeAccessToken(accessToken) resp.status = StatusType.Success logger('Offline authentication successful') diff --git a/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts b/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts index 068f5401c..53526a833 100644 --- a/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts +++ b/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts @@ -16,10 +16,9 @@ import { useTranslation } from '~hooks/useTranslation' import { useOffline } from '~hooks/useOffline' import type { MessageResponse } from '../types' import { useCallback } from 'react' -import { storeAccessToken } from '~utils/localStorage' import { handleGraphqlResponse } from '~utils/handleGraphqlResponse' import { StatusType } from '~hooks/api' -import { setUser, setAccessToken } from '~utils/localCrypto' +import { setPwdHash, setUser, checkSalt, setAccessToken } from '~utils/localCrypto' const AUTHENTICATE_USER = gql` ${CurrentUserFields} @@ -56,7 +55,8 @@ export function useLoginCallback(): BasicAuthCallback { toast, failureToast: c('hooks.useAuth.loginFailed'), onSuccess: ({ authenticate }: { authenticate: AuthenticationResponse }) => { - storeAccessToken(authenticate.accessToken) + checkSalt(username) // will create new salt if none found + setPwdHash(username, password) setCurrentUser(authenticate.user) setUser(username, authenticate.user) setAccessToken(username, authenticate.accessToken) diff --git a/packages/webapp/src/utils/localCrypto.ts b/packages/webapp/src/utils/localCrypto.ts index 6df95c995..07b78417d 100644 --- a/packages/webapp/src/utils/localCrypto.ts +++ b/packages/webapp/src/utils/localCrypto.ts @@ -85,26 +85,41 @@ const setCurrentUserId = (uid: string) => { } const getUser = (userId: string): string => { - return window.localStorage.getItem(userId.concat(USER_KEY)) + const currentPwdHash = getPwdHash(userId) + const encryptedUser = window.localStorage.getItem(userId.concat(USER_KEY)) + const dataBytes = CryptoJS.AES.decrypt(encryptedUser, currentPwdHash) + const user = dataBytes.toString(CryptoJS.enc.Utf8) + + return user } const setUser = (userId: string, user: User) => { - window.localStorage.setItem(userId.concat(USER_KEY), JSON.stringify(user)) + const encryptedUser = CryptoJS.AES.encrypt(JSON.stringify(user), getPwdHash(userId)).toString() + window.localStorage.setItem(userId.concat(USER_KEY), encryptedUser) } const getAccessToken = (userId: string): string => { - return window.localStorage.getItem(userId.concat(ACCESS_TOKEN_KEY)) + const currentPwdHash = getPwdHash(userId) + const encryptedAccessToken = window.localStorage.getItem(userId.concat(ACCESS_TOKEN_KEY)) + + if (!currentPwdHash || !encryptedAccessToken) { + return null + } + const dataBytes = CryptoJS.AES.decrypt(encryptedAccessToken, currentPwdHash) + const accessToken = dataBytes.toString(CryptoJS.enc.Utf8) + + return accessToken } const setAccessToken = (userId: string, accessToken: string) => { - window.localStorage.setItem(userId.concat(ACCESS_TOKEN_KEY), accessToken) + const encryptedAccessToken = CryptoJS.AES.encrypt(accessToken, getPwdHash(userId)).toString() + window.localStorage.setItem(userId.concat(ACCESS_TOKEN_KEY), encryptedAccessToken) } const clearUser = (uid: string): void => { window.localStorage.removeItem(uid.concat(VERIFY_TEXT_KEY)) window.localStorage.removeItem(uid.concat(HASH_PWD_KEY)) window.localStorage.removeItem(uid.concat(SALT_KEY)) - //TODO: remove CURRENT_USER_KEY? } export { diff --git a/packages/webapp/src/utils/localStorage.ts b/packages/webapp/src/utils/localStorage.ts index c8970a04b..f1fffdf42 100644 --- a/packages/webapp/src/utils/localStorage.ts +++ b/packages/webapp/src/utils/localStorage.ts @@ -14,14 +14,6 @@ export function clearStoredState() { localStorage.removeItem(RECOIL_PERSIST_KEY) } -export function storeAccessToken(token: string) { - localStorage.setItem(ACCESS_TOKEN_KEY, token) -} - -export function retrieveAccessToken() { - return localStorage.getItem(ACCESS_TOKEN_KEY) -} - export function storeLocale(locale: string) { localStorage.setItem(LOCALE_KEY, locale) } From 9503b3fe3f7ebdcdfae45b3b71e839c888bd56e2 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Tue, 12 Jul 2022 08:45:48 -0400 Subject: [PATCH 07/15] add guard for no userid --- packages/webapp/src/utils/localCrypto.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/webapp/src/utils/localCrypto.ts b/packages/webapp/src/utils/localCrypto.ts index 07b78417d..90a5be0fc 100644 --- a/packages/webapp/src/utils/localCrypto.ts +++ b/packages/webapp/src/utils/localCrypto.ts @@ -99,6 +99,9 @@ const setUser = (userId: string, user: User) => { } const getAccessToken = (userId: string): string => { + if (!userId) { + return null + } const currentPwdHash = getPwdHash(userId) const encryptedAccessToken = window.localStorage.getItem(userId.concat(ACCESS_TOKEN_KEY)) From 871592331333f0f7339485df394d003aeca5965d Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Tue, 12 Jul 2022 09:18:54 -0400 Subject: [PATCH 08/15] set user id sooner so access token can be retrieved on first request. --- .../webapp/src/components/forms/LoginForm/index.tsx | 1 - .../webapp/src/hooks/api/useAuth/useLoginCallback.ts | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index 10909e9e4..122be064a 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -63,7 +63,6 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ const onlineAuthStatus = resp.status === 'SUCCESS' const offlineAuthStatus = testPassword(values.username, values.password) localUserStore.username = values.username - setCurrentUserId(values.username) if (onlineAuthStatus && offlineAuthStatus) { localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex diff --git a/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts b/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts index 53526a833..632ea7b3d 100644 --- a/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts +++ b/packages/webapp/src/hooks/api/useAuth/useLoginCallback.ts @@ -18,7 +18,13 @@ import type { MessageResponse } from '../types' import { useCallback } from 'react' import { handleGraphqlResponse } from '~utils/handleGraphqlResponse' import { StatusType } from '~hooks/api' -import { setPwdHash, setUser, checkSalt, setAccessToken } from '~utils/localCrypto' +import { + setPwdHash, + setUser, + checkSalt, + setAccessToken, + setCurrentUserId +} from '~utils/localCrypto' const AUTHENTICATE_USER = gql` ${CurrentUserFields} @@ -45,6 +51,8 @@ export function useLoginCallback(): BasicAuthCallback { return useCallback( async (username: string, password: string) => { + setCurrentUserId(username) + if (isOffline) { return Promise.resolve({ status: StatusType.Failed, From 9a8422e3ab99017c5a1591be82ea4f1b39435081 Mon Sep 17 00:00:00 2001 From: Jason Baker Date: Tue, 12 Jul 2022 09:37:08 -0400 Subject: [PATCH 09/15] Unused declaration. --- packages/webapp/src/components/forms/LoginForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index 122be064a..87b3ae291 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -22,7 +22,7 @@ import { Checkbox } from '@fluentui/react' import { noop } from '~utils/noop' import { useNavCallback } from '~hooks/useNavCallback' import { ApplicationRoute } from '~types/ApplicationRoute' -import { testPassword, setCurrentUserId, APOLLO_KEY, getUser } from '~utils/localCrypto' +import { testPassword, APOLLO_KEY, getUser } from '~utils/localCrypto' import { createLogger } from '~utils/createLogger' import localforage from 'localforage' import { config } from '~utils/config' From 13593c7dd17872f38d9ec75b665471c2cb99fed9 Mon Sep 17 00:00:00 2001 From: Jason Baker Date: Tue, 12 Jul 2022 10:28:33 -0400 Subject: [PATCH 10/15] Offline data storage user check after browser crash or closed. - required when durableCache is enabled (encrypted local storage) - if user closes browser without loging off the crypto password is discarded - need to login to recreate the key (stored in memory) - detect durableCache is enabled, re-route to login if the in-memory object is empty or missing. --- packages/webapp/src/components/app/Routes.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/webapp/src/components/app/Routes.tsx b/packages/webapp/src/components/app/Routes.tsx index 8e91a04df..859c4b3f7 100644 --- a/packages/webapp/src/components/app/Routes.tsx +++ b/packages/webapp/src/components/app/Routes.tsx @@ -10,6 +10,9 @@ import { AuthorizedRoutes } from './AuthorizedRoutes' import { ApplicationRoute } from '~types/ApplicationRoute' import { useCurrentUser } from '~hooks/api/useCurrentUser' import { LoadingPlaceholder } from '~ui/LoadingPlaceholder' +import { config } from '~utils/config' +import { currentUserStore } from '~utils/current-user-store' + const logger = createLogger('Routes') const Login = lazy(() => /* webpackChunkName: "LoginPage" */ import('~pages/login')) @@ -20,6 +23,15 @@ const PasswordReset = lazy( export const Routes: FC = memo(function Routes() { const location = useLocation() const { currentUser } = useCurrentUser() + + // When saving encrypted data (durableCache), a session key is required (stored during login) + if (Boolean(config.features.durableCache.enabled)) { + const sessionPassword = currentUserStore.state.sessionPassword + if (!sessionPassword) { + location.pathname = '/login' + } + } + useEffect(() => { logger('routes rendering', location.pathname) }, [location.pathname]) From 9a0d190e07adc5b4933442cd93b525afefd0c4f3 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Tue, 12 Jul 2022 13:16:08 -0400 Subject: [PATCH 11/15] Don't set local forage with default user/pswd if current userId already exists. Return null when decrypting if there's no sessionPassword --- .../src/api/local-forage-encrypted-wrapper.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/api/local-forage-encrypted-wrapper.ts b/packages/webapp/src/api/local-forage-encrypted-wrapper.ts index 6f56d7178..031878110 100644 --- a/packages/webapp/src/api/local-forage-encrypted-wrapper.ts +++ b/packages/webapp/src/api/local-forage-encrypted-wrapper.ts @@ -14,9 +14,12 @@ export class LocalForageWrapperEncrypted extends LocalForageWrapper { passwd = 'notusedbyusers' ) { super(storage) - checkSalt(user) - setPwdHash(user, passwd) - setCurrentUserId(user) + const currentUser = getCurrentUserId() + if (!currentUser) { + checkSalt(user) + setPwdHash(user, passwd) + setCurrentUserId(user) + } } getItem(key: string): Promise { @@ -46,6 +49,10 @@ export class LocalForageWrapperEncrypted extends LocalForageWrapper { } private decrypt(cdata, currentUid): string { + if (!currentUserStore.state.sessionPassword) { + return null + } + const dataBytes = CryptoJS.AES.decrypt(cdata, currentUserStore.state.sessionPassword) return dataBytes.toString(CryptoJS.enc.Utf8) } From 8c968c060d6983a6c18f3715900b602c25cca676 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Wed, 13 Jul 2022 11:53:34 -0400 Subject: [PATCH 12/15] load persisted cache after browser refresh --- packages/webapp/src/api/cache.ts | 7 ++++--- packages/webapp/src/api/index.ts | 5 +++-- packages/webapp/src/components/app/App.tsx | 21 +++++++++++-------- .../webapp/src/components/app/Stateful.tsx | 19 ++++++++++------- .../src/components/forms/LoginForm/index.tsx | 20 ++++++++++++++++-- packages/webapp/src/store/index.ts | 6 ++++++ 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/packages/webapp/src/api/cache.ts b/packages/webapp/src/api/cache.ts index 7ff9f62ae..ac415eaa3 100644 --- a/packages/webapp/src/api/cache.ts +++ b/packages/webapp/src/api/cache.ts @@ -52,10 +52,11 @@ const cache: InMemoryCache = new InMemoryCache({ } }) -export function getCache() { - if (isDurableCacheInitialized) { +export function getCache(reloadCache = false) { + // console.log('reloadCache:', reloadCache) + if (isDurableCacheInitialized && !reloadCache) { logger('durable cache is enabled') - } else if (!isDurableCacheInitialized && isDurableCacheEnabled) { + } else if (isDurableCacheEnabled) { persistCache({ cache, storage: new LocalForageWrapperEncrypted(localForage) }) .then(() => { isDurableCacheInitialized = true diff --git a/packages/webapp/src/api/index.ts b/packages/webapp/src/api/index.ts index 1f90d7be5..b77e8b7cd 100644 --- a/packages/webapp/src/api/index.ts +++ b/packages/webapp/src/api/index.ts @@ -24,12 +24,13 @@ const isNodeServer = typeof window === 'undefined' export function createApolloClient( history: History, - queueLink: QueueLink + queueLink: QueueLink, + reloadCache: boolean ): ApolloClient { return new ApolloClient({ ssrMode: isNodeServer, link: createRootLink(history, queueLink), - cache: getCache() + cache: getCache(reloadCache) }) } diff --git a/packages/webapp/src/components/app/App.tsx b/packages/webapp/src/components/app/App.tsx index c9f93db5e..f5a3a62aa 100644 --- a/packages/webapp/src/components/app/App.tsx +++ b/packages/webapp/src/components/app/App.tsx @@ -12,6 +12,7 @@ import type { FC } from 'react' import { memo } from 'react' import { BrowserRouter } from 'react-router-dom' import { config } from '~utils/config' +import { RecoilRoot } from 'recoil' export const App: FC = memo(function App() { // Set the environment name as an attribute @@ -24,15 +25,17 @@ export const App: FC = memo(function App() { return ( - - - - - - - - - + + + + + + + + + + + ) diff --git a/packages/webapp/src/components/app/Stateful.tsx b/packages/webapp/src/components/app/Stateful.tsx index 64e8b2d7f..ace08d06c 100644 --- a/packages/webapp/src/components/app/Stateful.tsx +++ b/packages/webapp/src/components/app/Stateful.tsx @@ -7,18 +7,20 @@ import type { History } from 'history' import type { FC } from 'react' import { useEffect, memo } from 'react' import { useHistory } from 'react-router-dom' -import { RecoilRoot } from 'recoil' +import { useRecoilState } from 'recoil' import { createApolloClient } from '~api' import QueueLink from '../../utils/queueLink' import { useOffline } from '~hooks/useOffline' +import { sessionPasswordState } from '~store' // Create an Apollo Link to queue request while offline const queueLink = new QueueLink() export const Stateful: FC = memo(function Stateful({ children }) { const history: History = useHistory() as any - const apiClient = createApolloClient(history, queueLink) + let apiClient = createApolloClient(history, queueLink, false) const isOffline = useOffline() + const [sessionPassword] = useRecoilState(sessionPasswordState) useEffect(() => { if (isOffline) { @@ -28,9 +30,12 @@ export const Stateful: FC = memo(function Stateful({ children }) { } }, [isOffline]) - return ( - - {children} - - ) + useEffect(() => { + if (sessionPassword) { + // eslint-disable-next-line + apiClient = createApolloClient(history, queueLink, true) + } + }, [sessionPassword]) + + return {children} }) diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index 87b3ae291..d13adcfb4 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -13,7 +13,7 @@ import { useAuthUser } from '~hooks/api/useAuth' import { useRecoilState } from 'recoil' import { useCallback, useState } from 'react' import { useHistory } from 'react-router-dom' -import { currentUserState } from '~store' +import { currentUserState, sessionPasswordState } from '~store' import type { User } from '@cbosuite/schema/dist/client-types' import { Namespace, useTranslation } from '~hooks/useTranslation' import { FormSectionTitle } from '~components/ui/FormSectionTitle' @@ -53,6 +53,8 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ const [acceptedAgreement, setAcceptedAgreement] = useState(false) const isOffline = useOffline() const [, setCurrentUser] = useRecoilState(currentUserState) + const [, setSessionPassword] = useRecoilState(sessionPasswordState) + const history = useHistory() const handleLoginClick = useCallback( @@ -67,11 +69,15 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) + setSessionPassword(localUserStore.sessionPassword) + logger('Online and offline authentication successful!') } else if (onlineAuthStatus && !offlineAuthStatus) { localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) + setSessionPassword(localUserStore.sessionPassword) + localforage .removeItem(values.username.concat(APOLLO_KEY)) .then(() => logger(`Apollo persistent storage has been cleared.`)) @@ -80,6 +86,7 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) + setSessionPassword(localUserStore.sessionPassword) const userJsonString = getUser(values.username) const user = JSON.parse(userJsonString) @@ -98,7 +105,16 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ onLoginClick(resp.status) }, - [login, onLoginClick, isDurableCacheEnabled, localUserStore, isOffline, setCurrentUser, history] + [ + login, + onLoginClick, + isDurableCacheEnabled, + localUserStore, + isOffline, + setCurrentUser, + history, + setSessionPassword + ] ) const handlePasswordResetClick = useNavCallback(ApplicationRoute.PasswordReset) diff --git a/packages/webapp/src/store/index.ts b/packages/webapp/src/store/index.ts index f0e479c6a..ab9dbcc38 100644 --- a/packages/webapp/src/store/index.ts +++ b/packages/webapp/src/store/index.ts @@ -119,3 +119,9 @@ export const fieldFiltersState = atom({ default: [], effects_UNSTABLE: [persistAtom] }) + +// +export const sessionPasswordState = atom({ + key: 'sessionPassword', + default: '' +}) From 57ed740406ff1ceea3f141e878f08f77509bc8fa Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Wed, 13 Jul 2022 15:47:36 -0400 Subject: [PATCH 13/15] some cleanup --- packages/webapp/src/api/cache.ts | 1 - packages/webapp/src/components/app/Stateful.tsx | 3 ++- packages/webapp/src/utils/current-user-store.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/api/cache.ts b/packages/webapp/src/api/cache.ts index ac415eaa3..a441b306f 100644 --- a/packages/webapp/src/api/cache.ts +++ b/packages/webapp/src/api/cache.ts @@ -53,7 +53,6 @@ const cache: InMemoryCache = new InMemoryCache({ }) export function getCache(reloadCache = false) { - // console.log('reloadCache:', reloadCache) if (isDurableCacheInitialized && !reloadCache) { logger('durable cache is enabled') } else if (isDurableCacheEnabled) { diff --git a/packages/webapp/src/components/app/Stateful.tsx b/packages/webapp/src/components/app/Stateful.tsx index ace08d06c..ed3a2d898 100644 --- a/packages/webapp/src/components/app/Stateful.tsx +++ b/packages/webapp/src/components/app/Stateful.tsx @@ -9,7 +9,7 @@ import { useEffect, memo } from 'react' import { useHistory } from 'react-router-dom' import { useRecoilState } from 'recoil' import { createApolloClient } from '~api' -import QueueLink from '../../utils/queueLink' +import QueueLink from '~utils/queueLink' import { useOffline } from '~hooks/useOffline' import { sessionPasswordState } from '~store' @@ -32,6 +32,7 @@ export const Stateful: FC = memo(function Stateful({ children }) { useEffect(() => { if (sessionPassword) { + // TODO: fix lint error generated by line below // eslint-disable-next-line apiClient = createApolloClient(history, queueLink, true) } diff --git a/packages/webapp/src/utils/current-user-store.ts b/packages/webapp/src/utils/current-user-store.ts index 0fecfdb68..c7f4422ee 100644 --- a/packages/webapp/src/utils/current-user-store.ts +++ b/packages/webapp/src/utils/current-user-store.ts @@ -4,6 +4,7 @@ */ import { Store } from 'react-stores' +// TODO: may not need to using recoil to store this export const currentUserStore = new Store({ username: '', sessionPassword: '' From c7f9e6d60fea15887db8d093470f944790152a2a Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Wed, 13 Jul 2022 16:15:32 -0400 Subject: [PATCH 14/15] added comments explain storing session password in 2 spots --- packages/webapp/src/components/forms/LoginForm/index.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/webapp/src/components/forms/LoginForm/index.tsx b/packages/webapp/src/components/forms/LoginForm/index.tsx index d13adcfb4..7d7753cdf 100644 --- a/packages/webapp/src/components/forms/LoginForm/index.tsx +++ b/packages/webapp/src/components/forms/LoginForm/index.tsx @@ -66,6 +66,8 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ const offlineAuthStatus = testPassword(values.username, values.password) localUserStore.username = values.username if (onlineAuthStatus && offlineAuthStatus) { + // Store session password in react store so we can use it in LocalForageWrapperEncrypted class (recoil hook cannot be used in class) + // Store the session password in recoil so we can trigger loading and decrypting the cache from persistent storage (see Stateful.tsx) localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) @@ -73,6 +75,8 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ logger('Online and offline authentication successful!') } else if (onlineAuthStatus && !offlineAuthStatus) { + // Store session password in react store so we can use it in LocalForageWrapperEncrypted class (recoil hook cannot be used in class) + // Store the session password in recoil so we can trigger loading and decrypting the cache from persistent storage (see Stateful.tsx) localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) @@ -83,6 +87,8 @@ export const LoginForm: StandardFC = wrap(function LoginForm({ .then(() => logger(`Apollo persistent storage has been cleared.`)) logger('Password seems to have changed, clearing stored encrypted data.') } else if (!onlineAuthStatus && offlineAuthStatus && isOffline) { + // Store session password in react store so we can use it in LocalForageWrapperEncrypted class (recoil hook cannot be used in class) + // Store the session password in recoil so we can trigger loading and decrypting the cache from persistent storage (see Stateful.tsx) localUserStore.sessionPassword = CryptoJS.SHA512(values.password).toString( CryptoJS.enc.Hex ) From d0305571608ac482b85649289b5238da3cfea410 Mon Sep 17 00:00:00 2001 From: Mark Deitner Date: Wed, 13 Jul 2022 16:43:48 -0400 Subject: [PATCH 15/15] Don't encrypt before sessionPassoword is set --- packages/webapp/src/api/local-forage-encrypted-wrapper.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/webapp/src/api/local-forage-encrypted-wrapper.ts b/packages/webapp/src/api/local-forage-encrypted-wrapper.ts index 031878110..c0406b175 100644 --- a/packages/webapp/src/api/local-forage-encrypted-wrapper.ts +++ b/packages/webapp/src/api/local-forage-encrypted-wrapper.ts @@ -44,6 +44,9 @@ export class LocalForageWrapperEncrypted extends LocalForageWrapper { } private encrypt(data, currentUid): string { + if (!currentUserStore.state.sessionPassword) { + return + } const edata = CryptoJS.AES.encrypt(data, currentUserStore.state.sessionPassword).toString() return edata }