From 3050e5dea2dfd6a3c94640b1fc7504ffbfca507d Mon Sep 17 00:00:00 2001 From: Arno V Date: Thu, 18 Jul 2024 11:33:02 -0400 Subject: [PATCH 1/3] feat: custom built fp and logger in debug mode --- examples/code-flow/src/index.tsx | 1 + examples/code-flow/src/main.tsx | 22 +------ packages/auth-provider/package.json | 6 +- packages/auth-provider/src/common/types.d.ts | 2 + .../auth-provider/src/common/utilities.ts | 10 +--- .../components/AuthProvider/AuthProvider.tsx | 59 ++++++++++++++++--- .../AuthProvider/InternalContext.ts | 1 + pnpm-lock.yaml | 16 ++--- 8 files changed, 70 insertions(+), 47 deletions(-) diff --git a/examples/code-flow/src/index.tsx b/examples/code-flow/src/index.tsx index 1bc8501..e4b5086 100644 --- a/examples/code-flow/src/index.tsx +++ b/examples/code-flow/src/index.tsx @@ -15,6 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render( clientId={"b44c68f0-e5b3-4a1d-a3e3-df8632b0223b"} sessionExpiration={timeout} domain={"gizmette.local.com"} + debug > diff --git a/examples/code-flow/src/main.tsx b/examples/code-flow/src/main.tsx index af788f0..152f0a9 100644 --- a/examples/code-flow/src/main.tsx +++ b/examples/code-flow/src/main.tsx @@ -10,45 +10,27 @@ export const App = ({ timeout }: { timeout: string }) => { logout, isAuthenticated, getAccessToken, - isLoading, registeringForPasskey, loginWithPasskey, } = useAuth(); const [apiResponse, setApiResponse] = useState({ data: "" }); - console.info({ isAuthenticated, isLoading }); - - const logger = console; - logger.log("isAuthenticated", isAuthenticated); - logger.log("isLoading", isLoading); const handleValidLogin = async (e: { preventDefault: () => void }) => { e.preventDefault(); - const response = await login( + await login( process.env.PUBLIC_TEST_USER as string, process.env.PUBLIC_TEST_USER_PASSWORD as string, AUTH_TYPES.CODE, ); - if (!response) { - console.error(`==> [${Date.now()}] : `, "Login failed"); - } else { - console.info(`==> [${Date.now()}] : `, "Login successful"); - console.info(`==> [${Date.now()}] : `, response); - } }; const handleInvalidLogin = async (e: { preventDefault: () => void }) => { e.preventDefault(); - const response = await login( + await login( process.env.PUBLIC_TEST_USER as string, "invalid-password", AUTH_TYPES.CODE, ); - if (!response) { - console.error(`==> [${Date.now()}] : `, "Login failed"); - } else { - console.info(`==> [${Date.now()}] : `, "Login successful"); - console.info(`==> [${Date.now()}] : `, response); - } }; const handleValidAPICall = async (e: { preventDefault: () => void }) => { diff --git a/packages/auth-provider/package.json b/packages/auth-provider/package.json index f11fd5c..a99b9e5 100644 --- a/packages/auth-provider/package.json +++ b/packages/auth-provider/package.json @@ -14,9 +14,7 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", - "files": [ - "dist" - ], + "files": ["dist"], "scripts": { "build:check": "tsc", "build:js": "vite build", @@ -45,8 +43,8 @@ }, "dependencies": { "@simplewebauthn/browser": "10.0.0", - "@thumbmarkjs/thumbmarkjs": "0.14.8", "@versini/auth-common": "workspace:../auth-common", + "@versini/ui-fingerprint": "1.0.1", "@versini/ui-hooks": "4.0.1", "jose": "5.6.3", "uuid": "10.0.0" diff --git a/packages/auth-provider/src/common/types.d.ts b/packages/auth-provider/src/common/types.d.ts index b6bab2c..6deea13 100644 --- a/packages/auth-provider/src/common/types.d.ts +++ b/packages/auth-provider/src/common/types.d.ts @@ -25,6 +25,7 @@ export type AuthProviderProps = { clientId: string; accessType?: string; domain?: string; + debug?: boolean; }; export type AuthState = { @@ -36,6 +37,7 @@ export type AuthState = { userId?: string; username?: string; }; + debug?: boolean; }; export type LoginType = ( diff --git a/packages/auth-provider/src/common/utilities.ts b/packages/auth-provider/src/common/utilities.ts index aaa0b56..5688f75 100644 --- a/packages/auth-provider/src/common/utilities.ts +++ b/packages/auth-provider/src/common/utilities.ts @@ -1,4 +1,3 @@ -import { getFingerprint } from "@thumbmarkjs/thumbmarkjs"; import { API_TYPE, AUTH_TYPES, @@ -6,6 +5,7 @@ import { JWT, verifyAndExtractToken, } from "@versini/auth-common"; +import { getFingerprintHash } from "@versini/ui-fingerprint"; import { API_ENDPOINT } from "./constants"; import type { ServiceCallProps } from "./types"; @@ -379,13 +379,7 @@ export const graphQLCall = async ({ export const getCustomFingerprint = async () => { try { - const res = await getFingerprint(); - if (typeof res === "string") { - return res; - } else if (res.hash && typeof res.hash === "string") { - return res.hash; - } - return ""; + return await getFingerprintHash(); } catch (_error) { return ""; } diff --git a/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx b/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx index 4c638c8..ec737e5 100644 --- a/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx +++ b/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx @@ -42,6 +42,7 @@ export const AuthProvider = ({ sessionExpiration, clientId, domain = "", + debug = false, }: AuthProviderProps) => { const [state, dispatch] = useReducer(reducer, { isLoading: true, @@ -49,9 +50,11 @@ export const AuthProvider = ({ authenticationType: null, user: undefined, logoutReason: "", + debug, }); const effectDidRunRef = useRef(false); + const fingerprintRef = useRef(""); const [idToken, setIdToken, , removeIdToken] = useLocalStorage({ key: `${LOCAL_STORAGE_PREFIX}::${clientId}::@@user@@`, @@ -67,11 +70,23 @@ export const AuthProvider = ({ const [nonce, setNonce, , removeNonce] = useLocalStorage({ key: `${LOCAL_STORAGE_PREFIX}::${clientId}::@@nonce@@`, }); + + const logger = useCallback( + (...args: any[]) => { + if (debug) { + console.info(`==> [Auth ${Date.now()}]: `, ...args); + } + }, + [debug], + ); const tokenManager = new TokenManager(accessToken, refreshToken); const removeStateAndLocalStorage = useCallback( (logoutReason?: string) => { - console.warn(logoutReason); + logger( + "removeStateAndLocalStorage: removing state and local storage with reason: ", + logoutReason, + ); dispatch({ type: ACTION_TYPE_LOGOUT, payload: { @@ -84,11 +99,12 @@ export const AuthProvider = ({ removeNonce(); dispatch({ type: ACTION_TYPE_LOADING, payload: { isLoading: false } }); }, - [removeAccessToken, removeIdToken, removeNonce, removeRefreshToken], + [removeAccessToken, removeIdToken, removeNonce, removeRefreshToken, logger], ); const invalidateAndLogout = useCallback( async (message: string) => { + logger("invalidateAndLogout: invalidating and logging out"); const { user } = state; await logoutUser({ userId: user?.userId || "", @@ -108,9 +124,20 @@ export const AuthProvider = ({ idToken, refreshToken, removeStateAndLocalStorage, + logger, ], ); + /** + * This effect is responsible to set the fingerprintRef value when the + * component is first loaded. + */ + useEffect(() => { + (async () => { + fingerprintRef.current = await getCustomFingerprint(); + })(); + }, []); + /** * This effect is responsible to set the authentication state based on the * idToken stored in the local storage. It is used when the page is being @@ -125,6 +152,7 @@ export const AuthProvider = ({ try { const jwt = await verifyAndExtractToken(idToken); if (jwt && jwt.payload[JWT.USER_ID_KEY] !== "") { + logger("useEffect: setting the authentication state"); dispatch({ type: ACTION_TYPE_LOGIN, payload: { @@ -136,19 +164,24 @@ export const AuthProvider = ({ }, }); } else { + logger("useEffect: invalid JWT, invalidating and logging out"); await invalidateAndLogout(EXPIRED_SESSION); } } catch (_error) { + logger( + "useEffect: exception validating JWT, invalidating and logging out", + ); await invalidateAndLogout(EXPIRED_SESSION); } })(); } else { + logger("useEffect: setting the loading state to false"); dispatch({ type: ACTION_TYPE_LOADING, payload: { isLoading: false } }); } return () => { effectDidRunRef.current = true; }; - }, [state.isLoading, idToken, invalidateAndLogout]); + }, [state.isLoading, idToken, invalidateAndLogout, logger]); const login: LoginType = async (username, password, type) => { const _nonce = uuidv4(); @@ -158,9 +191,10 @@ export const AuthProvider = ({ removeAccessToken(); removeRefreshToken(); + logger("login: Logging in with type: ", type); + if (type === AUTH_TYPES.CODE) { const { code_verifier, code_challenge } = await pkceChallengePair(); - const preResponse = await getPreAuthCode({ nonce: _nonce, clientId, @@ -178,7 +212,7 @@ export const AuthProvider = ({ code: preResponse.code, code_verifier, domain, - fingerprint: await getCustomFingerprint(), + fingerprint: fingerprintRef.current, }); if (response.status) { setIdToken(response.idToken); @@ -210,7 +244,7 @@ export const AuthProvider = ({ nonce: _nonce, type, domain, - fingerprint: await getCustomFingerprint(), + fingerprint: fingerprintRef.current, }); if (response.status) { setIdToken(response.idToken); @@ -242,6 +276,7 @@ export const AuthProvider = ({ try { if (isAuthenticated && user && user.userId) { if (accessToken) { + logger("getAccessToken"); const jwtAccess = await verifyAndExtractToken(accessToken); if (jwtAccess && jwtAccess.payload[JWT.USER_ID_KEY] !== "") { return accessToken; @@ -251,6 +286,7 @@ export const AuthProvider = ({ * accessToken is not valid, so we need to try to refresh it using the * refreshToken - this is a silent refresh. */ + logger("getAccessToken: invalid access token, refreshing it"); const res = await tokenManager.refreshtoken({ clientId, userId: user.userId as string, @@ -265,12 +301,19 @@ export const AuthProvider = ({ /** * refreshToken is not valid, so we need to re-authenticate the user. */ + logger("getAccessToken: invalid refresh token, re-authenticating user"); await invalidateAndLogout(ACCESS_TOKEN_ERROR); return ""; } + logger( + "getAccessToken: user is not authenticated, cannot get access token", + ); await invalidateAndLogout(ACCESS_TOKEN_ERROR); return ""; } catch (_error) { + logger( + "getAccessToken: exception occurred, invalidating and logging out", + ); await invalidateAndLogout(ACCESS_TOKEN_ERROR); return ""; } @@ -333,6 +376,8 @@ export const AuthProvider = ({ removeAccessToken(); removeRefreshToken(); + logger("loginWithPasskey"); + const temporaryAnonymousUserId = uuidv4(); let response = await graphQLCall({ accessToken, @@ -356,7 +401,7 @@ export const AuthProvider = ({ authentication, nonce: _nonce, domain, - fingerprint: await getCustomFingerprint(), + fingerprint: fingerprintRef.current, }, }); diff --git a/packages/auth-provider/src/components/AuthProvider/InternalContext.ts b/packages/auth-provider/src/components/AuthProvider/InternalContext.ts index 8adb9ab..d5e9287 100644 --- a/packages/auth-provider/src/components/AuthProvider/InternalContext.ts +++ b/packages/auth-provider/src/components/AuthProvider/InternalContext.ts @@ -11,6 +11,7 @@ export const InternalContext = React.createContext<{ authenticationType: null, user: undefined, logoutReason: "", + debug: false, }, dispatch: () => {}, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 438530a..88bfd58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,12 +149,12 @@ importers: '@simplewebauthn/browser': specifier: 10.0.0 version: 10.0.0 - '@thumbmarkjs/thumbmarkjs': - specifier: 0.14.8 - version: 0.14.8 '@versini/auth-common': specifier: workspace:../auth-common version: link:../auth-common + '@versini/ui-fingerprint': + specifier: 1.0.1 + version: 1.0.1 '@versini/ui-hooks': specifier: 4.0.1 version: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1516,9 +1516,6 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@thumbmarkjs/thumbmarkjs@0.14.8': - resolution: {integrity: sha512-J+/HnYBv24ufFCoyqbFhtZk1zTGKWzHfyN15sc6kJq/1D/6H1oaKt+W1yWxkUwzclMS1n36EaoHbNjFeiuDqDg==} - '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -1683,6 +1680,9 @@ packages: react: ^18.3.1 react-dom: ^18.3.1 + '@versini/ui-fingerprint@1.0.1': + resolution: {integrity: sha512-V7Gtyk2FU7lRv3+qcWEHtvTV3XbZSVm9Q2/t28DChQZO89MMHWZQRfrfFH2UV30j8aRxDX0Z/jao0+ZOZ9Ixbg==} + '@versini/ui-hooks@4.0.1': resolution: {integrity: sha512-LiYYRGg5j1jFyQniJVx/Ke8hXaXIHBo7LPt0MyJR5b7FfF0AbxoIt8AfQrpYFERvjpq8kXlib0ELMdC0nbO4yw==} peerDependencies: @@ -7567,8 +7567,6 @@ snapshots: dependencies: '@testing-library/dom': 10.3.1 - '@thumbmarkjs/thumbmarkjs@0.14.8': {} - '@tokenizer/token@0.3.0': {} '@tufjs/canonical-json@2.0.0': {} @@ -7905,6 +7903,8 @@ snapshots: transitivePeerDependencies: - ts-node + '@versini/ui-fingerprint@1.0.1': {} + '@versini/ui-hooks@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 From 8e528a6db0a38f0f89dcd3a0ae068612ba1ce1fd Mon Sep 17 00:00:00 2001 From: Arno V Date: Thu, 18 Jul 2024 11:35:17 -0400 Subject: [PATCH 2/3] Update bundlesize.config.js --- packages/auth-provider/bundlesize.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth-provider/bundlesize.config.js b/packages/auth-provider/bundlesize.config.js index 6c95f15..6cf9688 100644 --- a/packages/auth-provider/bundlesize.config.js +++ b/packages/auth-provider/bundlesize.config.js @@ -9,7 +9,7 @@ export default { */ { path: "dist/index.js", - limit: "21 kb", + limit: "18 kb", }, ], }; From 6b5e00f9e48848a122c728004b2e40a55079d18d Mon Sep 17 00:00:00 2001 From: Arno V Date: Thu, 18 Jul 2024 11:42:19 -0400 Subject: [PATCH 3/3] cleanup useEffect to prevent potential memory leaks --- .../src/components/AuthProvider/AuthProvider.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx b/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx index ec737e5..0ea120b 100644 --- a/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx +++ b/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx @@ -72,7 +72,7 @@ export const AuthProvider = ({ }); const logger = useCallback( - (...args: any[]) => { + (...args: unknown[]) => { if (debug) { console.info(`==> [Auth ${Date.now()}]: `, ...args); } @@ -132,10 +132,17 @@ export const AuthProvider = ({ * This effect is responsible to set the fingerprintRef value when the * component is first loaded. */ + + // biome-ignore lint/correctness/useExhaustiveDependencies: logger is stable useEffect(() => { (async () => { + logger("useEffect: setting the fingerprint"); fingerprintRef.current = await getCustomFingerprint(); })(); + return () => { + logger("useEffect: cleaning up the fingerprint"); + fingerprintRef.current = ""; + }; }, []); /**