From 52c202ee7588481bbe13cb077d267f9e22a4c194 Mon Sep 17 00:00:00 2001 From: Nick Adamson Date: Tue, 16 Jan 2024 22:22:30 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20(SIWE):=20handle=20failed=20SIWE?= =?UTF-8?q?=20verification=20with=20nonce=20refetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/many-cobras-approve.md | 5 ++ package.json | 2 +- pnpm-lock.yaml | 11 ++- src/context/SIWEProvider.tsx | 10 +-- src/utils/siwe.ts | 116 ++++++++++++++++++------------ 5 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 .changeset/many-cobras-approve.md diff --git a/.changeset/many-cobras-approve.md b/.changeset/many-cobras-approve.md new file mode 100644 index 0000000..91ba08b --- /dev/null +++ b/.changeset/many-cobras-approve.md @@ -0,0 +1,5 @@ +--- +"@valorem-labs-inc/react-hooks": patch +--- + +use wagmi's QueryClient to force ConnectKit to refetch nonce after a `verify` post fails diff --git a/package.json b/package.json index ffbadbb..637b8e4 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@connectrpc/connect-query": "0.5.3", "@connectrpc/connect-web": "^1.2.0", "@tanstack/react-query": "^4.36.1", - "@valorem-labs-inc/sdk": "^0.0.11", + "@valorem-labs-inc/sdk": "^0.0.12-alpha.2", "@wagmi/core": "^1.4.13", "abitype": "0.8.7", "connectkit": "^1.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 826ad33..75a4163 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,8 +29,8 @@ dependencies: specifier: ^4.36.1 version: 4.36.1(react-dom@18.2.0)(react@18.2.0) '@valorem-labs-inc/sdk': - specifier: ^0.0.11 - version: 0.0.11(@bufbuild/protobuf@1.6.0)(@connectrpc/connect@1.2.0)(@wagmi/core@1.4.13)(typescript@5.3.3)(viem@1.21.4) + specifier: ^0.0.12-alpha.2 + version: 0.0.12-alpha.2(@bufbuild/protobuf@1.6.0)(@connectrpc/connect@1.2.0)(@wagmi/core@1.4.13)(typescript@5.3.3)(viem@1.21.4) typescript: specifier: ^5.3.0 version: 5.3.3 @@ -3175,8 +3175,8 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@valorem-labs-inc/sdk@0.0.11(@bufbuild/protobuf@1.6.0)(@connectrpc/connect@1.2.0)(@wagmi/core@1.4.13)(typescript@5.3.3)(viem@1.21.4): - resolution: {integrity: sha512-8oDhDwWE7DHdoWJ8kxg58aEj5LJzBwFWuBV7ll1ztrGrjqI1DXsZ7MLxHxISU1jiJSFD6hY6HCV3rQs1CS9caA==, tarball: https://npm.pkg.github.com/download/@valorem-labs-inc/sdk/0.0.11/be13ff988dcea6f926da89c23c7f508a6869f338} + /@valorem-labs-inc/sdk@0.0.12-alpha.2(@bufbuild/protobuf@1.6.0)(@connectrpc/connect@1.2.0)(@wagmi/core@1.4.13)(typescript@5.3.3)(viem@1.21.4): + resolution: {integrity: sha512-MgWGjxhz8AGvYZwQ4Bfn0Hys62O3LCOf0A+wXDwN6rg59BbJNvm/WGURtEGo1pvTchw59R1miig3baLPOLbuww==, tarball: https://npm.pkg.github.com/download/@valorem-labs-inc/sdk/0.0.12-alpha.2/45f0ca481ea5e02a4e95bec2a81a50e090d08551} engines: {node: '>=18'} peerDependencies: '@bufbuild/protobuf': ^1.6.0 @@ -3184,9 +3184,6 @@ packages: '@wagmi/core': ^1.4.13 typescript: ^5.3.0 viem: ^1.21.4 - peerDependenciesMeta: - typescript: - optional: true dependencies: '@bufbuild/protobuf': 1.6.0 '@connectrpc/connect': 1.2.0(@bufbuild/protobuf@1.6.0) diff --git a/src/context/SIWEProvider.tsx b/src/context/SIWEProvider.tsx index bb2ab88..4503033 100644 --- a/src/context/SIWEProvider.tsx +++ b/src/context/SIWEProvider.tsx @@ -1,7 +1,7 @@ import { SIWEProvider as Provider, type SIWESession } from 'connectkit'; import { type PropsWithChildren, useMemo } from 'react'; -import { useAccount } from 'wagmi'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useAccount, useQueryClient } from 'wagmi'; +import { useQuery } from '@tanstack/react-query'; import { getSIWEConfig } from '../utils/siwe'; import { usePromiseClient } from '../hooks/usePromiseClient'; import { @@ -43,7 +43,7 @@ export function SIWEProvider({ onSignIn, onSignOut, children }: SIWEProps) { const { address } = useAccount(); const logger = useLogger(); const authClient = usePromiseClient(Auth); - const queryClient = useQueryClient(); + const wagmiQueryClient = useQueryClient(); // Queries for authentication, nonce, session, and sign-out. const authenticateQuery = useQuery({ @@ -72,7 +72,7 @@ export function SIWEProvider({ onSignIn, onSignOut, children }: SIWEProps) { const SIWEConfig = useMemo(() => { return getSIWEConfig({ authClient, - queryClient, + wagmiQueryClient, nonceQuery, authenticateQuery, sessionQuery, @@ -83,7 +83,7 @@ export function SIWEProvider({ onSignIn, onSignOut, children }: SIWEProps) { // eslint-disable-next-line react-hooks/exhaustive-deps -- don't want to recompute when logger changes }, [ authClient, - queryClient, + wagmiQueryClient, nonceQuery, authenticateQuery, sessionQuery, diff --git a/src/utils/siwe.ts b/src/utils/siwe.ts index a19e6ae..60e1dfc 100644 --- a/src/utils/siwe.ts +++ b/src/utils/siwe.ts @@ -32,7 +32,7 @@ const createSIWEMessage: SIWEConfig['createMessage'] = ({ */ interface GetSIWEConfigProps { authClient: PromiseClient; - queryClient: QueryClient; + wagmiQueryClient: QueryClient; nonceQuery: UseQueryResult; authenticateQuery: UseQueryResult; sessionQuery: UseQueryResult; @@ -51,7 +51,7 @@ interface GetSIWEConfigProps { */ export const getSIWEConfig = ({ authClient, - queryClient, + wagmiQueryClient, nonceQuery, authenticateQuery, sessionQuery, @@ -65,76 +65,100 @@ export const getSIWEConfig = ({ // Returns a promise which, upon resolution, returns the nonce. async getNonce() { - logger.debug('Fetching nonce...'); + logger.debug('SIWE: Fetching nonce...'); const { data } = await nonceQuery.refetch(); if (data?.nonce === undefined) throw new Error('Could not fetch nonce'); - logger.debug(`Current nonce: ${data.nonce}`); + logger.debug(`SIWE: Current nonce: ${data.nonce}`); return data.nonce; }, // Returns a promise which, upon resolution, verifies the contents of the SIWE message. async verifyMessage({ message, signature }) { - logger.debug('Verifying message...'); - const res = await authClient.verify({ - body: JSON.stringify({ message, signature }), - }); - // verify address returned by Trade API matches current address - const verifiedAddress = fromH160ToAddress(res).toLowerCase(); - logger.debug('Message verified successfully'); - return verifiedAddress === address?.toLowerCase(); + logger.debug('SIWE: Verifying message...'); + + let verified = false; + try { + const res = await authClient.verify({ + body: JSON.stringify({ message, signature }), + }); + // verify address returned by Trade API matches current address + const verifiedAddress = fromH160ToAddress(res).toLowerCase(); + logger.info('SIWE: Signed in'); + verified = verifiedAddress === address?.toLowerCase(); + } catch (error) { + logger.error('SIWE: Error verifying message', { error }); + } + + if (!verified) { + logger.warn('SIWE: Fetching new nonce after failed verification...'); + await wagmiQueryClient.refetchQueries(['ckSiweNonce']); + } + + return verified; }, // Returns a promise which, upon resolution and disconnect/reconnect of the // client terminates the SIWE session. async signOut() { - logger.debug('Signing out...'); + logger.debug('SIWE: Signing out...'); try { await signOutQuery.refetch(); - logger.info('Signed out'); + logger.info('SIWE: Signed out'); return true; } catch (error) { - logger.error('Error signing out'); + logger.error('SIWE: Error signing out', { error }); return false; } }, // Returns a promise which, upon await, gets details about the current session. async getSession() { - logger.debug('Getting session...'); + logger.debug('SIWE: Getting session...'); + try { + // check auth endpoint to ensure session is valid + const { data: authData, error: authError } = + await authenticateQuery.refetch({}); + if (authData === undefined || authError !== null) { + logger.debug('SIWE: Could not get auth data', { authError }); + return null; + } + const authorizedAddress = fromH160ToAddress(authData); + if (authorizedAddress.toLowerCase() !== address?.toLowerCase()) { + logger.error( + 'SIWE: Authorized address does not match connected address', + ); + return null; + } + logger.debug( + 'SIWE: Authorized address matches connected address. Now checking /session endpoint.', + ); - // check auth endpoint to ensure session is valid - const { data: authData } = await authenticateQuery.refetch({}); - if (authData === undefined) { - logger.warn('Could not get auth data'); - return null; - } - const authorizedAddress = fromH160ToAddress(authData); - if (authorizedAddress.toLowerCase() !== address?.toLowerCase()) { - logger.error('Authorized address does not match connected address'); - return null; - } + // get session data + const { data: sessionData, error: sessionError } = + await sessionQuery.refetch(); + if ( + !sessionData?.address || + !sessionData.chainId || + sessionError !== null + ) { + logger.debug('SIWE: No session data found', { sessionError }); + return null; + } + const sessionAddress = fromH160ToAddress(sessionData.address); + if (sessionAddress.toLowerCase() === address.toLowerCase()) { + logger.debug('SIWE: Session is valid'); + return { + address: sessionAddress, + chainId: Number(fromH256(sessionData.chainId).toString()), + }; + } - // get session data - const { data: sessionData } = await sessionQuery.refetch(); - if (!sessionData?.address || !sessionData.chainId) { - logger.warn('No session data found'); + logger.error('SIWE: Auth route does not match session data'); + return null; + } catch (error) { + logger.error('SIWE: Error getting session', { error }); return null; } - const sessionAddress = fromH160ToAddress(sessionData.address); - if (sessionAddress.toLowerCase() === address.toLowerCase()) { - logger.debug('Session is valid'); - queryClient.setQueryData( - ['valorem.trade.v1.Auth', 'signed-out'], - false, - ); - return { - address: sessionAddress, - chainId: Number(fromH256(sessionData.chainId).toString()), - }; - } - - logger.error('Auth route does not match session data'); - return null; }, }; return config;