diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index d0cdd19f35..d4bb95d67c 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -577,7 +577,7 @@ export class App { createTokenIntrospectionMiddleware({ requestType: AccessType.IncomingPayment, requestAction: RequestAction.Read, - bypassError: true + canSkipAuthValidation: true }), authenticatedStatusMiddleware, getWalletAddressForSubresource, diff --git a/packages/backend/src/open_payments/auth/middleware.test.ts b/packages/backend/src/open_payments/auth/middleware.test.ts index 96f590fc23..26b06dc9da 100644 --- a/packages/backend/src/open_payments/auth/middleware.test.ts +++ b/packages/backend/src/open_payments/auth/middleware.test.ts @@ -87,19 +87,17 @@ describe('Auth Middleware', (): void => { await appContainer.shutdown() }) - describe('bypassError option', (): void => { - test('calls next for HTTP errors', async (): Promise => { + describe('canSkipAuthValidation option', (): void => { + test('calls next for undefined authorization header', async (): Promise => { const middleware = createTokenIntrospectionMiddleware({ requestType: type, requestAction: action, - bypassError: true + canSkipAuthValidation: true }) - ctx.request.headers.authorization = '' + ctx.request.headers.authorization = undefined await expect(middleware(ctx, next)).resolves.toBeUndefined() - expect(ctx.response.get('WWW-Authenticate')).toBe( - `GNAP as_uri=${Config.authServerGrantUrl}` - ) + expect(ctx.response.get('WWW-Authenticate')).toBeFalsy() expect(next).toHaveBeenCalled() }) @@ -107,7 +105,7 @@ describe('Auth Middleware', (): void => { const middleware = createTokenIntrospectionMiddleware({ requestType: AccessType.OutgoingPayment, requestAction: action, - bypassError: true + canSkipAuthValidation: true }) jest.spyOn(tokenIntrospectionClient, 'introspect').mockResolvedValueOnce({ @@ -140,6 +138,63 @@ describe('Auth Middleware', (): void => { ) expect(next).not.toHaveBeenCalled() }) + + test('proceeds with validation when authorization header exists, even with canSkipAuthValidation true', async (): Promise => { + const middleware = createTokenIntrospectionMiddleware({ + requestType: type, + requestAction: action, + canSkipAuthValidation: true + }) + ctx.request.headers.authorization = 'GNAP valid_token' + jest.spyOn(tokenIntrospectionClient, 'introspect').mockResolvedValueOnce({ + active: true, + access: [{ type: type, actions: [action] }], + client: 'test-client' + } as TokenInfo) + + await middleware(ctx, next) + + expect(tokenIntrospectionClient.introspect).toHaveBeenCalled() + expect(ctx.client).toBe('test-client') + expect(next).toHaveBeenCalled() + }) + + test('throws OpenPaymentsServerRouteError for invalid token with skipAuthValidation true', async (): Promise => { + const middleware = createTokenIntrospectionMiddleware({ + requestType: type, + requestAction: action, + canSkipAuthValidation: true + }) + ctx.request.headers.authorization = 'GNAP invalid_token' + jest + .spyOn(tokenIntrospectionClient, 'introspect') + .mockRejectedValueOnce(new Error()) + + await expect(middleware(ctx, next)).rejects.toThrow( + OpenPaymentsServerRouteError + ) + expect(ctx.response.get('WWW-Authenticate')).toBe( + `GNAP as_uri=${Config.authServerGrantUrl}` + ) + expect(next).not.toHaveBeenCalled() + }) + + test('throws OpenPaymentsServerRouteError when canSkipAuthValidation is false and no authorization header', async (): Promise => { + const middleware = createTokenIntrospectionMiddleware({ + requestType: type, + requestAction: action, + canSkipAuthValidation: false + }) + ctx.request.headers.authorization = '' + + await expect(middleware(ctx, next)).rejects.toThrow( + OpenPaymentsServerRouteError + ) + expect(ctx.response.get('WWW-Authenticate')).toBe( + `GNAP as_uri=${Config.authServerGrantUrl}` + ) + expect(next).not.toHaveBeenCalled() + }) }) test.each` @@ -498,12 +553,25 @@ describe('authenticatedStatusMiddleware', (): void => { await appContainer.shutdown() }) - test('sets ctx.authenticated to false if http signature is invalid', async (): Promise => { + test('sets ctx.authenticated to false if missing auth header', async (): Promise => { const ctx = createContext({ headers: { 'signature-input': '' } }) expect(authenticatedStatusMiddleware(ctx, next)).resolves.toBeUndefined() + expect(next).toHaveBeenCalled() + expect(ctx.authenticated).toBe(false) + }) + + test('sets ctx.authenticated to false if http signature is invalid and existing auth header', async (): Promise => { + const ctx = createContext({ + headers: { 'signature-input': '', authorization: 'GNAP token' } + }) + + expect(authenticatedStatusMiddleware(ctx, next)).rejects.toMatchObject({ + status: 401, + message: 'Signature validation error: missing keyId in signature input' + }) expect(next).not.toHaveBeenCalled() expect(ctx.authenticated).toBe(false) }) diff --git a/packages/backend/src/open_payments/auth/middleware.ts b/packages/backend/src/open_payments/auth/middleware.ts index 27ed023a14..1efa7fccc0 100644 --- a/packages/backend/src/open_payments/auth/middleware.ts +++ b/packages/backend/src/open_payments/auth/middleware.ts @@ -67,11 +67,11 @@ function toOpenPaymentsAccess( export function createTokenIntrospectionMiddleware({ requestType, requestAction, - bypassError = false + canSkipAuthValidation = false }: { requestType: AccessType requestAction: RequestAction - bypassError?: boolean + canSkipAuthValidation?: boolean }) { return async ( ctx: WalletAddressUrlContext, @@ -79,14 +79,19 @@ export function createTokenIntrospectionMiddleware({ ): Promise => { const config = await ctx.container.use('config') try { - const parts = ctx.request.headers.authorization?.split(' ') - if (parts?.length !== 2 || parts[0] !== 'GNAP') { + if (canSkipAuthValidation && !ctx.request.headers.authorization) { + await next() + return + } + + const authSplit = ctx.request.headers.authorization?.split(' ') + if (authSplit?.length !== 2 || authSplit[0] !== 'GNAP') { throw new OpenPaymentsServerRouteError( 401, 'Missing or invalid authorization header value' ) } - const token = parts[1] + const token = authSplit[1] const tokenIntrospectionClient = await ctx.container.use( 'tokenIntrospectionClient' ) @@ -146,15 +151,11 @@ export function createTokenIntrospectionMiddleware({ } } } catch (err) { - if (!(err instanceof OpenPaymentsServerRouteError)) { - throw err + if (err instanceof OpenPaymentsServerRouteError) { + ctx.set('WWW-Authenticate', `GNAP as_uri=${config.authServerGrantUrl}`) } - ctx.set('WWW-Authenticate', `GNAP as_uri=${config.authServerGrantUrl}`) - - if (!bypassError) { - throw err - } + throw err } await next() @@ -166,14 +167,13 @@ export const authenticatedStatusMiddleware = async ( next: () => Promise ): Promise => { ctx.authenticated = false - try { - await throwIfSignatureInvalid(ctx) - ctx.authenticated = true - } catch (err) { - if (!(err instanceof OpenPaymentsServerRouteError)) { - throw err - } + if (!ctx.request.headers.authorization) { + await next() + return } + + await throwIfSignatureInvalid(ctx) + ctx.authenticated = true await next() } 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 84920d13c3..bb11006b4a 100644 --- a/packages/backend/src/open_payments/payment/incoming_remote/service.ts +++ b/packages/backend/src/open_payments/payment/incoming_remote/service.ts @@ -231,14 +231,6 @@ async function getIncomingPayment( accessToken: grant.accessToken }) - // TODO: remove after #2889 is completed - if (!incomingPayment.walletAddress) { - throw new OpenPaymentsClientError('Got invalid incoming payment', { - status: 401, - description: 'Received public incoming payment instead of private' - }) - } - return incomingPayment } catch (err) { const errorMessage = 'Could not get remote incoming payment' diff --git a/packages/documentation/src/content/docs/integration/playground/overview.mdx b/packages/documentation/src/content/docs/integration/playground/overview.mdx index 59c7d41c25..35a0101c2f 100644 --- a/packages/documentation/src/content/docs/integration/playground/overview.mdx +++ b/packages/documentation/src/content/docs/integration/playground/overview.mdx @@ -178,6 +178,18 @@ You can either trigger the debugger by adding `debugger` statements in the code #### Debugging with VS Code: To debug with VS Code, add this configuration to your `.vscode/launch.json`: +```json +{ + "name": "Attach to docker (cloud-nine-backend)", + "type": "node", + "request": "attach", + "port": 9229, + "address": "localhost", + "localRoot": "${workspaceFolder}", + "remoteRoot": "/home/rafiki/", + "restart": true +}, +``` The `localRoot` variable will depend on the location of the `launch.json` file relative to Rafiki’s root directory.