diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 320baf8ded..0d4bded715 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -93,6 +93,7 @@ services: - '3003:3003' - '3006:3006' - "9230:9229" + - '3009:3009' environment: NODE_ENV: ${NODE_ENV:-development} TRUST_PROXY: ${TRUST_PROXY} diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 461ba0ad58..b5b15f9fa4 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -82,6 +82,7 @@ services: - '4003:3003' - '4006:3006' - '9232:9229' + - '4009:3009' environment: NODE_ENV: development AUTH_DATABASE_URL: postgresql://happy_life_bank_auth:happy_life_bank_auth@shared-database/happy_life_bank_auth diff --git a/localenv/mock-account-servicing-entity/app/lib/apiClient.ts b/localenv/mock-account-servicing-entity/app/lib/apiClient.ts index 0c4b525612..7678f154f5 100644 --- a/localenv/mock-account-servicing-entity/app/lib/apiClient.ts +++ b/localenv/mock-account-servicing-entity/app/lib/apiClient.ts @@ -38,7 +38,7 @@ export class ApiClient { // get grant --> GET /grant/:id/:nonce const { interactId, nonce } = params const response = await axios.get( - `http://localhost:3006/grant/${interactId}/${nonce}`, + `http://localhost:3009/grant/${interactId}/${nonce}`, { headers: { 'x-idp-secret': idpSecret @@ -71,7 +71,7 @@ export class ApiClient { const acceptanceSubPath = acceptanceDecision ? 'accept' : 'reject' const response = await axios.post( - `http://localhost:3006/grant/${interactId}/${nonce}/${acceptanceSubPath}`, + `http://localhost:3009/grant/${interactId}/${nonce}/${acceptanceSubPath}`, {}, { headers: { diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index 2b89194c69..971a7ead67 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -107,6 +107,7 @@ export type AppContainer = IocContract export class App { private authServer!: Server + private interactionServer!: Server private introspectionServer!: Server private adminServer!: Server private logger!: Logger @@ -328,26 +329,6 @@ export class App { interactionRoutes.finish ) - // Grant lookup - router.get( - '/grant/:id/:nonce', - createValidatorMiddleware(openApi.idpSpec, { - path: '/grant/{id}/{nonce}', - method: HttpMethod.GET - }), - interactionRoutes.details - ) - - // Grant accept/reject - router.post( - '/grant/:id/:nonce/:choice', - createValidatorMiddleware(openApi.idpSpec, { - path: '/grant/{id}/{nonce}/{choice}', - method: HttpMethod.POST - }), - interactionRoutes.acceptOrReject - ) - koa.use(cors()) koa.keys = [this.config.cookieKey] @@ -418,6 +399,54 @@ export class App { this.introspectionServer = koa.listen(port) } + public async startInteractionServer(port: number | string): Promise { + const koa = await this.createKoaServer() + + const router = new Router() + router.use(bodyParser()) + + const openApi = await this.container.use('openApi') + const interactionRoutes = await this.container.use('interactionRoutes') + + // Grant accept/reject + router.post( + '/grant/:id/:nonce/:choice', + createValidatorMiddleware(openApi.idpSpec, { + path: '/grant/{id}/{nonce}/{choice}', + method: HttpMethod.POST + }), + interactionRoutes.acceptOrReject + ) + + // Grant lookup + router.get( + '/grant/:id/:nonce', + createValidatorMiddleware(openApi.idpSpec, { + path: '/grant/{id}/{nonce}', + method: HttpMethod.GET + }), + interactionRoutes.details + ) + + koa.use(cors()) + koa.keys = [this.config.cookieKey] + koa.use( + session( + { + key: 'sessionId', + maxAge: 60 * 1000, + signed: true + }, + koa + ) + ) + + koa.use(router.middleware()) + koa.use(router.routes()) + + this.interactionServer = koa.listen(port) + } + private async createKoaServer(): Promise> { const koa = new Koa({ proxy: this.config.trustProxy @@ -454,6 +483,9 @@ export class App { if (this.authServer) { await this.stopServer(this.authServer) } + if (this.interactionServer) { + await this.stopServer(this.interactionServer) + } if (this.adminServer) { await this.stopServer(this.adminServer) } @@ -482,6 +514,10 @@ export class App { return this.getPort(this.authServer) } + public getInteractionPort(): number { + return this.getPort(this.interactionServer) + } + public getIntrospectionPort(): number { return this.getPort(this.introspectionServer) } diff --git a/packages/auth/src/config/app.ts b/packages/auth/src/config/app.ts index 051797db27..d0bf03072a 100644 --- a/packages/auth/src/config/app.ts +++ b/packages/auth/src/config/app.ts @@ -30,6 +30,7 @@ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), adminPort: envInt('ADMIN_PORT', 3003), authPort: envInt('AUTH_PORT', 3006), + interactionPort: envInt('INTERACTION_PORT', 3009), introspectionPort: envInt('INTROSPECTION_PORT', 3007), env: envString('NODE_ENV', 'development'), trustProxy: envBool('TRUST_PROXY', false), diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index d48076efef..2315fe083b 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -299,6 +299,9 @@ export const start = async ( await app.startAuthServer(config.authPort) logger.info(`Auth server listening on ${app.getAuthPort()}`) + await app.startInteractionServer(config.interactionPort) + logger.info(`Interaction server listening on ${app.getInteractionPort()}`) + await app.startIntrospectionServer(config.introspectionPort) logger.info(`Introspection server listening on ${app.getIntrospectionPort()}`) } diff --git a/packages/auth/src/tests/app.ts b/packages/auth/src/tests/app.ts index 998f60fad2..87338ba17c 100644 --- a/packages/auth/src/tests/app.ts +++ b/packages/auth/src/tests/app.ts @@ -33,6 +33,7 @@ export const createTestApp = async ( config.authPort = 0 config.introspectionPort = 0 config.adminPort = 0 + config.interactionPort = 0 const logger = createLogger({ transport: { diff --git a/packages/documentation/src/content/docs/concepts/open-payments/grant-interaction.mdx b/packages/documentation/src/content/docs/concepts/open-payments/grant-interaction.mdx index 045f18876f..276ee069ee 100644 --- a/packages/documentation/src/content/docs/concepts/open-payments/grant-interaction.mdx +++ b/packages/documentation/src/content/docs/concepts/open-payments/grant-interaction.mdx @@ -14,7 +14,9 @@ If the AS deems interaction necessary to issue a grant, there are five main endp - `GET /interact/:id/:nonce` (made by the client to the AS, establishes an interaction session, redirects browser session to IDP consent screen) - `GET /grant/:id/:nonce` (made by the IDP to the AS, secured with `x-idp-secret` header, returns grant info for the consent screen to enumerate ) + - **This is served on INTERACTION_PORT and its default value is 3009** - `POST /grant/:id/:nonce/(accept OR reject)` (made by the IDP to the AS, secured with `x-idp-secret` header, accepts or rejects the grant based on the user's input on the consent screen. **IDP then redirects to `GET /interact/:id/:nonce/finish`**) + - **This is served on INTERACTION_PORT and its default value is 3009** - `GET /interact/:id/:nonce/finish` (ends the interaction established by `GET /interact/:id/:nonce`, redirects browser session to client callback. Contains a query param that either indicates a failure, or on success, a `hash` parameter that the client can use to verify the successful interaction, and the `interact_ref` that identifies the interaction on the AS.) - Examples include: - `?result=grant_rejected` (if interaction was rejected) - `?result=grant_invalid` (if grant is not in a state where it may be accepted or rejected, e.g. already approved) - `?hash=p28jsq0Y2KK3WS__a42tavNC64ldGTBroywsWxT4md_jZQ1R\HZT8BOWYHcLmObM7XHPAdJzTZMtKBsaraJ64A &interact_ref=4IFWWIKYBC2PQ6U56NL1` (if interaction was accepted) - `hash` is a `sha-256` hash of values provided by the client in the body of the [grant initialization request](https://docs.openpayments.guide/reference/post-request) (`interact.finish.nonce`), values returned in the AS response for that request (`interact.finish`), the `interact_ref` provided alongside the `hash`, and the uri of the grant initialization request (`https://auth-server.com/`). diff --git a/packages/documentation/src/content/docs/integration/deployment.md b/packages/documentation/src/content/docs/integration/deployment.md index 341160d248..a88b13fa32 100644 --- a/packages/documentation/src/content/docs/integration/deployment.md +++ b/packages/documentation/src/content/docs/integration/deployment.md @@ -123,6 +123,7 @@ Now, the Admin UI can be found on localhost:3010. | `IDENTITY_SERVER_SECRET` | auth.identityServer.secret | | API key to fetch the identity server endpoint | | `INCOMING_PAYMENT_INTERACTION` | auth.interaction.incomingPayment | `false` | flag - incoming payments grant requests are interactive or not | | `INTERACTION_EXPIRY_SECONDS` | auth.interactionExpirySeconds | `600` | time in seconds for which a user can interact with a grant request | +| `INTERACTION_PORT` | auth.port.interaction | `3009` | Port number for the interaction APIs | | `INTROSPECTION_PORT` | auth.port.introspection | `3007` | port of this Open Payments Auth - Token Introspection Server | | `LIST_ALL_ACCESS_INTERACTION` | | `true` | Specify whether grant requests including a `list-all` action should require interaction. In these requests, the client asks to list resources that they themselves did not create. | | `LOG_LEVEL` | auth.logLevel | `info` | [Pino Log Level](https://getpino.io/#/docs/api?id=levels) | diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 31052fd01d..a54177bc7d 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -100,9 +100,16 @@ describe('Integration tests', (): void => { }) expect(senderWalletAddress.id).toBe(senderWalletAddressUrl) - const incomingPaymentGrant = await grantRequestIncomingPayment( - receiverWalletAddress - ) + let incomingPaymentGrant + try { + incomingPaymentGrant = await grantRequestIncomingPayment( + receiverWalletAddress + ) + } catch (err) { + console.log('ERROR: ', err) + throw err + } + const incomingPayment = await createIncomingPayment( receiverWalletAddress, incomingPaymentGrant.access_token.value, diff --git a/test/integration/lib/config.ts b/test/integration/lib/config.ts index d5802e9ef0..86aab0b22d 100644 --- a/test/integration/lib/config.ts +++ b/test/integration/lib/config.ts @@ -8,6 +8,7 @@ import { resolve } from 'path' export type TestConfig = Config & { integrationServerPort: number + interactionServer: string walletAddressUrl: string keyId: string } @@ -15,6 +16,7 @@ export type TestConfig = Config & { type EnvConfig = { OPEN_PAYMENTS_URL: string AUTH_SERVER_DOMAIN: string + INTERACTION_SERVER: string INTEGRATION_SERVER_PORT: string WALLET_ADDRESS_URL: string GRAPHQL_URL: string @@ -24,6 +26,7 @@ type EnvConfig = { const REQUIRED_KEYS: (keyof EnvConfig)[] = [ 'OPEN_PAYMENTS_URL', 'AUTH_SERVER_DOMAIN', + 'INTERACTION_SERVER', 'INTEGRATION_SERVER_PORT', 'WALLET_ADDRESS_URL', 'GRAPHQL_URL', @@ -61,6 +64,7 @@ const createConfig = (name: string): TestConfig => { publicHost: env.OPEN_PAYMENTS_URL, testnetAutoPeerUrl: '', authServerDomain: env.AUTH_SERVER_DOMAIN, + interactionServer: env.INTERACTION_SERVER, integrationServerPort: parseInt(env.INTEGRATION_SERVER_PORT), walletAddressUrl: env.WALLET_ADDRESS_URL, graphqlUrl: env.GRAPHQL_URL, diff --git a/test/integration/lib/test-actions/index.ts b/test/integration/lib/test-actions/index.ts index e975ae4fda..c47998ef2e 100644 --- a/test/integration/lib/test-actions/index.ts +++ b/test/integration/lib/test-actions/index.ts @@ -48,6 +48,7 @@ async function consentInteraction( ) { const { idpSecret } = deps.sendingASE.config const { interactId, nonce, cookie } = await _startAndAcceptInteraction( + deps, outgoingPaymentGrant, senderWalletAddress, idpSecret @@ -74,6 +75,7 @@ async function consentInteractionWithInteractRef( ): Promise { const { idpSecret } = deps.sendingASE.config const { interactId, nonce, cookie } = await _startAndAcceptInteraction( + deps, outgoingPaymentGrant, senderWalletAddress, idpSecret @@ -104,6 +106,7 @@ async function consentInteractionWithInteractRef( } async function _startAndAcceptInteraction( + deps: TestActionsDeps, outgoingPaymentGrant: PendingGrant, senderWalletAddress: WalletAddress, idpSecret: string @@ -125,7 +128,7 @@ async function _startAndAcceptInteraction( // Accept const acceptResponse = await fetch( - `${senderWalletAddress.authServer}/grant/${interactId}/${nonce}/accept`, + `${deps.sendingASE.config.interactionServer}/grant/${interactId}/${nonce}/accept`, { method: 'POST', headers: { diff --git a/test/integration/lib/test-actions/open-payments.ts b/test/integration/lib/test-actions/open-payments.ts index 007c306c65..6f54f25b68 100644 --- a/test/integration/lib/test-actions/open-payments.ts +++ b/test/integration/lib/test-actions/open-payments.ts @@ -95,6 +95,7 @@ async function grantRequestIncomingPayment( receiverWalletAddress: WalletAddress ): Promise { const { sendingASE } = deps + const grant = await sendingASE.opClient.grant.request( { url: receiverWalletAddress.authServer diff --git a/test/integration/testenv/cloud-nine-wallet/.env b/test/integration/testenv/cloud-nine-wallet/.env index a1c80fc105..f2cb1e1bbc 100644 --- a/test/integration/testenv/cloud-nine-wallet/.env +++ b/test/integration/testenv/cloud-nine-wallet/.env @@ -1,5 +1,6 @@ OPEN_PAYMENTS_URL=https://cloud-nine-wallet-test-backend:3100 AUTH_SERVER_DOMAIN=http://cloud-nine-wallet-test-auth:3106 +INTERACTION_SERVER=http://cloud-nine-wallet-test-auth:3109 INTEGRATION_SERVER_PORT=8888 WALLET_ADDRESS_URL=https://cloud-nine-wallet-test-backend:3100/.well-known/pay GRAPHQL_URL=http://cloud-nine-wallet-test-backend:3101/graphql diff --git a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml index 8f0e47939b..cd42756e39 100644 --- a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml +++ b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml @@ -53,11 +53,14 @@ services: - '3103:3103' - '3106:3106' - '3107:3107' + - '3109:3109' environment: NODE_ENV: ${NODE_ENV:-development} AUTH_SERVER_URL: http://cloud-nine-wallet-test-auth:3106 + INTERACTION_SERVER: http://cloud-nine-wallet-test-auth:3109 AUTH_DATABASE_URL: postgresql://cloud_nine_wallet_test_auth:cloud_nine_wallet_test_auth@shared-database/cloud_nine_wallet_test_auth INTROSPECTION_PORT: 3107 + INTERACTION_PORT: 3109 AUTH_PORT: 3106 ADMIN_PORT: 3103 REDIS_URL: redis://shared-redis:6379/1 diff --git a/test/integration/testenv/happy-life-bank/.env b/test/integration/testenv/happy-life-bank/.env index a2414409ec..d5da571ecc 100644 --- a/test/integration/testenv/happy-life-bank/.env +++ b/test/integration/testenv/happy-life-bank/.env @@ -1,5 +1,6 @@ OPEN_PAYMENTS_URL=https://happy-life-bank-test-backend:4100 AUTH_SERVER_DOMAIN=http://happy-life-bank-test-auth:4106 +INTERACTION_SERVER=http://happy-life-bank-test-auth:4109 INTEGRATION_SERVER_PORT=8889 WALLET_ADDRESS_URL=https://happy-life-bank-test-backend:4100/accounts/pfry GRAPHQL_URL=http://happy-life-bank-test-backend:4101/graphql diff --git a/test/integration/testenv/happy-life-bank/docker-compose.yml b/test/integration/testenv/happy-life-bank/docker-compose.yml index fd5d86c68a..09121fa9b2 100644 --- a/test/integration/testenv/happy-life-bank/docker-compose.yml +++ b/test/integration/testenv/happy-life-bank/docker-compose.yml @@ -53,10 +53,13 @@ services: - '4103:4103' - '4106:4106' - '4107:4107' + - '4109:4109' environment: NODE_ENV: development AUTH_DATABASE_URL: postgresql://happy_life_bank_test_auth:happy_life_bank_test_auth@shared-database/happy_life_bank_test_auth AUTH_SERVER_URL: http://happy-life-bank-test-auth:4106 + INTERACTION_SERVER: http://happy-life-bank-test-auth:4109 + INTERACTION_PORT: 4109 INTROSPECTION_PORT: 4107 ADMIN_PORT: 4103 AUTH_PORT: 4106 @@ -64,5 +67,6 @@ services: IDENTITY_SERVER_URL: http://localhost:3030/mock-idp/ IDENTITY_SERVER_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= COOKIE_KEY: 42397d1f371dd4b8b7d0308a689a57c882effd4ea909d792302542af47e2cd37 + AUTH_CHOICE_PORT: 4109 depends_on: - cloud-nine-wallet-test-auth \ No newline at end of file