From 39a271613155cee2036c930c1e10d7a0f66fe839 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Fri, 20 Oct 2023 14:00:50 -0500 Subject: [PATCH 1/4] fix: safe auth --- .../src/components/OnboardingAuthProvider.tsx | 86 ++++++++++++++++-- .../controllers/auth/authenticateWallet.ts | 91 ++++++++++++++++++- .../src/utils/networks/networkSwitch.ts | 2 +- 3 files changed, 167 insertions(+), 12 deletions(-) diff --git a/centrifuge-app/src/components/OnboardingAuthProvider.tsx b/centrifuge-app/src/components/OnboardingAuthProvider.tsx index 53cfb23100..92fd8a8a1b 100644 --- a/centrifuge-app/src/components/OnboardingAuthProvider.tsx +++ b/centrifuge-app/src/components/OnboardingAuthProvider.tsx @@ -2,6 +2,8 @@ import Centrifuge from '@centrifuge/centrifuge-js' import { useCentrifuge, useCentrifugeUtils, useEvmProvider, useWallet } from '@centrifuge/centrifuge-react' import { encodeAddress } from '@polkadot/util-crypto' import { Wallet } from '@subwallet/wallet-connect/types' +import { BigNumber, ethers } from 'ethers' +import { hashMessage } from 'ethers/lib/utils' import * as React from 'react' import { useMutation, useQuery } from 'react-query' @@ -222,20 +224,41 @@ Nonce: ${nonce} Issued At: ${new Date().toISOString()}` const signedMessage = await signer?.signMessage(message) - const tokenRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/authenticateWallet`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ + + let body + + if (signedMessage === '0x') { + const messageHash = hashMessage(message) + + const isValid = await isValidSignature(signer, address, messageHash, evmChainId || 1) + + if (isValid) { + body = JSON.stringify({ + safeAddress: address, + messageHash, + evmChainId, + }) + } else { + throw new Error('Invalid signature') + } + } else { + body = JSON.stringify({ message, signature: signedMessage, address, nonce, network: isEvmOnSubstrate ? 'evmOnSubstrate' : 'evm', chainId: evmChainId || 1, - }), + }) + } + + const tokenRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/authenticateWallet`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body, }) if (tokenRes.status !== 200) { throw new Error('Failed to authenticate wallet') @@ -249,3 +272,50 @@ Issued At: ${new Date().toISOString()}` ) } } + +const isValidSignature = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => { + const MAGIC_VALUE_BYTES = '0x20c13b0b' + + const safeContract = new ethers.Contract( + safeAddress, + [ + 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', + 'function getMessageHash(bytes memory message) public view returns (bytes32)', + 'function getThreshold() public view returns (uint256)', + ], + provider + ) + + const safeMessageHash = await safeContract.getMessageHash(messageHash) + + const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) + + if (!safeMessage) { + throw new Error('Unable to fetch SafeMessage') + } + + const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() + + if (!threshold || threshold > safeMessage.confirmations.length) { + throw new Error('Threshold has not been met') + } + + const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) + + return response === MAGIC_VALUE_BYTES +} + +const TX_SERVICE_URLS: Record = { + '1': 'https://safe-transaction-mainnet.safe.global/api', + '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', +} + +const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { + const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] + + const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { + headers: { 'Content-Type': 'application/json' }, + }) + + return response.json() +} diff --git a/onboarding-api/src/controllers/auth/authenticateWallet.ts b/onboarding-api/src/controllers/auth/authenticateWallet.ts index 34ec8c1169..ccb77ad9a8 100644 --- a/onboarding-api/src/controllers/auth/authenticateWallet.ts +++ b/onboarding-api/src/controllers/auth/authenticateWallet.ts @@ -1,6 +1,9 @@ +import { InfuraProvider } from '@ethersproject/providers' import { isAddress } from '@polkadot/util-crypto' +import { BigNumber, ethers } from 'ethers' import { Request, Response } from 'express' import * as jwt from 'jsonwebtoken' +import fetch from 'node-fetch' import { InferType, number, object, string, StringSchema } from 'yup' import { SupportedNetworks } from '../../database' import { reportHttpError } from '../../utils/httpError' @@ -33,13 +36,36 @@ const verifyWalletInput = object({ chainId: number().required(), }) +const verifySafeInput = object({ + messageHash: string().required(), + safeAddress: string() + .required() + .test({ + name: 'is-address', + test(value, ctx) { + if (isAddress(value)) return true + return ctx.createError({ message: 'Invalid address', path: ctx.path }) + }, + }), + evmChainId: number().required(), +}) + export const authenticateWalletController = async ( - req: Request<{}, {}, InferType>, + req: Request<{}, {}, InferType>, res: Response ) => { try { - await validateInput(req.body, verifyWalletInput) - const payload = await new NetworkSwitch(req.body.network).verifiyWallet(req, res) + let payload + + if ('safeAddress' in req.body) { + await validateInput(req.body, verifySafeInput) + + const provider = new InfuraProvider(req.body.evmChainId, process.env.INFURA_KEY) + payload = await verifySafeWallet(provider, req.body.safeAddress, req.body.messageHash, req.body.evmChainId) + } else { + await validateInput(req.body, verifyWalletInput) + payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res) + } const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '8h', @@ -51,3 +77,62 @@ export const authenticateWalletController = async ( return res.status(error.code).send({ error: error.message, e }) } } + +const verifySafeWallet = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => { + try { + const MAGIC_VALUE_BYTES = '0x20c13b0b' + + const safeContract = new ethers.Contract( + safeAddress, + [ + 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', + 'function getMessageHash(bytes memory message) public view returns (bytes32)', + 'function getThreshold() public view returns (uint256)', + ], + provider + ) + + const safeMessageHash = await safeContract.getMessageHash(messageHash) + + const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) + + if (!safeMessage) { + throw new Error('Unable to fetch SafeMessage') + } + + const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() + + if (!threshold || threshold > safeMessage.confirmations.length) { + throw new Error('Threshold has not been met') + } + + const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) + + if (response === MAGIC_VALUE_BYTES) { + return { + address: safeAddress, + chainId: evmChainId, + network: 'evm', + } + } + + throw new Error('Invalid signature') + } catch { + throw new Error('Something went wrong') + } +} + +const TX_SERVICE_URLS: Record = { + '1': 'https://safe-transaction-mainnet.safe.global/api', + '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', +} + +const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { + const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] + + const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { + headers: { 'Content-Type': 'application/json' }, + }) + + return response.json() +} diff --git a/onboarding-api/src/utils/networks/networkSwitch.ts b/onboarding-api/src/utils/networks/networkSwitch.ts index 73a9d14976..19fcf6e146 100644 --- a/onboarding-api/src/utils/networks/networkSwitch.ts +++ b/onboarding-api/src/utils/networks/networkSwitch.ts @@ -18,7 +18,7 @@ export class NetworkSwitch { this.network = network } - verifiyWallet = (req: Request, res: Response) => { + verifyWallet = (req: Request, res: Response) => { if (this.network === 'substrate') { return verifySubstrateWallet(req, res) } else if (this.network === 'evm' || this.network === 'evmOnSubstrate') { From 1765bc950aa8d607246930dcc4a50c09aa1dfdab Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 23 Oct 2023 14:12:14 -0500 Subject: [PATCH 2/4] refactor, use nonce --- .../src/components/OnboardingAuthProvider.tsx | 2 + .../controllers/auth/authenticateWallet.ts | 76 +------------------ onboarding-api/src/utils/networks/evm.ts | 71 +++++++++++++++++ .../src/utils/networks/networkSwitch.ts | 8 +- 4 files changed, 82 insertions(+), 75 deletions(-) diff --git a/centrifuge-app/src/components/OnboardingAuthProvider.tsx b/centrifuge-app/src/components/OnboardingAuthProvider.tsx index 92fd8a8a1b..c287133d85 100644 --- a/centrifuge-app/src/components/OnboardingAuthProvider.tsx +++ b/centrifuge-app/src/components/OnboardingAuthProvider.tsx @@ -237,6 +237,8 @@ Issued At: ${new Date().toISOString()}` safeAddress: address, messageHash, evmChainId, + network: 'evmOnSafe', + nonce, }) } else { throw new Error('Invalid signature') diff --git a/onboarding-api/src/controllers/auth/authenticateWallet.ts b/onboarding-api/src/controllers/auth/authenticateWallet.ts index ccb77ad9a8..79bd3811f1 100644 --- a/onboarding-api/src/controllers/auth/authenticateWallet.ts +++ b/onboarding-api/src/controllers/auth/authenticateWallet.ts @@ -1,9 +1,6 @@ -import { InfuraProvider } from '@ethersproject/providers' import { isAddress } from '@polkadot/util-crypto' -import { BigNumber, ethers } from 'ethers' import { Request, Response } from 'express' import * as jwt from 'jsonwebtoken' -import fetch from 'node-fetch' import { InferType, number, object, string, StringSchema } from 'yup' import { SupportedNetworks } from '../../database' import { reportHttpError } from '../../utils/httpError' @@ -48,6 +45,8 @@ const verifySafeInput = object({ }, }), evmChainId: number().required(), + network: string().oneOf(['evmOnSafe']).required() as StringSchema<'evmOnSafe'>, + nonce: string().required(), }) export const authenticateWalletController = async ( @@ -55,17 +54,9 @@ export const authenticateWalletController = async ( res: Response ) => { try { - let payload - - if ('safeAddress' in req.body) { - await validateInput(req.body, verifySafeInput) + await validateInput(req.body, req.body.network === 'evmOnSafe' ? verifySafeInput : verifyWalletInput) - const provider = new InfuraProvider(req.body.evmChainId, process.env.INFURA_KEY) - payload = await verifySafeWallet(provider, req.body.safeAddress, req.body.messageHash, req.body.evmChainId) - } else { - await validateInput(req.body, verifyWalletInput) - payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res) - } + const payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res) const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '8h', @@ -77,62 +68,3 @@ export const authenticateWalletController = async ( return res.status(error.code).send({ error: error.message, e }) } } - -const verifySafeWallet = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => { - try { - const MAGIC_VALUE_BYTES = '0x20c13b0b' - - const safeContract = new ethers.Contract( - safeAddress, - [ - 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', - 'function getMessageHash(bytes memory message) public view returns (bytes32)', - 'function getThreshold() public view returns (uint256)', - ], - provider - ) - - const safeMessageHash = await safeContract.getMessageHash(messageHash) - - const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) - - if (!safeMessage) { - throw new Error('Unable to fetch SafeMessage') - } - - const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() - - if (!threshold || threshold > safeMessage.confirmations.length) { - throw new Error('Threshold has not been met') - } - - const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) - - if (response === MAGIC_VALUE_BYTES) { - return { - address: safeAddress, - chainId: evmChainId, - network: 'evm', - } - } - - throw new Error('Invalid signature') - } catch { - throw new Error('Something went wrong') - } -} - -const TX_SERVICE_URLS: Record = { - '1': 'https://safe-transaction-mainnet.safe.global/api', - '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', -} - -const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { - const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] - - const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { - headers: { 'Content-Type': 'application/json' }, - }) - - return response.json() -} diff --git a/onboarding-api/src/utils/networks/evm.ts b/onboarding-api/src/utils/networks/evm.ts index 7463fc7b60..72c027f914 100644 --- a/onboarding-api/src/utils/networks/evm.ts +++ b/onboarding-api/src/utils/networks/evm.ts @@ -1,7 +1,9 @@ import { isAddress } from '@ethersproject/address' import { Contract } from '@ethersproject/contracts' import { InfuraProvider, JsonRpcProvider, Provider } from '@ethersproject/providers' +import { BigNumber, ethers } from 'ethers' import { Request, Response } from 'express' +import fetch from 'node-fetch' import { SiweMessage } from 'siwe' import { InferType } from 'yup' import { signAndSendDocumentsInput } from '../../controllers/emails/signAndSendDocuments' @@ -66,3 +68,72 @@ export async function verifyEvmWallet(req: Request, res: Response): Promise { + const { safeAddress, messageHash, evmChainId, nonce } = req.body + const MAGIC_VALUE_BYTES = '0x20c13b0b' + + if (!isAddress(safeAddress)) { + throw new HttpError(400, 'Invalid address') + } + + const cookieNonce = req.signedCookies[`onboarding-auth-${safeAddress.toLowerCase()}`] + + if (!cookieNonce || cookieNonce !== nonce) { + throw new HttpError(400, 'Invalid nonce') + } + + const provider = new InfuraProvider(req.body.evmChainId, process.env.INFURA_KEY) + const safeContract = new ethers.Contract( + safeAddress, + [ + 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', + 'function getMessageHash(bytes memory message) public view returns (bytes32)', + 'function getThreshold() public view returns (uint256)', + ], + provider + ) + + const safeMessageHash = await safeContract.getMessageHash(messageHash) + + const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) + + if (!safeMessage) { + throw new HttpError(400, 'Unable to fetch SafeMessage') + } + + const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() + + if (!threshold || threshold > safeMessage.confirmations.length) { + throw new HttpError(400, 'Threshold has not been met') + } + + const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) + + if (response === MAGIC_VALUE_BYTES) { + res.clearCookie(`onboarding-auth-${safeAddress.toLowerCase()}`) + + return { + address: safeAddress, + chainId: evmChainId, + network: 'evm', + } + } + + throw new HttpError(400, 'Invalid signature') +} + +const TX_SERVICE_URLS: Record = { + '1': 'https://safe-transaction-mainnet.safe.global/api', + '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', +} + +const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { + const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] + + const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { + headers: { 'Content-Type': 'application/json' }, + }) + + return response.json() +} diff --git a/onboarding-api/src/utils/networks/networkSwitch.ts b/onboarding-api/src/utils/networks/networkSwitch.ts index 19fcf6e146..334d9f2362 100644 --- a/onboarding-api/src/utils/networks/networkSwitch.ts +++ b/onboarding-api/src/utils/networks/networkSwitch.ts @@ -9,18 +9,20 @@ import { validateSubstrateRemark, verifySubstrateWallet, } from './centrifuge' -import { validateEvmRemark, verifyEvmWallet } from './evm' +import { validateEvmRemark, verifyEvmWallet, verifySafeWallet } from './evm' import { addTinlakeInvestorToMemberList, getTinlakePoolById } from './tinlake' export class NetworkSwitch { - network: SupportedNetworks - constructor(network: SupportedNetworks = 'substrate') { + network: SupportedNetworks | 'evmOnSafe' + constructor(network: SupportedNetworks | 'evmOnSafe' = 'substrate') { this.network = network } verifyWallet = (req: Request, res: Response) => { if (this.network === 'substrate') { return verifySubstrateWallet(req, res) + } else if (this.network === 'evmOnSafe') { + return verifySafeWallet(req, res) } else if (this.network === 'evm' || this.network === 'evmOnSubstrate') { return verifyEvmWallet(req, res) } From c5c63ff06abb707db3abe73457d416327949dc1f Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 23 Oct 2023 21:12:42 -0500 Subject: [PATCH 3/4] move clearCookie logic --- onboarding-api/src/utils/networks/evm.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/onboarding-api/src/utils/networks/evm.ts b/onboarding-api/src/utils/networks/evm.ts index 72c027f914..23475a905f 100644 --- a/onboarding-api/src/utils/networks/evm.ts +++ b/onboarding-api/src/utils/networks/evm.ts @@ -109,10 +109,9 @@ export const verifySafeWallet = async (req: Request, res: Response) => { } const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) + res.clearCookie(`onboarding-auth-${safeAddress.toLowerCase()}`) if (response === MAGIC_VALUE_BYTES) { - res.clearCookie(`onboarding-auth-${safeAddress.toLowerCase()}`) - return { address: safeAddress, chainId: evmChainId, From ee8ec50632c9f9435f9de06f4a879f097706679c Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Mon, 23 Oct 2023 22:07:12 -0500 Subject: [PATCH 4/4] update goerli urls --- centrifuge-app/src/components/OnboardingAuthProvider.tsx | 2 +- onboarding-api/src/utils/networks/evm.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/centrifuge-app/src/components/OnboardingAuthProvider.tsx b/centrifuge-app/src/components/OnboardingAuthProvider.tsx index c287133d85..307ddb15fc 100644 --- a/centrifuge-app/src/components/OnboardingAuthProvider.tsx +++ b/centrifuge-app/src/components/OnboardingAuthProvider.tsx @@ -309,7 +309,7 @@ const isValidSignature = async (provider: any, safeAddress: string, messageHash: const TX_SERVICE_URLS: Record = { '1': 'https://safe-transaction-mainnet.safe.global/api', - '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', + '5': 'https://safe-transaction-goerli.safe.global/api', } const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { diff --git a/onboarding-api/src/utils/networks/evm.ts b/onboarding-api/src/utils/networks/evm.ts index 23475a905f..02dc376266 100644 --- a/onboarding-api/src/utils/networks/evm.ts +++ b/onboarding-api/src/utils/networks/evm.ts @@ -124,7 +124,7 @@ export const verifySafeWallet = async (req: Request, res: Response) => { const TX_SERVICE_URLS: Record = { '1': 'https://safe-transaction-mainnet.safe.global/api', - '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', + '5': 'https://safe-transaction-goerli.safe.global/api', } const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => {