diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 6f22a11e27..db03ac395d 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -25,6 +25,7 @@ services: IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= DISPLAY_NAME: Cloud Nine Wallet DISPLAY_ICON: wallet-icon.svg + OPERATOR_TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 volumes: - ../cloud-nine-wallet/seed.yml:/workspace/seed.yml - ../cloud-nine-wallet/private-key.pem:/workspace/private-key.pem diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 15d41cce4a..e365d27b3a 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -21,6 +21,7 @@ services: IDP_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= DISPLAY_NAME: Happy Life Bank DISPLAY_ICON: bank-icon.svg + OPERATOR_TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d volumes: - ../happy-life-bank/seed.yml:/workspace/seed.yml - ../happy-life-bank/private-key.pem:/workspace/private-key.pem diff --git a/localenv/mock-account-servicing-entity/app/lib/apolloClient.ts b/localenv/mock-account-servicing-entity/app/lib/apolloClient.ts index d71fcb7d6d..2ddeb515c1 100644 --- a/localenv/mock-account-servicing-entity/app/lib/apolloClient.ts +++ b/localenv/mock-account-servicing-entity/app/lib/apolloClient.ts @@ -68,7 +68,8 @@ const authLink = setContext((request, { headers }) => { return { headers: { ...headers, - signature: `t=${timestamp}, v${version}=${digest}` + signature: `t=${timestamp}, v${version}=${digest}`, + 'tenant-id': process.env.OPERATOR_TENANT_ID } } }) diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index 8aabae2830..843d3f9d2d 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -267,7 +267,7 @@ export class App { /* Back-channel GNAP Routes */ // Grant Initiation router.post( - '/:tenantId/', + '/:tenantId', createValidatorMiddleware(openApi.authServerSpec, { path: '/', method: HttpMethod.POST diff --git a/packages/auth/src/grant/model.ts b/packages/auth/src/grant/model.ts index 4fb8f7dce1..c395cece82 100644 --- a/packages/auth/src/grant/model.ts +++ b/packages/auth/src/grant/model.ts @@ -138,7 +138,7 @@ export function toOpenPaymentPendingGrant( access_token: { value: grant.continueToken }, - uri: `${authServerUrl}/continue/${grant.continueId}`, + uri: `${authServerUrl}/${grant.tenantId}/continue/${grant.continueId}`, wait: waitTimeSeconds } } @@ -158,7 +158,7 @@ export function toOpenPaymentsGrantContinuation( access_token: { value: grant.continueToken }, - uri: `${args.authServerUrl}/continue/${grant.continueId}`, + uri: `${args.authServerUrl}/${grant.tenantId}/continue/${grant.continueId}`, wait: args.waitTimeSeconds } } @@ -178,7 +178,7 @@ export function toOpenPaymentsGrant( access_token: { value: grant.continueToken }, - uri: `${args.authServerUrl}/continue/${grant.continueId}` + uri: `${args.authServerUrl}/${grant.tenantId}/continue/${grant.continueId}` } } } diff --git a/packages/auth/src/grant/routes.test.ts b/packages/auth/src/grant/routes.test.ts index c0544fd909..9ce4ff5622 100644 --- a/packages/auth/src/grant/routes.test.ts +++ b/packages/auth/src/grant/routes.test.ts @@ -32,7 +32,12 @@ import { AccessTokenService } from '../accessToken/service' import { generateNonce } from '../shared/utils' import { ClientService } from '../client/service' import { withConfigOverride } from '../tests/helpers' -import { AccessAction, AccessType } from '@interledger/open-payments' +import { + AccessAction, + AccessType, + GrantContinuation, + PendingGrant +} from '@interledger/open-payments' import { generateBaseGrant } from '../tests/grant' import { generateBaseInteraction } from '../tests/interaction' import { GNAPErrorCode } from '../shared/gnapErrors' @@ -73,6 +78,18 @@ const BASE_GRANT_REQUEST = { } } +function getGrantContinueId(continueUrl: string): string { + const continueUrlObj = new URL(continueUrl) + const pathItems = continueUrlObj.pathname.split('/') + return pathItems[pathItems.length - 1] +} + +function getInteractionId(redirectUrl: string): string { + const redirectUrlObj = new URL(redirectUrl) + const pathItems = redirectUrlObj.pathname.split('/') + return pathItems[pathItems.length - 2] +} + describe('Grant Routes', (): void => { let deps: IocContract let appContainer: TestContainer @@ -215,6 +232,14 @@ describe('Grant Routes', (): void => { ).resolves.toBeUndefined() expect(ctx.response).toSatisfyApiSpec() expect(ctx.status).toBe(200) + const createdGrant = await Grant.query().findOne({ + continueId: getGrantContinueId( + (ctx.body as GrantContinuation).continue.uri + ), + continueToken: (ctx.body as GrantContinuation) + .continue.access_token.value + }) + assert.ok(createdGrant) expect(ctx.body).toEqual({ access_token: { value: expect.any(String), @@ -224,9 +249,9 @@ describe('Grant Routes', (): void => { }, continue: { access_token: { - value: expect.any(String) + value: createdGrant.continueToken }, - uri: expect.any(String) + uri: `${config.authServerUrl}/${tenant.id}/continue/${createdGrant.continueId}` } }) } @@ -270,16 +295,37 @@ describe('Grant Routes', (): void => { await expect(grantRoutes.create(ctx)).resolves.toBeUndefined() expect(ctx.response).toSatisfyApiSpec() expect(ctx.status).toBe(200) + const createdGrant = await Grant.query().findOne({ + continueId: getGrantContinueId( + (ctx.body as PendingGrant).continue.uri + ), + continueToken: (ctx.body as PendingGrant).continue.access_token.value + }) + assert.ok(createdGrant) + const createdInteraction = await Interaction.query().findOne({ + nonce: (ctx.body as PendingGrant).interact.finish, + id: getInteractionId((ctx.body as PendingGrant).interact.redirect) + }) + assert.ok(createdInteraction) + const expectedRedirectUrl = new URL( + config.authServerUrl + + `/interact/${createdInteraction.id}/${createdInteraction.nonce}` + ) + expectedRedirectUrl.searchParams.set( + 'clientName', + TEST_CLIENT_DISPLAY.name + ) + expectedRedirectUrl.searchParams.set('clientUri', CLIENT) expect(ctx.body).toEqual({ interact: { - redirect: expect.any(String), - finish: expect.any(String) + redirect: expectedRedirectUrl.toString(), + finish: createdInteraction.nonce }, continue: { access_token: { - value: expect.any(String) + value: createdGrant.continueToken }, - uri: expect.any(String), + uri: `${config.authServerUrl}/${tenant.id}/continue/${createdGrant.continueId}`, wait: Config.waitTimeSeconds } }) @@ -563,6 +609,14 @@ describe('Grant Routes', (): void => { assert.ok(accessToken) expect(ctx.status).toBe(200) + const createdGrant = await Grant.query().findOne({ + continueId: getGrantContinueId( + (ctx.body as GrantContinuation).continue.uri + ), + continueToken: (ctx.body as GrantContinuation).continue.access_token + .value + }) + assert.ok(createdGrant) expect(ctx.body).toEqual({ access_token: { value: accessToken.value, @@ -578,9 +632,9 @@ describe('Grant Routes', (): void => { }, continue: { access_token: { - value: expect.any(String) + value: createdGrant.continueToken }, - uri: expect.any(String) + uri: `${config.authServerUrl}/${tenant.id}/continue/${createdGrant.continueId}` } }) }) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b167410756..959473863a 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -417,6 +417,7 @@ export function initIocContainer( }) container.singleton('remoteIncomingPaymentService', async (deps) => { return await createRemoteIncomingPaymentService({ + config: await deps.use('config'), logger: await deps.use('logger'), knex: await deps.use('knex'), grantService: await deps.use('grantService'), diff --git a/packages/backend/src/open_payments/payment/incoming/routes.ts b/packages/backend/src/open_payments/payment/incoming/routes.ts index eecbc1db5a..eb1d59cb58 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.ts @@ -84,7 +84,7 @@ async function getIncomingPaymentPublic( } ctx.body = incomingPayment.toPublicOpenPaymentsType( - deps.config.authServerGrantUrl + `${deps.config.authServerGrantUrl}/${deps.config.operatorTenantId}` // TODO: update this when tenanted incoming payments added ) } diff --git a/packages/backend/src/open_payments/payment/incoming_remote/service.ts b/packages/backend/src/open_payments/payment/incoming_remote/service.ts index 72c830e5c5..dcb1e43b99 100644 --- a/packages/backend/src/open_payments/payment/incoming_remote/service.ts +++ b/packages/backend/src/open_payments/payment/incoming_remote/service.ts @@ -13,6 +13,7 @@ import { BaseService } from '../../../shared/baseService' import { Amount, serializeAmount } from '../../amount' import { RemoteIncomingPaymentError } from './errors' import { isGrantError } from '../../grant/errors' +import { IAppConfig } from '../../../config/app' interface CreateRemoteIncomingPaymentArgs { walletAddressUrl: string @@ -35,6 +36,7 @@ export interface RemoteIncomingPaymentService { } interface ServiceDependencies extends BaseService { + config: IAppConfig grantService: GrantService openPaymentsUrl: string openPaymentsClient: AuthenticatedClient @@ -102,7 +104,7 @@ async function createIncomingPayment( walletAddress.resourceServer ?? new URL(walletAddress.id).origin const grantOptions = { - authServer: walletAddress.authServer, + authServer: `${walletAddress.authServer}/cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d`, // TODO: update with wallet address tenant id when tenanted wallet addresses are in accessType: AccessType.IncomingPayment, accessActions: [AccessAction.Create, AccessAction.ReadAll] } diff --git a/test/integration/lib/test-actions/open-payments.ts b/test/integration/lib/test-actions/open-payments.ts index 6f54f25b68..6206560fab 100644 --- a/test/integration/lib/test-actions/open-payments.ts +++ b/test/integration/lib/test-actions/open-payments.ts @@ -94,11 +94,11 @@ async function grantRequestIncomingPayment( deps: OpenPaymentsActionsDeps, receiverWalletAddress: WalletAddress ): Promise { - const { sendingASE } = deps + const { sendingASE, receivingASE } = deps const grant = await sendingASE.opClient.grant.request( { - url: receiverWalletAddress.authServer + url: `${receiverWalletAddress.authServer}/${receivingASE.config.operatorTenantId}` }, { access_token: { @@ -185,7 +185,7 @@ async function grantRequestQuote( const { sendingASE } = deps const grant = await sendingASE.opClient.grant.request( { - url: senderWalletAddress.authServer + url: `${senderWalletAddress.authServer}/${sendingASE.config.operatorTenantId}` }, { access_token: { @@ -232,10 +232,10 @@ async function grantRequestOutgoingPayment( limits: GrantRequestPaymentLimits, finish?: InteractFinish ): Promise { - const { receivingASE } = deps + const { receivingASE, sendingASE } = deps const grant = await receivingASE.opClient.grant.request( { - url: senderWalletAddress.authServer + url: `${senderWalletAddress.authServer}/${sendingASE.config.operatorTenantId}` }, { access_token: {