From 15ccf0d28b1e02877ac7696d202bcd535e1a0ebf Mon Sep 17 00:00:00 2001 From: Igor Shadurin Date: Sat, 25 May 2024 13:28:53 +0100 Subject: [PATCH 1/4] feat: added app existence method --- frame/src/api.ts | 6 ++- src/controllers/v1/app/exists-action.ts | 39 +++++++++++++++ src/controllers/v1/app/index.ts | 2 + .../v1/app/interface/IExistsResponse.ts | 4 ++ src/utils.ts | 4 +- src/utils/eth.ts | 3 +- test/controllers/app.test.ts | 47 +++++++++++++++++++ test/utils-test/eth.test.ts | 8 ++++ 8 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/controllers/v1/app/exists-action.ts create mode 100644 src/controllers/v1/app/interface/IExistsResponse.ts create mode 100644 test/utils-test/eth.test.ts diff --git a/frame/src/api.ts b/frame/src/api.ts index 6aa9770..449512b 100644 --- a/frame/src/api.ts +++ b/frame/src/api.ts @@ -50,8 +50,12 @@ export async function answerAuthRequest( return req.json() } +/** + * Get the auth data. + * @param messageBytesProof Message bytes proof + */ export async function getAuthData(messageBytesProof: string): Promise { - const url = getUrl(`v1/authorization/list`) + const url = getUrl(`v1/authorization/list?rand=${Math.random()}`) const req = await fetch(url, { method: 'POST', headers: { diff --git a/src/controllers/v1/app/exists-action.ts b/src/controllers/v1/app/exists-action.ts new file mode 100644 index 0000000..6990b36 --- /dev/null +++ b/src/controllers/v1/app/exists-action.ts @@ -0,0 +1,39 @@ +import { Request, Response, NextFunction } from 'express' +import { getAppBySignerAddress } from '../../../db/app' +import { is0xEthAddress, isEthAddress, prepareEthAddress } from '../../../utils/eth' +import { IExistsResponse } from './interface/IExistsResponse' + +export function getAppExistsParams(req: Request): { applicationAddress: string } { + const { applicationAddress } = req.query + + if (!applicationAddress) { + throw new Error('"applicationAddress" is required') + } + + if (!is0xEthAddress(applicationAddress) && !isEthAddress(applicationAddress)) { + throw new Error('Invalid "applicationAddress"') + } + + return { applicationAddress: prepareEthAddress(applicationAddress as string) } +} + +/** + * Checks if the app with the address already exists. + * @param req Request + * @param res Response + * @param next Next function + */ +export default async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const { applicationAddress } = getAppExistsParams(req) + + const isExists = Boolean(await getAppBySignerAddress(applicationAddress)) + + res.json({ + status: 'ok', + isExists, + }) + } catch (e) { + next(e) + } +} diff --git a/src/controllers/v1/app/index.ts b/src/controllers/v1/app/index.ts index 44750ea..5013cdc 100644 --- a/src/controllers/v1/app/index.ts +++ b/src/controllers/v1/app/index.ts @@ -1,11 +1,13 @@ import express from 'express' import createAction from './create-action' +import existsAction from './exists-action' import testFrameAction from './test-frame-action' import testWebhookAction from './test-webhook-action' import testWebhookFailAction from './test-webhook-fail-action' const router = express.Router() router.post('/create', createAction) +router.get('/exists', existsAction) router.get('/test-frame', testFrameAction) router.all('/test-webhook', testWebhookAction) router.all('/test-webhook-fail', testWebhookFailAction) diff --git a/src/controllers/v1/app/interface/IExistsResponse.ts b/src/controllers/v1/app/interface/IExistsResponse.ts new file mode 100644 index 0000000..be26512 --- /dev/null +++ b/src/controllers/v1/app/interface/IExistsResponse.ts @@ -0,0 +1,4 @@ +export interface IExistsResponse { + status: string + isExists: boolean +} diff --git a/src/utils.ts b/src/utils.ts index f148133..925450b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,10 +39,10 @@ export function assertStringLength(data: unknown, length: number): asserts data * * @param value Value to check */ -export function isHexString(value: string): boolean { +export function isHexString(value: unknown): value is string { const hexRegEx = /^[0-9A-Fa-f]*$/ - return hexRegEx.test(value) + return hexRegEx.test(value as string) } /** diff --git a/src/utils/eth.ts b/src/utils/eth.ts index 6056202..1b8c596 100644 --- a/src/utils/eth.ts +++ b/src/utils/eth.ts @@ -1,5 +1,6 @@ -import { ethers, isHexString } from 'ethers' +import { ethers } from 'ethers' import { Bytes, is0xHexString } from './bytes' +import { isHexString } from '../utils' export type EthAddress = Bytes<20> export const ETH_ADDR_HEX_LENGTH = 40 diff --git a/test/controllers/app.test.ts b/test/controllers/app.test.ts index fd072c5..4458c50 100644 --- a/test/controllers/app.test.ts +++ b/test/controllers/app.test.ts @@ -8,6 +8,7 @@ import { getAppByFid, getAppsCount } from '../../src/db/app' import { ICreateRequest } from '../../src/controllers/v1/app/interface/ICreateRequest' import { getConfigData, setConfigData } from '../../src/config' import { Wallet } from 'ethers' +import { prepareEthAddress } from '../../src/utils/eth' export interface InputMock { /** @@ -197,4 +198,50 @@ describe('App', () => { expect(data1).toEqual({ status: 'ok' }) expect(await getAppsCount()).toEqual(2) }) + + it('should check app existence', async () => { + const fid = 123 + const postData: ICreateRequest = { + frameUrlBytes: '0x123', + frameCallbackUrlBytes: '0x222', + frameSignerAddressBytes: '0x333', + } + const wallet = Wallet.createRandom() + const authorizedFrameUrl = 'https://auth-frame.com' + const frameUrl = 'https://example.com' + const callbackUrl = 'https://example.com/callback' + setConfigData({ + ...getConfigData(), + authorizedFrameUrl, + }) + mockInputData(fid, frameUrl, authorizedFrameUrl, [ + { + bytes: postData.frameUrlBytes, + input: frameUrl, + }, + { + bytes: postData.frameCallbackUrlBytes, + input: callbackUrl, + }, + { + bytes: postData.frameSignerAddressBytes, + input: wallet.address, + }, + ]) + + const data = (await supertestApp.post(`/v1/app/create`).send(postData)).body + expect(data).toEqual({ status: 'ok' }) + expect(await getAppByFid(fid)).toBeDefined() + expect(await getAppsCount()).toEqual(1) + + // check with the full address + const data1 = (await supertestApp.get(`/v1/app/exists?applicationAddress=${wallet.address}`).send()).body + expect(data1).toEqual({ status: 'ok', isExists: true }) + + // check with the prepared address + const data2 = ( + await supertestApp.get(`/v1/app/exists?applicationAddress=${prepareEthAddress(wallet.address)}`).send() + ).body + expect(data2).toEqual({ status: 'ok', isExists: true }) + }) }) diff --git a/test/utils-test/eth.test.ts b/test/utils-test/eth.test.ts new file mode 100644 index 0000000..473c4d1 --- /dev/null +++ b/test/utils-test/eth.test.ts @@ -0,0 +1,8 @@ +import { isEthAddress } from '../../src/utils/eth' + +describe('eth', () => { + it('isEthAddress', async () => { + expect(isEthAddress('0x123')).toBeFalsy() + expect(isEthAddress('1f1cf52bcb17acab0e3222d535a94495d656650e')).toBeTruthy() + }) +}) From 6920ffb04b60bdc45d9d22fae91b2b64fbd765fa Mon Sep 17 00:00:00 2001 From: Igor Shadurin Date: Mon, 27 May 2024 18:23:52 +0100 Subject: [PATCH 2/4] feat: added signature for incorrect answer --- src/controllers/v1/app/exists-action.ts | 4 +- .../v1/authorization/answer-action.ts | 15 ++++- .../v1/authorization/utils/error-message.ts | 4 ++ .../v1/authorization/utils/get-proof.ts | 6 +- .../utils/request-create-utils.ts | 4 +- src/controllers/v1/delegated-fs/utils/get.ts | 8 +-- src/service/delegated-fs/delegated-fs.ts | 22 ++++++- src/utils/eth.ts | 12 ++++ src/utils/http.ts | 26 ++++++-- test/controllers/authorization.test.ts | 63 ++++++++++++++++++- 10 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 src/controllers/v1/authorization/utils/error-message.ts diff --git a/src/controllers/v1/app/exists-action.ts b/src/controllers/v1/app/exists-action.ts index 6990b36..2035da3 100644 --- a/src/controllers/v1/app/exists-action.ts +++ b/src/controllers/v1/app/exists-action.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express' import { getAppBySignerAddress } from '../../../db/app' -import { is0xEthAddress, isEthAddress, prepareEthAddress } from '../../../utils/eth' +import { isAnyEthAddress, prepareEthAddress } from '../../../utils/eth' import { IExistsResponse } from './interface/IExistsResponse' export function getAppExistsParams(req: Request): { applicationAddress: string } { @@ -10,7 +10,7 @@ export function getAppExistsParams(req: Request): { applicationAddress: string } throw new Error('"applicationAddress" is required') } - if (!is0xEthAddress(applicationAddress) && !isEthAddress(applicationAddress)) { + if (!isAnyEthAddress(applicationAddress)) { throw new Error('Invalid "applicationAddress"') } diff --git a/src/controllers/v1/authorization/answer-action.ts b/src/controllers/v1/authorization/answer-action.ts index 076eb15..6b5867b 100644 --- a/src/controllers/v1/authorization/answer-action.ts +++ b/src/controllers/v1/authorization/answer-action.ts @@ -12,6 +12,7 @@ import { getRequestAnswerData } from './utils/request-create-utils' import { callbackFrameUrl, ICallbackFailRequest, ICallbackSuccessRequest } from '../../../utils/http' import { DelegatedFs } from '../../../service/delegated-fs/delegated-fs' import { Wallet } from 'ethers' +import { INVALID_CHALLENGE_ANSWER } from './utils/error-message' /** * Handles the answer of the user's answer to the challenge. Called from trusted Frame's service. @@ -36,17 +37,25 @@ export default async ( await updateAuthorizationRequestStatus(requestId, AuthorizationRequestStatus.ACCEPTED) } else { await updateAuthorizationRequestStatus(requestId, AuthorizationRequestStatus.INCORRECT) - const errorMessage = 'Invalid answer for the challenge' + const proof = await DelegatedFs.createDelegateSignature( + authRequest.user_main_address, + authRequest.user_delegated_address, + authRequest.app_signer_address, + new Wallet(signer), + INVALID_CHALLENGE_ANSWER, + ) const data: ICallbackFailRequest = { success: false, requestId, userMainAddress: authRequest.user_main_address, userDelegatedAddress: authRequest.user_delegated_address, applicationAddress: authRequest.app_signer_address, - errorMessage, + errorMessage: INVALID_CHALLENGE_ANSWER, + proof, } + await callbackFrameUrl(app.callback_url, data) - throw new Error(errorMessage) + throw new Error(INVALID_CHALLENGE_ANSWER) } const proof = await DelegatedFs.createDelegateSignature( diff --git a/src/controllers/v1/authorization/utils/error-message.ts b/src/controllers/v1/authorization/utils/error-message.ts new file mode 100644 index 0000000..f9ac2b5 --- /dev/null +++ b/src/controllers/v1/authorization/utils/error-message.ts @@ -0,0 +1,4 @@ +/** + * Error message for invalid challenge answer. + */ +export const INVALID_CHALLENGE_ANSWER = 'Invalid answer for the challenge' diff --git a/src/controllers/v1/authorization/utils/get-proof.ts b/src/controllers/v1/authorization/utils/get-proof.ts index 2977118..a306f78 100644 --- a/src/controllers/v1/authorization/utils/get-proof.ts +++ b/src/controllers/v1/authorization/utils/get-proof.ts @@ -1,15 +1,15 @@ import { IGetProofRequest } from '../interface/IGetProofRequest' import { Request } from 'express' -import { is0xEthAddress, prepareEthAddress } from '../../../../utils/eth' +import { isAnyEthAddress, prepareEthAddress } from '../../../../utils/eth' export function getGetProofParams(req: Request): IGetProofRequest { const { userAddress, applicationAddress } = req.query - if (!is0xEthAddress(userAddress)) { + if (!isAnyEthAddress(userAddress)) { throw new Error('Invalid "userAddress"') } - if (!is0xEthAddress(applicationAddress)) { + if (!isAnyEthAddress(applicationAddress)) { throw new Error('Invalid "applicationAddress"') } diff --git a/src/controllers/v1/authorization/utils/request-create-utils.ts b/src/controllers/v1/authorization/utils/request-create-utils.ts index d166f0d..3eaa4c5 100644 --- a/src/controllers/v1/authorization/utils/request-create-utils.ts +++ b/src/controllers/v1/authorization/utils/request-create-utils.ts @@ -4,7 +4,7 @@ import { isWithinMaxMinutes } from '../../../../utils/time' import { MAX_REQUESTS_TIME_MINUTES } from '../../app/utils/app-create-utils' import { getAppBySignerAddress, getAppByUrl, IApp } from '../../../../db/app' import { extractSignerAddress } from '../../../../utils/crypto' -import { is0xEthAddress, prepareEthAddress, prepareEthSignature } from '../../../../utils/eth' +import { isAnyEthAddress, prepareEthAddress, prepareEthSignature } from '../../../../utils/eth' import { IAnswerRequest } from '../interface/IAnswerRequest' import { AuthorizationRequestStatus, @@ -188,7 +188,7 @@ export function getRequestIsAuthorizedData(data: IIsAuthorizedRequest): { fid: n throw new Error('"appSignerAddress" is required') } - if (!is0xEthAddress(appSignerAddress)) { + if (!isAnyEthAddress(appSignerAddress)) { throw new Error('"appSignerAddress" is invalid ETH address') } diff --git a/src/controllers/v1/delegated-fs/utils/get.ts b/src/controllers/v1/delegated-fs/utils/get.ts index 570a956..23763a3 100644 --- a/src/controllers/v1/delegated-fs/utils/get.ts +++ b/src/controllers/v1/delegated-fs/utils/get.ts @@ -1,4 +1,4 @@ -import { is0xEthAddress } from '../../../../utils/eth' +import { isAnyEthAddress, prepareEthAddress } from '../../../../utils/eth' import { Request } from 'express' /** @@ -12,7 +12,7 @@ export function getUserAppParams(req: Request): { userAddress: string; applicati throw new Error('"userAddress" is required') } - if (!is0xEthAddress(userAddress)) { + if (!isAnyEthAddress(userAddress)) { throw new Error('Invalid "userAddress"') } @@ -20,9 +20,9 @@ export function getUserAppParams(req: Request): { userAddress: string; applicati throw new Error('"applicationAddress" is required') } - if (!is0xEthAddress(applicationAddress)) { + if (!isAnyEthAddress(applicationAddress)) { throw new Error('Invalid "applicationAddress"') } - return { userAddress, applicationAddress } + return { userAddress: prepareEthAddress(userAddress), applicationAddress: prepareEthAddress(applicationAddress) } } diff --git a/src/service/delegated-fs/delegated-fs.ts b/src/service/delegated-fs/delegated-fs.ts index 10c3d4e..0a20d26 100644 --- a/src/service/delegated-fs/delegated-fs.ts +++ b/src/service/delegated-fs/delegated-fs.ts @@ -49,8 +49,14 @@ export class DelegatedFs { * @param userAddress User address * @param userDelegatedAddress User delegated address * @param applicationAddress Application address + * @param errorText Error text */ - static getDelegatedText(userAddress: string, userDelegatedAddress: string, applicationAddress: string): string { + static getDelegatedText( + userAddress: string, + userDelegatedAddress: string, + applicationAddress: string, + errorText?: string, + ): string { userAddress = prepareEthAddress(userAddress) userDelegatedAddress = prepareEthAddress(userDelegatedAddress) applicationAddress = prepareEthAddress(applicationAddress) @@ -60,7 +66,13 @@ export class DelegatedFs { throw new Error('Delegated text addresses must be unique') } - return `${userAddress}${userDelegatedAddress}${applicationAddress}` + let text = `${userAddress}${userDelegatedAddress}${applicationAddress}` + + if (errorText) { + text = `ERROR${text}${errorText}` + } + + return text } /** @@ -69,15 +81,19 @@ export class DelegatedFs { * @param userDelegatedAddress User delegated address * @param applicationAddress Application address * @param signer Signer + * @param errorText Error text */ static async createDelegateSignature( userAddress: string, userDelegatedAddress: string, applicationAddress: string, signer: ISigner, + errorText?: string, ): Promise { return prepareEthSignature( - await signer.signMessage(DelegatedFs.getDelegatedText(userAddress, userDelegatedAddress, applicationAddress)), + await signer.signMessage( + DelegatedFs.getDelegatedText(userAddress, userDelegatedAddress, applicationAddress, errorText), + ), ) } diff --git a/src/utils/eth.ts b/src/utils/eth.ts index 1b8c596..6ebf285 100644 --- a/src/utils/eth.ts +++ b/src/utils/eth.ts @@ -51,6 +51,10 @@ export function prepareEthAddress(address: string): string { return address.toLowerCase() } +/** + * Prepares the Ethereum signature by removing the 0x prefix. + * @param signature Ethereum signature + */ export function prepareEthSignature(signature: string): string { if (is0xEthSignature(signature)) { signature = signature.replace(/^0x/, '') @@ -58,3 +62,11 @@ export function prepareEthSignature(signature: string): string { return signature } + +/** + * Checks if the value is any Ethereum address. + * @param value Value to check + */ +export function isAnyEthAddress(value: unknown): value is string { + return is0xEthAddress(value) || isEthAddress(value) +} diff --git a/src/utils/http.ts b/src/utils/http.ts index 079544f..167bc7e 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -1,16 +1,34 @@ export interface ICallbackResult { + /** + * Indicates whether the request was successful. + */ success: boolean + /** + * The ID of the request. + */ requestId: number + /** + * User main address in the form of hex without 0x prefix. + */ userMainAddress: string + /** + * User delegated address which created by 3rd party application for the user in the form of hex without 0x prefix. + */ userDelegatedAddress: string + /** + * Application address in the form of hex without 0x prefix. + */ applicationAddress: string -} - -export interface ICallbackSuccessRequest extends ICallbackResult { + /** + * Authentication service proof in the form of hex without 0x prefix. + */ proof: string } -export interface ICallbackFailRequest extends ICallbackResult { +export interface ICallbackSuccessRequest extends ICallbackResult {} + +export interface ICallbackFailRequest extends Omit { + success: false errorMessage: string } diff --git a/test/controllers/authorization.test.ts b/test/controllers/authorization.test.ts index 3b844e2..5c7ccde 100644 --- a/test/controllers/authorization.test.ts +++ b/test/controllers/authorization.test.ts @@ -9,7 +9,12 @@ import { prepareEthAddress, prepareEthSignature } from '../../src/utils/eth' import { ICreateResponse } from '../../src/controllers/v1/authorization/interface/ICreateResponse' import { IAnswerRequest } from '../../src/controllers/v1/authorization/interface/IAnswerRequest' import { IAnswerResponse } from '../../src/controllers/v1/authorization/interface/IAnswerResponse' -import { callbackFrameUrl, ICallbackResponse, ICallbackSuccessRequest } from '../../src/utils/http' +import { + callbackFrameUrl, + ICallbackFailRequest, + ICallbackResponse, + ICallbackSuccessRequest, +} from '../../src/utils/http' import { AuthorizationRequestStatus, getAuthorizationRequestById } from '../../src/db/authorization-request' import { extractSignerAddress, SIGNATURE_LENGTH_WITHOUT_0x } from '../../src/utils/crypto' import { IListRequest } from '../../src/controllers/v1/authorization/interface/IListRequest' @@ -20,6 +25,7 @@ import { insertMockedApp } from '../utils/app' import { DelegatedFs } from '../../src/service/delegated-fs/delegated-fs' import { Wallet } from 'ethers' import { getConfigData } from '../../src/config' +import { INVALID_CHALLENGE_ANSWER } from '../../src/controllers/v1/authorization/utils/error-message' const testDb = knex(configurations.development) @@ -235,7 +241,7 @@ describe('Authorization', () => { expect(userStatus3).toStrictEqual({ status: 'ok', isAuthorized: true }) }) - it('should get proof an authorization request', async () => { + it('should get proof of an authorization request', async () => { callbackFrameUrlMock.mockReset() mockCallbackFrameUrl({ success: true }) const { userMainWallet, userDelegatedWallet, appWallet, authServiceWallet } = await insertMockedApp(mockInteractor) @@ -314,4 +320,57 @@ describe('Authorization', () => { expect(authRequest?.proof_signature).toHaveLength(SIGNATURE_LENGTH_WITHOUT_0x) expect(authRequest?.proof_signature).toStrictEqual(callbackData.proof) }) + + it('should get signed callback of rejected authorization request', async () => { + callbackFrameUrlMock.mockReset() + mockCallbackFrameUrl({ success: true }) + const { userMainWallet, userDelegatedWallet, appWallet, authServiceWallet } = await insertMockedApp(mockInteractor) + const serviceSignature = await appWallet.signMessage(prepareEthAddress(userDelegatedWallet.address)) + const postData: ICreateAuthRequest = { + messageBytesProof: '0x123', + userDelegatedAddress: userDelegatedWallet.address, + serviceSignature, + } + const data = (await supertestApp.post(`/v1/authorization/create`).send(postData)).body as ICreateResponse + expect(data).toEqual({ status: 'ok', requestId: 1, answer: data.answer }) + + const answerData1: IAnswerRequest = { + requestId: data.requestId, + messageBytesProof: '0x123', + answer: data.answer + 1, + } + + expect(callbackFrameUrlMock).toHaveBeenCalledTimes(0) + const answer1 = (await supertestApp.post(`/v1/authorization/answer`).send(answerData1)).body as IAnswerResponse + expect(answer1).toStrictEqual({ status: 'error', message: 'Invalid answer for the challenge' }) + expect(callbackFrameUrlMock).toHaveBeenCalledTimes(1) + const callbackData = callbackFrameUrlMock.mock.calls[0][1] as ICallbackFailRequest + expect(callbackData.userMainAddress).toEqual(prepareEthAddress(userMainWallet.address)) + expect(callbackData.userDelegatedAddress).toEqual(prepareEthAddress(userDelegatedWallet.address)) + expect(callbackData.applicationAddress).toEqual(prepareEthAddress(appWallet.address)) + expect(callbackData.errorMessage).toEqual(INVALID_CHALLENGE_ANSWER) + expect(callbackData.success).toBeFalsy() + + expect( + extractSignerAddress( + DelegatedFs.getDelegatedText( + userMainWallet.address, + userDelegatedWallet.address, + appWallet.address, + INVALID_CHALLENGE_ANSWER, + ), + `0x${callbackData.proof}`, + ), + ).toStrictEqual(prepareEthAddress(authServiceWallet.address)) + + callbackFrameUrlMock.mockReset() + expect(callbackFrameUrlMock).toHaveBeenCalledTimes(0) + const answer2 = (await supertestApp.post(`/v1/authorization/answer`).send(answerData1)).body as IAnswerResponse + expect(answer2).toStrictEqual({ status: 'error', message: 'Authorization request not found' }) + expect(callbackFrameUrlMock).toHaveBeenCalledTimes(0) + + const authRequest = await getAuthorizationRequestById(data.requestId) + expect(authRequest?.status).toBe(AuthorizationRequestStatus.INCORRECT) + expect(authRequest?.proof_signature).toHaveLength(0) + }) }) From 92bdc0a2a7a2857149428813f8d51a8335c47bd5 Mon Sep 17 00:00:00 2001 From: Igor Shadurin Date: Tue, 28 May 2024 10:59:43 +0100 Subject: [PATCH 3/4] feat: callback on rejection with signature --- .../v1/authorization/reject-action.ts | 31 ++++++++++- .../v1/authorization/utils/error-message.ts | 4 ++ test/controllers/authorization.test.ts | 52 ++++++++++++++++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/controllers/v1/authorization/reject-action.ts b/src/controllers/v1/authorization/reject-action.ts index d93b563..761b7b3 100644 --- a/src/controllers/v1/authorization/reject-action.ts +++ b/src/controllers/v1/authorization/reject-action.ts @@ -8,6 +8,11 @@ import { import { getRequestListData } from './utils/request-create-utils' import { IGeneralResponse } from './interface/IGeneralResponse' import { IRejectRequest } from './interface/IRejectRequest' +import { DelegatedFs } from '../../../service/delegated-fs/delegated-fs' +import { Wallet } from 'ethers' +import { USER_REJECTED_REQUEST } from './utils/error-message' +import { callbackFrameUrl, ICallbackFailRequest } from '../../../utils/http' +import { getAppBySignerAddress } from '../../../db/app' export default async ( req: Request, @@ -15,7 +20,7 @@ export default async ( next: NextFunction, ): Promise => { try { - const { neynarApiKey, authorizedFrameUrl } = getConfigData() + const { neynarApiKey, authorizedFrameUrl, signer } = getConfigData() const { fid } = await getRequestListData(neynarApiKey, authorizedFrameUrl, req.body) const authRequest = await getActiveAuthorizationRequestByUser(fid) @@ -28,6 +33,30 @@ export default async ( } await updateAuthorizationRequestStatus(authRequest.id, AuthorizationRequestStatus.REJECTED) + const proof = await DelegatedFs.createDelegateSignature( + authRequest.user_main_address, + authRequest.user_delegated_address, + authRequest.app_signer_address, + new Wallet(signer), + USER_REJECTED_REQUEST, + ) + const data: ICallbackFailRequest = { + success: false, + requestId: authRequest.id, + userMainAddress: authRequest.user_main_address, + userDelegatedAddress: authRequest.user_delegated_address, + applicationAddress: authRequest.app_signer_address, + errorMessage: USER_REJECTED_REQUEST, + proof, + } + + const app = await getAppBySignerAddress(authRequest.app_signer_address) + + if (!app) { + throw new Error('App not found') + } + + await callbackFrameUrl(app.callback_url, data) res.json({ status: 'ok', diff --git a/src/controllers/v1/authorization/utils/error-message.ts b/src/controllers/v1/authorization/utils/error-message.ts index f9ac2b5..8ddf0ae 100644 --- a/src/controllers/v1/authorization/utils/error-message.ts +++ b/src/controllers/v1/authorization/utils/error-message.ts @@ -2,3 +2,7 @@ * Error message for invalid challenge answer. */ export const INVALID_CHALLENGE_ANSWER = 'Invalid answer for the challenge' +/** + * Error message for user rejection. + */ +export const USER_REJECTED_REQUEST = 'User rejected the request' diff --git a/test/controllers/authorization.test.ts b/test/controllers/authorization.test.ts index 5c7ccde..e8f0d24 100644 --- a/test/controllers/authorization.test.ts +++ b/test/controllers/authorization.test.ts @@ -25,7 +25,10 @@ import { insertMockedApp } from '../utils/app' import { DelegatedFs } from '../../src/service/delegated-fs/delegated-fs' import { Wallet } from 'ethers' import { getConfigData } from '../../src/config' -import { INVALID_CHALLENGE_ANSWER } from '../../src/controllers/v1/authorization/utils/error-message' +import { + INVALID_CHALLENGE_ANSWER, + USER_REJECTED_REQUEST, +} from '../../src/controllers/v1/authorization/utils/error-message' const testDb = knex(configurations.development) @@ -321,7 +324,7 @@ describe('Authorization', () => { expect(authRequest?.proof_signature).toStrictEqual(callbackData.proof) }) - it('should get signed callback of rejected authorization request', async () => { + it('should get signed callback of incorrect answer of a authorization request', async () => { callbackFrameUrlMock.mockReset() mockCallbackFrameUrl({ success: true }) const { userMainWallet, userDelegatedWallet, appWallet, authServiceWallet } = await insertMockedApp(mockInteractor) @@ -373,4 +376,49 @@ describe('Authorization', () => { expect(authRequest?.status).toBe(AuthorizationRequestStatus.INCORRECT) expect(authRequest?.proof_signature).toHaveLength(0) }) + + it('should get signed callback of a rejected authorization request', async () => { + callbackFrameUrlMock.mockReset() + mockCallbackFrameUrl({ success: true }) + const { userMainWallet, userDelegatedWallet, appWallet, authServiceWallet } = await insertMockedApp(mockInteractor) + const serviceSignature = await appWallet.signMessage(prepareEthAddress(userDelegatedWallet.address)) + const postData: ICreateAuthRequest = { + messageBytesProof: '0x123', + userDelegatedAddress: userDelegatedWallet.address, + serviceSignature, + } + const data = (await supertestApp.post(`/v1/authorization/create`).send(postData)).body as ICreateResponse + expect(data).toEqual({ status: 'ok', requestId: 1, answer: data.answer }) + + const postDataReject: IListRequest = { + messageBytesProof: '0x123', + } + + expect(callbackFrameUrlMock).toHaveBeenCalledTimes(0) + const answer1 = (await supertestApp.post(`/v1/authorization/reject`).send(postDataReject)).body as IAnswerResponse + expect(answer1).toStrictEqual({ status: 'ok' }) + expect(callbackFrameUrlMock).toHaveBeenCalledTimes(1) + const callbackData = callbackFrameUrlMock.mock.calls[0][1] as ICallbackFailRequest + expect(callbackData.userMainAddress).toEqual(prepareEthAddress(userMainWallet.address)) + expect(callbackData.userDelegatedAddress).toEqual(prepareEthAddress(userDelegatedWallet.address)) + expect(callbackData.applicationAddress).toEqual(prepareEthAddress(appWallet.address)) + expect(callbackData.errorMessage).toEqual(USER_REJECTED_REQUEST) + expect(callbackData.success).toBeFalsy() + + expect( + extractSignerAddress( + DelegatedFs.getDelegatedText( + userMainWallet.address, + userDelegatedWallet.address, + appWallet.address, + USER_REJECTED_REQUEST, + ), + `0x${callbackData.proof}`, + ), + ).toStrictEqual(prepareEthAddress(authServiceWallet.address)) + + const authRequest = await getAuthorizationRequestById(data.requestId) + expect(authRequest?.status).toBe(AuthorizationRequestStatus.REJECTED) + expect(authRequest?.proof_signature).toHaveLength(0) + }) }) From f6640f3d0dbd383123db7618629bbb8fe0de3fa7 Mon Sep 17 00:00:00 2001 From: Igor Shadurin Date: Tue, 28 May 2024 11:08:27 +0100 Subject: [PATCH 4/4] fix: user delegated address fix --- .github/workflows/tests.yaml | 32 ++++++++++++++++++++++++++ test/controllers/authorization.test.ts | 6 ++--- 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..1a903b6 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,32 @@ +name: Test + +on: + push: + branches: + - master + pull_request: + branches: + - '**' + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install dependencies + run: npm ci + + - name: Lint check + run: npm run lint:check + + - name: Check types + run: npm run check:types diff --git a/test/controllers/authorization.test.ts b/test/controllers/authorization.test.ts index e8f0d24..4f33ea6 100644 --- a/test/controllers/authorization.test.ts +++ b/test/controllers/authorization.test.ts @@ -168,11 +168,11 @@ describe('Authorization', () => { }) it('should reject authorization request', async () => { - const { userMainWallet, appWallet } = await insertMockedApp(mockInteractor) + const { userDelegatedWallet, appWallet } = await insertMockedApp(mockInteractor) const createData: ICreateAuthRequest = { messageBytesProof: '0x123', - userDelegatedAddress: userMainWallet.address, - serviceSignature: await appWallet.signMessage(prepareEthAddress(userMainWallet.address)), + userDelegatedAddress: userDelegatedWallet.address, + serviceSignature: await appWallet.signMessage(prepareEthAddress(userDelegatedWallet.address)), } const postData: IListRequest = { messageBytesProof: '0x123',