Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): adding OpenPaymentsServerRouteError #2635

Merged
merged 14 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@graphql-tools/schema": "^10.0.3",
"@interledger/http-signature-utils": "2.0.2",
"@interledger/open-payments": "6.8.0",
"@interledger/openapi": "1.2.1",
"@interledger/openapi": "2.0.1",
"@interledger/pay": "0.4.0-alpha.9",
"@interledger/stream-receiver": "^0.3.3-alpha.3",
"@koa/cors": "^5.0.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import { PaymentMethodHandlerService } from './payment-method/handler/service'
import { IlpPaymentService } from './payment-method/ilp/service'
import { TelemetryService } from './telemetry/service'
import { ApolloArmor } from '@escape.tech/graphql-armor'
import { openPaymentsServerErrorMiddleware } from './open_payments/errors'
export interface AppContextData {
logger: Logger
container: AppContainer
Expand Down Expand Up @@ -384,10 +385,12 @@ export class App {
const koa = await this.createKoaServer()

const router = new Router<DefaultState, AppContext>()

router.use(bodyParser())
router.get('/healthz', (ctx: AppContext): void => {
ctx.status = 200
})
router.use(openPaymentsServerErrorMiddleware)

const walletAddressKeyRoutes = await this.container.use(
'walletAddressKeyRoutes'
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export function initIocContainer(
container.singleton('walletAddressKeyRoutes', async (deps) => {
return createWalletAddressKeyRoutes({
config: await deps.use('config'),
logger: await deps.use('logger'),
walletAddressKeyService: await deps.use('walletAddressKeyService'),
walletAddressService: await deps.use('walletAddressService')
})
Expand Down
146 changes: 107 additions & 39 deletions packages/backend/src/open_payments/auth/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { generateKeyPairSync } from 'crypto'
import { faker } from '@faker-js/faker'
import { Client, ActiveTokenInfo } from 'token-introspection'
import { Client, ActiveTokenInfo, TokenInfo } from 'token-introspection'
import { v4 as uuid } from 'uuid'
import {
generateJwk,
Expand Down Expand Up @@ -28,6 +28,8 @@ import { createWalletAddress } from '../../tests/walletAddress'
import { setup } from '../wallet_address/model.test'
import { parseLimits } from '../payment/outgoing/limits'
import { AccessAction, AccessType } from '@interledger/open-payments'
import { OpenPaymentsServerRouteError } from '../errors'
import assert from 'assert'

const nock = (global as unknown as { nock: typeof import('nock') }).nock

Expand Down Expand Up @@ -85,47 +87,64 @@ describe('Auth Middleware', (): void => {
})
ctx.request.headers.authorization = ''

const throwSpy = jest.spyOn(ctx, 'throw')
await expect(middleware(ctx, next)).resolves.toBeUndefined()
expect(throwSpy).toHaveBeenCalledWith(401, 'Unauthorized')
expect(ctx.response.get('WWW-Authenticate')).toBe(
`GNAP as_uri=${Config.authServerGrantUrl}`
)
expect(next).toHaveBeenCalled()
})

test('throws error for unkonwn errors', async (): Promise<void> => {
test('throws error for unknown errors', async (): Promise<void> => {
const middleware = createTokenIntrospectionMiddleware({
requestType: type,
requestAction: action,
bypassError: true
})
ctx.request.headers.authorization = ''
const error = new Error('Unknown')
ctx.throw = jest.fn().mockImplementation(() => {
throw error
}) as never

await expect(middleware(ctx, next)).rejects.toBe(error)
jest
.spyOn(tokenIntrospectionClient, 'introspect')
.mockResolvedValueOnce({ active: true, access: {} } as TokenInfo) // causes an error other than OpenPaymentsServerRouteError

expect.assertions(3)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handy expect.assertions(num) method when testing errors in try/catch to make sure the number assertions actually happened (instead of being ignored)


try {
await middleware(ctx, next)
} catch (err) {
assert(err instanceof Error)
assert(!(err instanceof OpenPaymentsServerRouteError))
expect(err.message).toBe('tokenInfo.access.find is not a function')
}

expect(ctx.response.get('WWW-Authenticate')).not.toBe(
`GNAP as_uri=${Config.authServerGrantUrl}`
)
expect(next).not.toHaveBeenCalled()
})
})

test.each`
authorization | description
${undefined} | ${'missing'}
${'Bearer NOT-GNAP'} | ${'invalid'}
${'GNAP'} | ${'missing'}
${'Bearer NOT-GNAP'} | ${'non-GNAP'}
${'GNAP'} | ${'missing token'}
${'GNAP multiple tokens'} | ${'invalid'}
`(
'returns 401 for $description access token',
'returns 401 for $description authorization header value',
async ({ authorization }): Promise<void> => {
const introspectSpy = jest.spyOn(tokenIntrospectionClient, 'introspect')
ctx.request.headers.authorization = authorization
await expect(middleware(ctx, next)).resolves.toBeUndefined()

expect.assertions(5)
try {
await middleware(ctx, next)
} catch (err) {
assert(err instanceof OpenPaymentsServerRouteError)
expect(err.status).toBe(401)
expect(err.message).toEqual(
'Missing or invalid authorization header value'
)
}
expect(introspectSpy).not.toHaveBeenCalled()
expect(ctx.status).toBe(401)
expect(ctx.message).toEqual('Unauthorized')
expect(ctx.response.get('WWW-Authenticate')).toBe(
`GNAP as_uri=${Config.authServerGrantUrl}`
)
Expand All @@ -139,12 +158,19 @@ describe('Auth Middleware', (): void => {
.mockImplementation(() => {
throw new Error('test error')
})
await expect(middleware(ctx, next)).resolves.toBeUndefined()

expect.assertions(5)
try {
await middleware(ctx, next)
} catch (err) {
assert(err instanceof OpenPaymentsServerRouteError)
expect(err.status).toBe(401)
expect(err.message).toEqual('Invalid Token')
}

expect(introspectSpy).toHaveBeenCalledWith({
access_token: token
})
expect(ctx.status).toBe(401)
expect(ctx.message).toEqual('Invalid Token')
expect(ctx.response.get('WWW-Authenticate')).toBe(
`GNAP as_uri=${Config.authServerGrantUrl}`
)
Expand Down Expand Up @@ -533,18 +559,33 @@ describe('HTTP Signature Middleware', (): void => {

test('returns 401 for missing keyid', async (): Promise<void> => {
ctx.request.headers['signature-input'] = 'aaaaaaaaaa'
await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({
status: 401,
message: 'Invalid signature input'
})
expect.assertions(3)

try {
await httpsigMiddleware(ctx, next)
} catch (err) {
assert(err instanceof OpenPaymentsServerRouteError)
expect(err.message).toBe(
'Signature validation error: missing keyId in signature input'
)
expect(err.status).toBe(401)
}

expect(next).not.toHaveBeenCalled()
})

test('returns 401 for failed client key request', async (): Promise<void> => {
await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({
status: 401,
message: 'Invalid signature input'
})
expect.assertions(3)

try {
await httpsigMiddleware(ctx, next)
} catch (err) {
assert(err instanceof OpenPaymentsServerRouteError)
expect(err.message).toBe(
'Signature validation error: could not retrieve client keys'
)
expect(err.status).toBe(401)
}
expect(next).not.toHaveBeenCalled()
})

Expand All @@ -555,10 +596,19 @@ describe('HTTP Signature Middleware', (): void => {
keys: [key]
})
ctx.request.headers['signature'] = 'aaaaaaaaaa='
await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({
status: 401,
message: 'Invalid signature'
})

expect.assertions(3)

try {
await httpsigMiddleware(ctx, next)
} catch (err) {
assert(err instanceof OpenPaymentsServerRouteError)
expect(err.message).toBe(
'Signature validation error: provided signature is invalid'
)
expect(err.status).toBe(401)
}

expect(next).not.toHaveBeenCalled()
scope.done()
})
Expand All @@ -570,11 +620,20 @@ describe('HTTP Signature Middleware', (): void => {
.reply(200, {
keys: [key]
})
await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({
status: 401,
message: 'Invalid signature input'
})

expect.assertions(3)

try {
await httpsigMiddleware(ctx, next)
} catch (err) {
assert(err instanceof OpenPaymentsServerRouteError)
expect(err.message).toBe(
'Signature validation error: could not retrieve client keys'
)
expect(err.status).toBe(401)
}
expect(next).not.toHaveBeenCalled()

scope.done()
})

Expand All @@ -586,11 +645,20 @@ describe('HTTP Signature Middleware', (): void => {
keys: [key]
})
ctx.request.headers['content-digest'] = 'aaaaaaaaaa='
await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({
status: 401,
message: 'Invalid signature'
})

expect.assertions(3)

try {
await httpsigMiddleware(ctx, next)
} catch (err) {
assert(err instanceof OpenPaymentsServerRouteError)
expect(err.message).toBe(
'Signature validation error: provided signature is invalid'
)
expect(err.status).toBe(401)
}
expect(next).not.toHaveBeenCalled()

scope.done()
})
}
Expand Down
Loading
Loading