-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add nonce guard in the sign controller (#240)
Save used nonces in the sign service
- Loading branch information
1 parent
a6fcffa
commit 06fbb3d
Showing
9 changed files
with
564 additions
and
228 deletions.
There are no files selected for viewing
105 changes: 105 additions & 0 deletions
105
apps/vault/src/shared/guard/__test__/unit/nonce.guard.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { ExecutionContext } from '@nestjs/common' | ||
import { Test } from '@nestjs/testing' | ||
import { Request } from 'express' | ||
import { MockProxy, mock } from 'jest-mock-extended' | ||
import { REQUEST_HEADER_CLIENT_ID } from '../../../../main.constant' | ||
import { NonceService } from '../../../../vault/core/service/nonce.service' | ||
import { ApplicationException } from '../../../exception/application.exception' | ||
import { NonceGuard } from '../../nonce.guard' | ||
|
||
const mockExecutionContext = (req?: Partial<Request>) => { | ||
return { | ||
switchToHttp: () => ({ | ||
getRequest: () => (req || {}) as Request | ||
}) | ||
} as ExecutionContext | ||
} | ||
|
||
describe(NonceGuard.name, () => { | ||
let guard: NonceGuard | ||
let nonceServiceMock: MockProxy<NonceService> | ||
|
||
beforeEach(async () => { | ||
nonceServiceMock = mock<NonceService>() | ||
|
||
const module = await Test.createTestingModule({ | ||
providers: [ | ||
NonceGuard, | ||
{ | ||
provide: NonceService, | ||
useValue: nonceServiceMock | ||
} | ||
] | ||
}).compile() | ||
|
||
guard = module.get<NonceGuard>(NonceGuard) | ||
}) | ||
|
||
const clientId = 'test-client-id' | ||
|
||
const signTransactionRequest = { | ||
request: { | ||
action: 'signTransaction', | ||
nonce: 'random-nonce-111', | ||
transactionRequest: { | ||
from: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1', | ||
to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B', | ||
chainId: 137, | ||
value: '0x5af3107a4000', | ||
data: '0x', | ||
nonce: 317, | ||
type: '2', | ||
gas: '21004', | ||
maxFeePerGas: '291175227375', | ||
maxPriorityFeePerGas: '81000000000' | ||
}, | ||
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1' | ||
} | ||
} | ||
|
||
const validHeaders = { | ||
[REQUEST_HEADER_CLIENT_ID]: clientId | ||
} | ||
|
||
it(`throws when ${REQUEST_HEADER_CLIENT_ID} header is not present`, async () => { | ||
const context = mockExecutionContext({ | ||
headers: {}, | ||
body: signTransactionRequest | ||
}) | ||
|
||
await expect(() => guard.canActivate(context)).rejects.toThrow(ApplicationException) | ||
await expect(() => guard.canActivate(context)).rejects.toThrow( | ||
`Missing or invalid ${REQUEST_HEADER_CLIENT_ID} header` | ||
) | ||
}) | ||
|
||
it(`throws when nonce request property is not present`, async () => { | ||
const req = mockExecutionContext({ | ||
headers: validHeaders, | ||
body: {} | ||
}) | ||
|
||
await expect(() => guard.canActivate(req)).rejects.toThrow(ApplicationException) | ||
await expect(() => guard.canActivate(req)).rejects.toThrow('Missing request nonce') | ||
}) | ||
|
||
it('throws when nonce already exists', async () => { | ||
nonceServiceMock.exist.mockResolvedValue(true) | ||
|
||
const req = mockExecutionContext({ | ||
headers: validHeaders, | ||
body: signTransactionRequest | ||
}) | ||
|
||
await expect(() => guard.canActivate(req)).rejects.toThrow(ApplicationException) | ||
await expect(() => guard.canActivate(req)).rejects.toThrow('Nonce already used') | ||
}) | ||
|
||
it('returns true when nonce is valid', async () => { | ||
const canActivate = await guard.canActivate( | ||
mockExecutionContext({ body: signTransactionRequest, headers: validHeaders }) | ||
) | ||
|
||
expect(canActivate).toEqual(true) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common' | ||
import { Request } from 'express' | ||
import { get } from 'lodash/fp' | ||
import { REQUEST_HEADER_CLIENT_ID } from '../../main.constant' | ||
import { NonceService } from '../../vault/core/service/nonce.service' | ||
import { ApplicationException } from '../exception/application.exception' | ||
|
||
@Injectable() | ||
export class NonceGuard implements CanActivate { | ||
constructor(private nonceService: NonceService) {} | ||
|
||
async canActivate(context: ExecutionContext): Promise<boolean> { | ||
const req = context.switchToHttp().getRequest() | ||
const nonce = this.getNonce(req) | ||
const clientId = this.getClientId(req) | ||
|
||
if (await this.nonceService.exist(clientId, nonce)) { | ||
throw new ApplicationException({ | ||
message: 'Nonce already used', | ||
suggestedHttpStatusCode: HttpStatus.FORBIDDEN, | ||
context: { clientId, nonce } | ||
}) | ||
} | ||
|
||
return true | ||
} | ||
|
||
private getNonce(req: Request): string { | ||
const nonce = get(['body', 'request', 'nonce'], req) | ||
|
||
if (!nonce) { | ||
throw new ApplicationException({ | ||
message: 'Missing request nonce', | ||
suggestedHttpStatusCode: HttpStatus.BAD_REQUEST | ||
}) | ||
} | ||
|
||
return nonce | ||
} | ||
|
||
private getClientId(req: Request): string { | ||
const clientId = get(['headers', REQUEST_HEADER_CLIENT_ID], req) | ||
|
||
if (Array.isArray(clientId)) { | ||
throw new ApplicationException({ | ||
message: `Invalid ${REQUEST_HEADER_CLIENT_ID} header`, | ||
suggestedHttpStatusCode: HttpStatus.FORBIDDEN | ||
}) | ||
} | ||
|
||
if (!clientId) { | ||
throw new ApplicationException({ | ||
message: `Missing or invalid ${REQUEST_HEADER_CLIENT_ID} header`, | ||
suggestedHttpStatusCode: HttpStatus.BAD_REQUEST | ||
}) | ||
} | ||
|
||
return clientId | ||
} | ||
} |
Oops, something went wrong.