Skip to content

Commit

Permalink
fix: safe auth (#1651)
Browse files Browse the repository at this point in the history
  • Loading branch information
JP authored Nov 1, 2023
1 parent 70c8e32 commit 8da3b5f
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 15 deletions.
88 changes: 80 additions & 8 deletions centrifuge-app/src/components/OnboardingAuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -222,20 +224,43 @@ 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,
network: 'evmOnSafe',
nonce,
})
} 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')
Expand All @@ -249,3 +274,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<string, string> = {
'1': 'https://safe-transaction-mainnet.safe.global/api',
'5': 'https://safe-transaction-goerli.safe.global/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()
}
23 changes: 20 additions & 3 deletions onboarding-api/src/controllers/auth/authenticateWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,30 @@ 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(),
network: string().oneOf(['evmOnSafe']).required() as StringSchema<'evmOnSafe'>,
nonce: string().required(),
})

export const authenticateWalletController = async (
req: Request<{}, {}, InferType<typeof verifyWalletInput>>,
req: Request<{}, {}, InferType<typeof verifyWalletInput | typeof verifySafeInput>>,
res: Response
) => {
try {
await validateInput(req.body, verifyWalletInput)
const payload = await new NetworkSwitch(req.body.network).verifiyWallet(req, res)
await validateInput(req.body, req.body.network === 'evmOnSafe' ? verifySafeInput : verifyWalletInput)

const payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res)

const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '8h',
Expand Down
70 changes: 70 additions & 0 deletions onboarding-api/src/utils/networks/evm.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -66,3 +68,71 @@ export async function verifyEvmWallet(req: Request, res: Response): Promise<Requ
chainId,
}
}

export const verifySafeWallet = async (req: Request, res: Response) => {
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)
res.clearCookie(`onboarding-auth-${safeAddress.toLowerCase()}`)

if (response === MAGIC_VALUE_BYTES) {
return {
address: safeAddress,
chainId: evmChainId,
network: 'evm',
}
}

throw new HttpError(400, 'Invalid signature')
}

const TX_SERVICE_URLS: Record<string, string> = {
'1': 'https://safe-transaction-mainnet.safe.global/api',
'5': 'https://safe-transaction-goerli.safe.global/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()
}
10 changes: 6 additions & 4 deletions onboarding-api/src/utils/networks/networkSwitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

verifiyWallet = (req: Request, res: Response) => {
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)
}
Expand Down

0 comments on commit 8da3b5f

Please sign in to comment.