diff --git a/packages/backend/src/open_payments/auth/middleware.test.ts b/packages/backend/src/open_payments/auth/middleware.test.ts index f8e9bf1573..7cc76393eb 100644 --- a/packages/backend/src/open_payments/auth/middleware.test.ts +++ b/packages/backend/src/open_payments/auth/middleware.test.ts @@ -263,6 +263,37 @@ describe('Auth Middleware', (): void => { scope.done() }) + test('returns 500 if getOrCreateGrantReference throws', async (): Promise => { + const getOrCreateGrantReferenceSpy = jest + .spyOn(grantReferenceService, 'getOrCreate') + .mockImplementationOnce(async () => { + throw new Error('unexpected') + }) + + const grant = new TokenInfo( + { + active: true, + clientId: uuid(), + grant: uuid(), + access: [ + { + type: AccessType.IncomingPayment, + actions: [AccessAction.Read], + identifier: ctx.paymentPointer.url + } + ] + }, + mockKeyInfo + ) + + const scope = mockAuthServer(grant.toJSON()) + await expect(middleware(ctx, next)).rejects.toMatchObject({ + status: 500 + }) + expect(getOrCreateGrantReferenceSpy).toHaveBeenCalled() + scope.done() + }) + test.each` limitAccount ${false} diff --git a/packages/backend/src/open_payments/auth/middleware.ts b/packages/backend/src/open_payments/auth/middleware.ts index a1b9e47e34..41953224aa 100644 --- a/packages/backend/src/open_payments/auth/middleware.ts +++ b/packages/backend/src/open_payments/auth/middleware.ts @@ -1,6 +1,4 @@ import { AccessType, AccessAction } from './grant' -import { Transaction } from 'objection' -import { GrantReference } from '../grantReference/model' import { HttpSigContext, verifySigAndChallenge } from 'auth' export function createAuthMiddleware({ @@ -55,26 +53,19 @@ export function createAuthMiddleware({ ctx.throw(401, `Invalid signature`) } } - await GrantReference.transaction(async (trx: Transaction) => { - const grantRef = await grantReferenceService.get(grant.grant, trx) - if (grantRef) { - if (grantRef.clientId !== grant.clientId) { - logger.debug( - `clientID ${grant.clientId} for grant ${grant.grant} does not match internal reference clientId ${grantRef.clientId}.` - ) - ctx.throw(500) - } - } else if (action === AccessAction.Create) { - // Grant and client ID's are only stored for create routes - await grantReferenceService.create( - { - id: grant.grant, - clientId: grant.clientId - }, - trx - ) - } - }) + + try { + await grantReferenceService.getOrCreate( + { id: grant.grant, clientId: grant.clientId }, + action + ) + } catch (error) { + const errorMessage = + error?.message ?? 'Failed to get or create grant reference' + logger.error({ error }, errorMessage) + ctx.throw(500) + } + ctx.grant = grant // Unless the relevant grant action is ReadAll/ListAll add the diff --git a/packages/backend/src/open_payments/grantReference/service.test.ts b/packages/backend/src/open_payments/grantReference/service.test.ts index 711f176776..c486273e51 100644 --- a/packages/backend/src/open_payments/grantReference/service.test.ts +++ b/packages/backend/src/open_payments/grantReference/service.test.ts @@ -9,6 +9,7 @@ import { Config } from '../../config/app' import { GrantReferenceService } from './service' import { truncateTables } from '../../tests/tableManager' import { GrantReference } from './model' +import { AccessAction } from '../auth/grant' describe('Grant Reference Service', (): void => { let deps: IocContract @@ -79,4 +80,60 @@ describe('Grant Reference Service', (): void => { ) }) }) + + describe('Get or Create Grant Reference', (): void => { + test('returns undefined for a non-existing grant reference', async (): Promise => { + expect( + await grantReferenceService.getOrCreate( + { id: uuid(), clientId: uuid() }, + AccessAction.List + ) + ).toBeUndefined() + }) + + test('throws an error when clientId does not match', async (): Promise => { + const id = uuid() + + await grantReferenceService.create({ + id, + clientId: uuid() + }) + + await expect( + async () => + await grantReferenceService.getOrCreate( + { id, clientId: uuid() }, + AccessAction.List + ) + ).rejects.toThrowError('does not match internal reference clientId') + }) + + test('fetch an existing grant reference', async (): Promise => { + const existingRef = await grantReferenceService.create({ + id: uuid(), + clientId: uuid() + }) + + const retrievedRef = await grantReferenceService.getOrCreate( + existingRef, + AccessAction.List + ) + + expect(retrievedRef).toEqual(existingRef) + }) + + test('creates a grant reference', async (): Promise => { + const receivedRef = { + id: uuid(), + clientId: uuid() + } + + const grantRef = await grantReferenceService.getOrCreate( + receivedRef, + AccessAction.Create + ) + + expect(grantRef).toEqual(receivedRef) + }) + }) }) diff --git a/packages/backend/src/open_payments/grantReference/service.ts b/packages/backend/src/open_payments/grantReference/service.ts index 74a453597c..0e8c9ed0f6 100644 --- a/packages/backend/src/open_payments/grantReference/service.ts +++ b/packages/backend/src/open_payments/grantReference/service.ts @@ -1,4 +1,5 @@ import { Transaction, TransactionOrKnex } from 'objection' +import { AccessAction } from '../auth/grant' import { GrantReference } from './model' export interface GrantReferenceService { @@ -8,13 +9,18 @@ export interface GrantReferenceService { trx?: Transaction ): Promise lock(grantId: string, trx: TransactionOrKnex): Promise + getOrCreate( + options: CreateGrantReferenceOptions, + action: AccessAction + ): Promise } export async function createGrantReferenceService(): Promise { return { get: (grantId, trx) => getGrantReference(grantId, trx), create: (options, trx) => createGrantReference(options, trx), - lock: (grantId, trx) => lockGrantReference(grantId, trx) + lock: (grantId, trx) => lockGrantReference(grantId, trx), + getOrCreate: (options, action) => getOrCreateGrantReference(options, action) } } @@ -42,3 +48,26 @@ async function lockGrantReference(grantId: string, trx: TransactionOrKnex) { .forNoKeyUpdate() .timeout(5000) } + +async function getOrCreateGrantReference( + options: CreateGrantReferenceOptions, + action: AccessAction +) { + const grant = await GrantReference.transaction(async (trx: Transaction) => { + const grantRef = await getGrantReference(options.id, trx) + if (grantRef) { + if (grantRef.clientId !== options.clientId) { + throw new Error( + `clientID ${options.clientId} for grant ${options.id} does not match internal reference clientId ${grantRef.clientId}.` + ) + } + + return grantRef + } else if (action === AccessAction.Create) { + // Grant and client ID's are only stored for create routes + return await createGrantReference(options, trx) + } + }) + + return grant +}