From 713f61c781936d821326fb841481a1b2c5f3a26c Mon Sep 17 00:00:00 2001 From: rtwohey Date: Tue, 12 Mar 2024 14:50:47 -0400 Subject: [PATCH 1/4] feat(IV): utilize useSyncExternalStore for independent data source --- .../android/app/capacitor.build.gradle | 2 +- .../src/core/AuthProvider.tsx | 18 +++---- demos/security-trifecta/src/utils/auth.ts | 8 +-- .../src/utils/session-vault.ts | 49 +++++++++++++------ 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/demos/security-trifecta/android/app/capacitor.build.gradle b/demos/security-trifecta/android/app/capacitor.build.gradle index bec7852..7cdb5f4 100644 --- a/demos/security-trifecta/android/app/capacitor.build.gradle +++ b/demos/security-trifecta/android/app/capacitor.build.gradle @@ -19,7 +19,7 @@ dependencies { implementation "net.zetetic:android-database-sqlcipher:4.5.2" implementation "androidx.sqlite:sqlite:2.0.1" } -apply from: "../../../../node_modules/.pnpm/@ionic-enterprise+identity-vault@5.12.1_typescript@5.3.3/node_modules/@ionic-enterprise/identity-vault/src/android/ionicnativeauth.gradle" +apply from: "../../../../node_modules/.pnpm/@ionic-enterprise+identity-vault@5.12.4_typescript@5.3.3/node_modules/@ionic-enterprise/identity-vault/src/android/ionicnativeauth.gradle" if (hasProperty('postBuildExtras')) { postBuildExtras() diff --git a/demos/security-trifecta/src/core/AuthProvider.tsx b/demos/security-trifecta/src/core/AuthProvider.tsx index 954680f..f32dea3 100644 --- a/demos/security-trifecta/src/core/AuthProvider.tsx +++ b/demos/security-trifecta/src/core/AuthProvider.tsx @@ -1,10 +1,10 @@ -import { ReactNode, createContext, useContext, useEffect, useState } from 'react'; +import { ReactNode, createContext, useContext, useEffect, useState, useSyncExternalStore } from 'react'; import { IonSpinner, useIonModal } from '@ionic/react'; import { AuthResult } from '@ionic-enterprise/auth'; -import { useHistory } from 'react-router'; -import { registerCallback, unregisterCallback } from '../utils/session-vault'; +import { getSnapshot, registerCallback, subscribe, unregisterCallback } from '../utils/session-vault'; import { setupAuthConnect } from '../utils/auth'; import { PinDialog } from '../pages/PinDialog/PinDialog'; +import { useHistory } from 'react-router'; type Props = { children?: ReactNode }; type Context = { session?: AuthResult }; @@ -15,7 +15,6 @@ let handlePasscodeRequest: CustomPasscodeCallback = () => {}; const AuthContext = createContext(undefined); const AuthProvider = ({ children }: Props) => { const history = useHistory(); - const [session, setSession] = useState(undefined); const [isSetup, setIsSetup] = useState(false); const [isSetPasscodeMode, setIsSetPasscodeMode] = useState(false); const [showModal, setShowModal] = useState(false); @@ -24,6 +23,8 @@ const AuthProvider = ({ children }: Props) => { onDismiss: (opts: { data: any; role?: string }) => handlePasscodeRequest(opts), }); + const session = useSyncExternalStore(subscribe, getSnapshot); + const handlePasscodeRequested = (isPasscodeSetRequest: boolean, onComplete: (code: string) => void): void => { handlePasscodeRequest = (opts) => { onComplete(opts.role === 'cancel' ? '' : opts.data); @@ -39,18 +40,15 @@ const AuthProvider = ({ children }: Props) => { }, []); useEffect(() => { - registerCallback('onSessionChange', (s: AuthResult | undefined) => { - setSession(s); - }); - registerCallback('onVaultLock', () => history.replace('/login')); registerCallback('onPasscodeRequested', (isSetPasscodeMode, onComplete) => handlePasscodeRequested(isSetPasscodeMode, onComplete), ); + registerCallback('onVaultLock', () => history.replace('/login')); + return () => { - unregisterCallback('onSessionChange'); - unregisterCallback('onVaultLock'); unregisterCallback('onPasscodeRequested'); + unregisterCallback('onVaultLock'); }; }, []); diff --git a/demos/security-trifecta/src/utils/auth.ts b/demos/security-trifecta/src/utils/auth.ts index dcb49c2..0c405f9 100644 --- a/demos/security-trifecta/src/utils/auth.ts +++ b/demos/security-trifecta/src/utils/auth.ts @@ -1,6 +1,6 @@ import { Auth0Provider, AuthConnect, AuthResult, ProviderOptions, TokenType } from '@ionic-enterprise/auth'; import { isPlatform } from '@ionic/react'; -import { clearSession, getSession, setSession } from './session-vault'; +import { clearSession, setSession, getSnapshot } from './session-vault'; const isMobile = isPlatform('hybrid'); const url = isMobile ? 'io.ionic.acdemo://auth-action-complete' : 'http://localhost:8100/auth-action-complete'; @@ -40,7 +40,7 @@ const performRefresh = async (authResult: AuthResult): Promise => { - let authResult = await getSession(); + let authResult = await getSnapshot(); if (authResult && (await AuthConnect.isAccessTokenExpired(authResult))) { authResult = await performRefresh(authResult); } @@ -63,7 +63,7 @@ const login = async (): Promise => { }; const logout = async (): Promise => { - const authResult = await getSession(); + const authResult = await getSnapshot(); if (authResult) { await AuthConnect.logout(provider, authResult); await clearSession(); @@ -71,7 +71,7 @@ const logout = async (): Promise => { }; const getUserEmail = async (): Promise => { - const authResult = await getSession(); + const authResult = await getSnapshot(); if (authResult) { const { email } = (await AuthConnect.decodeToken(TokenType.id, authResult)) as any; return email; diff --git a/demos/security-trifecta/src/utils/session-vault.ts b/demos/security-trifecta/src/utils/session-vault.ts index 074ae3d..fbd37a5 100644 --- a/demos/security-trifecta/src/utils/session-vault.ts +++ b/demos/security-trifecta/src/utils/session-vault.ts @@ -1,5 +1,5 @@ import { Preferences } from '@capacitor/preferences'; -import { DeviceSecurityType, IdentityVaultConfig, VaultErrorCodes, VaultType } from '@ionic-enterprise/identity-vault'; +import { DeviceSecurityType, IdentityVaultConfig, VaultType } from '@ionic-enterprise/identity-vault'; import { AuthResult } from '@ionic-enterprise/auth'; import { createVault } from './vault-factory'; import { provisionBiometricPermission } from './device'; @@ -9,7 +9,6 @@ import { isPlatform } from '@ionic/react'; type VaultUnlockType = Pick; type CallbackMap = { - onSessionChange?: (session: AuthResult | undefined) => void; onVaultLock?: () => void; onPasscodeRequested?: (isPasscodeSetRequest: boolean, onComplete: (code: string) => void) => void; }; @@ -18,8 +17,33 @@ const keys = { session: 'session', mode: 'last-unlock-mode' }; const vault = createVault(); let session: AuthResult | undefined; +let listeners: any[] = []; + const callbackMap: CallbackMap = {}; +const subscribe = (listener: any) => { + listeners = [...listeners, listener]; + return () => { + listeners = listeners.filter((l) => l !== listener); + }; +}; + +const getSnapshot = (): AuthResult | undefined => { + return session; +}; + +const emitChange = () => { + for (let listener of listeners) { + listener(); + } +}; + +const setSession = async (newSession: AuthResult | undefined) => { + session = newSession; + await vault.setValue(keys.session, session); + emitChange(); +}; + const initializeVault = async (): Promise => { vault.initialize({ key: 'io.ionic.teatastereact', @@ -31,9 +55,10 @@ const initializeVault = async (): Promise => { unlockVaultOnLoad: false, }); - vault.onLock(() => { + vault.onLock(async () => { session = undefined; - if (callbackMap.onVaultLock) callbackMap.onVaultLock(); + if (callbackMap.onVaultLock) await callbackMap.onVaultLock(); + emitChange(); }); vault.onPasscodeRequested((isPasscodeSetRequest, onComplete) => { @@ -45,27 +70,21 @@ const clearSession = async (): Promise => { session = undefined; await vault.clear(); await setUnlockMode('SecureStorage'); - if (callbackMap.onSessionChange) callbackMap.onSessionChange(undefined); + emitChange(); }; -const getSession = async (): Promise => { +const getSession = async (): Promise => { if (!session) session = (await vault.getValue(keys.session)) || undefined; - return session; + emitChange(); }; const restoreSession = async (): Promise => { const s = (await vault.getValue(keys.session)) || undefined; session = s; - if (callbackMap.onSessionChange) callbackMap.onSessionChange(session); + emitChange(); return session; }; -const setSession = async (s: AuthResult): Promise => { - session = s; - await vault.setValue(keys.session, s); - if (callbackMap.onSessionChange) callbackMap.onSessionChange(session); -}; - const getUnlockModeConfig = async (unlockMode: UnlockMode): Promise => { switch (unlockMode) { case 'Biometrics': @@ -122,4 +141,6 @@ export { registerCallback, unregisterCallback, canUseLocking, + subscribe, + getSnapshot, }; From ab83c3903cd096dfeec5b636ecee1f3591044f74 Mon Sep 17 00:00:00 2001 From: rtwohey Date: Wed, 10 Apr 2024 15:28:11 -0400 Subject: [PATCH 2/4] Updated private route to check for the session with getSnapshot from the external store --- demos/security-trifecta/src/core/PrivateRoute.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demos/security-trifecta/src/core/PrivateRoute.tsx b/demos/security-trifecta/src/core/PrivateRoute.tsx index 07b13b3..85630d4 100644 --- a/demos/security-trifecta/src/core/PrivateRoute.tsx +++ b/demos/security-trifecta/src/core/PrivateRoute.tsx @@ -1,11 +1,12 @@ import { ReactNode } from 'react'; import { Redirect, Route, useLocation } from 'react-router'; import { useAuth } from './AuthProvider'; +import { getSnapshot } from '../utils/session-vault'; type Props = { children?: ReactNode }; export const PrivateRoute = ({ children }: Props) => { - const { session } = useAuth(); + const session = getSnapshot(); // If there is no session, redirect the user to the login page. if (!session) return ; From 8f89c3465f0aa81057460dbcc9a65d9431bdf6e9 Mon Sep 17 00:00:00 2001 From: rtwohey Date: Tue, 4 Jun 2024 10:23:47 -0400 Subject: [PATCH 3/4] adjusted private routes to use external store --- demos/security-trifecta/src/core/AuthProvider.tsx | 2 +- demos/security-trifecta/src/core/PrivateRoute.tsx | 3 +-- demos/security-trifecta/src/utils/useAuth.ts | 8 ++++++++ 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 demos/security-trifecta/src/utils/useAuth.ts diff --git a/demos/security-trifecta/src/core/AuthProvider.tsx b/demos/security-trifecta/src/core/AuthProvider.tsx index f32dea3..fc0519e 100644 --- a/demos/security-trifecta/src/core/AuthProvider.tsx +++ b/demos/security-trifecta/src/core/AuthProvider.tsx @@ -44,7 +44,7 @@ const AuthProvider = ({ children }: Props) => { handlePasscodeRequested(isSetPasscodeMode, onComplete), ); - registerCallback('onVaultLock', () => history.replace('/login')); + registerCallback('onVaultLock', () => history.replace('/')); return () => { unregisterCallback('onPasscodeRequested'); diff --git a/demos/security-trifecta/src/core/PrivateRoute.tsx b/demos/security-trifecta/src/core/PrivateRoute.tsx index 85630d4..49690b7 100644 --- a/demos/security-trifecta/src/core/PrivateRoute.tsx +++ b/demos/security-trifecta/src/core/PrivateRoute.tsx @@ -1,6 +1,5 @@ import { ReactNode } from 'react'; -import { Redirect, Route, useLocation } from 'react-router'; -import { useAuth } from './AuthProvider'; +import { Redirect } from 'react-router'; import { getSnapshot } from '../utils/session-vault'; type Props = { children?: ReactNode }; diff --git a/demos/security-trifecta/src/utils/useAuth.ts b/demos/security-trifecta/src/utils/useAuth.ts new file mode 100644 index 0000000..5330776 --- /dev/null +++ b/demos/security-trifecta/src/utils/useAuth.ts @@ -0,0 +1,8 @@ +import { useSyncExternalStore } from 'react'; +import { subscribe, getSnapshot } from './session-vault'; + +const useAuth = () => { + return useSyncExternalStore(subscribe, getSnapshot); +}; + +export default useAuth; From ccc2e60af032649603243e8aa94c7a23d1de32fd Mon Sep 17 00:00:00 2001 From: rtwohey Date: Wed, 5 Jun 2024 12:36:40 -0400 Subject: [PATCH 4/4] rename useAuth to useSession for clarity --- .../security-trifecta/src/utils/{useAuth.ts => useSession.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename demos/security-trifecta/src/utils/{useAuth.ts => useSession.ts} (75%) diff --git a/demos/security-trifecta/src/utils/useAuth.ts b/demos/security-trifecta/src/utils/useSession.ts similarity index 75% rename from demos/security-trifecta/src/utils/useAuth.ts rename to demos/security-trifecta/src/utils/useSession.ts index 5330776..bac75c8 100644 --- a/demos/security-trifecta/src/utils/useAuth.ts +++ b/demos/security-trifecta/src/utils/useSession.ts @@ -1,8 +1,8 @@ import { useSyncExternalStore } from 'react'; import { subscribe, getSnapshot } from './session-vault'; -const useAuth = () => { +const useSession = () => { return useSyncExternalStore(subscribe, getSnapshot); }; -export default useAuth; +export default useSession;