Skip to content

Commit

Permalink
🐛 (SIWE): handle failed SIWE verification with nonce refetch
Browse files Browse the repository at this point in the history
  • Loading branch information
nickadamson committed Jan 17, 2024
1 parent d94bae4 commit 52c202e
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-cobras-approve.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 4 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions src/context/SIWEProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -72,7 +72,7 @@ export function SIWEProvider({ onSignIn, onSignOut, children }: SIWEProps) {
const SIWEConfig = useMemo(() => {
return getSIWEConfig({
authClient,
queryClient,
wagmiQueryClient,
nonceQuery,
authenticateQuery,
sessionQuery,
Expand All @@ -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,
Expand Down
116 changes: 70 additions & 46 deletions src/utils/siwe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const createSIWEMessage: SIWEConfig['createMessage'] = ({
*/
interface GetSIWEConfigProps {
authClient: PromiseClient<typeof Auth>;
queryClient: QueryClient;
wagmiQueryClient: QueryClient;
nonceQuery: UseQueryResult<NonceText>;
authenticateQuery: UseQueryResult<H160>;
sessionQuery: UseQueryResult<SiweSession>;
Expand All @@ -51,7 +51,7 @@ interface GetSIWEConfigProps {
*/
export const getSIWEConfig = ({
authClient,
queryClient,
wagmiQueryClient,
nonceQuery,
authenticateQuery,
sessionQuery,
Expand All @@ -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;
Expand Down

0 comments on commit 52c202e

Please sign in to comment.