diff --git a/packages/auth/migrations/20220504161932_create_grants_table.js b/packages/auth/migrations/20220504161932_create_grants_table.js index 77f81f60dd..c13b505cfa 100644 --- a/packages/auth/migrations/20220504161932_create_grants_table.js +++ b/packages/auth/migrations/20220504161932_create_grants_table.js @@ -13,7 +13,6 @@ exports.up = function (knex) { table.string('finishUri') table.string('clientNonce') table.string('client').notNullable() - table.string('clientKeyId').notNullable() table.string('interactId').unique() table.string('interactRef').unique() diff --git a/packages/auth/seeds/development/01_grants.js b/packages/auth/seeds/development/01_grants.js index 8c9e4ed304..2636ffc557 100644 --- a/packages/auth/seeds/development/01_grants.js +++ b/packages/auth/seeds/development/01_grants.js @@ -12,7 +12,6 @@ exports.seed = function (knex) { finishMethod: 'redirect', finishUri: 'https://example.com/finish', client: 'https://backend/accounts/gfranklin', - clientKeyId: 'keyid-742ab7cd-1624-4d2e-af6e-e15a71638669', clientNonce: 'example-client-nonce', continueToken: '566a929a-86bb-41b8-b12d-718fa4ab2db2', continueId: '92c98ab7-9240-43b4-a86f-402f1c6fd6f5', @@ -29,7 +28,6 @@ exports.seed = function (knex) { finishMethod: 'redirect', finishUri: 'http://peer-auth:3006/finish', client: 'https://peer-backend/accounts/pfry', - clientKeyId: 'keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5', clientNonce: 'example-client-nonce', continueToken: 'fc7d255b-66f7-46f5-af56-65831a110604', continueId: '006856cd-a34a-4d4a-bb69-af1e07980834', @@ -46,7 +44,6 @@ exports.seed = function (knex) { finishMethod: 'redirect', finishUri: 'http://localhost:3300/mock-idp/fake-client?', client: 'https://backend/accounts/gfranklin', - clientKeyId: 'keyid-742ab7cd-1624-4d2e-af6e-e15a71638669', clientNonce: 'demo-client-nonce', continueToken: '301294e4-db8d-445b-b77e-d27583719ecc', continueId: 'edbe6928-7d80-44a8-94b8-514e75759439', diff --git a/packages/auth/src/access/service.test.ts b/packages/auth/src/access/service.test.ts index 6d5e390af5..607dea51e2 100644 --- a/packages/auth/src/access/service.test.ts +++ b/packages/auth/src/access/service.test.ts @@ -44,7 +44,6 @@ describe('Access Service', (): void => { finishUri: 'https://example.com/finish', clientNonce: generateNonce(), client: faker.internet.url(), - clientKeyId: 'test-key', interactId: v4(), interactRef: generateNonce(), interactNonce: generateNonce() diff --git a/packages/auth/src/accessToken/routes.test.ts b/packages/auth/src/accessToken/routes.test.ts index daf2fffb8c..de4da80680 100644 --- a/packages/auth/src/accessToken/routes.test.ts +++ b/packages/auth/src/accessToken/routes.test.ts @@ -1,7 +1,6 @@ import { faker } from '@faker-js/faker' import nock from 'nock' import { Knex } from 'knex' -import crypto from 'crypto' import { v4 } from 'uuid' import jestOpenAPI from 'jest-openapi' @@ -16,7 +15,6 @@ import { AccessToken } from './model' import { Access } from '../access/model' import { AccessTokenRoutes } from './routes' import { createContext } from '../tests/context' -import { generateTestKeys, JWK } from 'http-signature-utils' import { generateNonce, generateToken } from '../shared/utils' import { AccessType, AccessAction } from 'open-payments' @@ -25,7 +23,6 @@ describe('Access Token Routes', (): void => { let appContainer: TestContainer let trx: Knex.Transaction let accessTokenRoutes: AccessTokenRoutes - let testClientKey: JWK beforeAll(async (): Promise => { deps = await initIocContainer(Config) @@ -33,8 +30,6 @@ describe('Access Token Routes', (): void => { accessTokenRoutes = await deps.use('accessTokenRoutes') const openApi = await deps.use('openApi') jestOpenAPI(openApi.authServerSpec) - - testClientKey = generateTestKeys().publicKey }) afterEach(async (): Promise => { @@ -93,10 +88,7 @@ describe('Access Token Routes', (): void => { const method = 'POST' beforeEach(async (): Promise => { - grant = await Grant.query(trx).insertAndFetch({ - ...BASE_GRANT, - clientKeyId: testClientKey.kid - }) + grant = await Grant.query(trx).insertAndFetch(BASE_GRANT) access = await Access.query(trx).insertAndFetch({ grantId: grant.id, ...BASE_ACCESS @@ -135,14 +127,6 @@ describe('Access Token Routes', (): void => { }) test('Successfully introspects valid token', async (): Promise => { - const clientId = crypto.createHash('sha256').update(CLIENT).digest('hex') - - const scope = nock(CLIENT) - .get('/jwks.json') - .reply(200, { - keys: [testClientKey] - }) - const ctx = createContext( { headers: { @@ -175,13 +159,8 @@ describe('Access Token Routes', (): void => { identifier: access.identifier } ], - key: { - proof: 'httpsig', - jwk: testClientKey - }, - client_id: clientId + client: CLIENT }) - scope.done() }) test('Successfully introspects expired token', async (): Promise => { @@ -226,10 +205,7 @@ describe('Access Token Routes', (): void => { const method = 'DELETE' beforeEach(async (): Promise => { - grant = await Grant.query(trx).insertAndFetch({ - ...BASE_GRANT, - clientKeyId: testClientKey.kid - }) + grant = await Grant.query(trx).insertAndFetch(BASE_GRANT) token = await AccessToken.query(trx).insertAndFetch({ grantId: grant.id, ...BASE_TOKEN @@ -317,10 +293,7 @@ describe('Access Token Routes', (): void => { let managementId: string beforeEach(async (): Promise => { - grant = await Grant.query(trx).insertAndFetch({ - ...BASE_GRANT, - clientKeyId: testClientKey.kid - }) + grant = await Grant.query(trx).insertAndFetch(BASE_GRANT) access = await Access.query(trx).insertAndFetch({ grantId: grant.id, ...BASE_ACCESS diff --git a/packages/auth/src/accessToken/routes.ts b/packages/auth/src/accessToken/routes.ts index d87416e7fe..3a8d34fec9 100644 --- a/packages/auth/src/accessToken/routes.ts +++ b/packages/auth/src/accessToken/routes.ts @@ -1,12 +1,12 @@ -import { createHash } from 'crypto' import { Logger } from 'pino' import { ActiveTokenInfo, TokenInfo } from 'token-introspection' import { Access } from '../access/model' import { AppContext } from '../app' import { IAppConfig } from '../config/app' -import { AccessTokenService, Introspection } from './service' +import { AccessTokenService } from './service' import { accessToBody } from '../shared/utils' import { ClientService } from '../client/service' +import { Grant } from '../grant/model' type TokenRequest = Omit & { body: BodyT @@ -63,31 +63,26 @@ async function introspectToken( ctx: IntrospectContext ): Promise { const { body } = ctx.request - const introspectionResult = await deps.accessTokenService.introspect( + const grant = await deps.accessTokenService.introspect( // body.access_token exists since it is checked for by the request validation body['access_token'] ) - ctx.body = introspectionToBody(introspectionResult) + ctx.body = grantToTokenInfo(grant) } -function introspectionToBody(introspection?: Introspection): TokenInfo { - if (!introspection) { +function grantToTokenInfo(grant?: Grant): TokenInfo { + if (!grant) { return { active: false } } - const { grant, jwk } = introspection return { active: true, grant: grant.id, access: grant.access.map((a: Access) => accessToBody(a) ) as ActiveTokenInfo['access'], - key: { - proof: 'httpsig', - jwk - }, - client_id: createHash('sha256').update(grant.client).digest('hex') + client: grant.client } } diff --git a/packages/auth/src/accessToken/service.test.ts b/packages/auth/src/accessToken/service.test.ts index 20e41d4f30..777f58e020 100644 --- a/packages/auth/src/accessToken/service.test.ts +++ b/packages/auth/src/accessToken/service.test.ts @@ -13,7 +13,6 @@ import { FinishMethod, Grant, GrantState, StartMethod } from '../grant/model' import { AccessToken } from './model' import { AccessTokenService } from './service' import { Access } from '../access/model' -import { generateTestKeys, JWK } from 'http-signature-utils' import { generateNonce, generateToken } from '../shared/utils' import { AccessType, AccessAction } from 'open-payments' @@ -22,14 +21,11 @@ describe('Access Token Service', (): void => { let appContainer: TestContainer let trx: Knex.Transaction let accessTokenService: AccessTokenService - let testClientKey: JWK beforeAll(async (): Promise => { deps = await initIocContainer(Config) appContainer = await createTestApp(deps) accessTokenService = await deps.use('accessTokenService') - - testClientKey = generateTestKeys().publicKey }) afterEach(async (): Promise => { @@ -76,7 +72,6 @@ describe('Access Token Service', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testClientKey.kid, continueToken: generateToken(), continueId: v4(), interactId: v4(), @@ -145,19 +140,9 @@ describe('Access Token Service', (): void => { describe('Introspect', (): void => { test('Can introspect active token', async (): Promise => { - const scope = nock(CLIENT) - .get('/jwks.json') - .reply(200, { - keys: [testClientKey] - }) - await expect(accessTokenService.introspect(token.value)).resolves.toEqual( - { - grant, - jwk: testClientKey - } + grant ) - scope.done() }) test('Can introspect expired token', async (): Promise => { @@ -181,12 +166,6 @@ describe('Access Token Service', (): void => { test('Cannot introspect non-existing token', async (): Promise => { expect(accessTokenService.introspect('uuid')).resolves.toBeUndefined() }) - - test('Cannot introspect with non-existing key', async (): Promise => { - await expect( - accessTokenService.introspect(token.value) - ).resolves.toBeUndefined() - }) }) describe('Revoke', (): void => { @@ -195,7 +174,6 @@ describe('Access Token Service', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testClientKey.kid, continueToken: generateToken(), continueId: v4(), interactId: v4(), @@ -248,7 +226,6 @@ describe('Access Token Service', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testClientKey.kid, continueToken: generateToken(), continueId: v4(), interactId: v4(), diff --git a/packages/auth/src/accessToken/service.ts b/packages/auth/src/accessToken/service.ts index 6a9420d0d3..2efb4d16de 100644 --- a/packages/auth/src/accessToken/service.ts +++ b/packages/auth/src/accessToken/service.ts @@ -1,6 +1,5 @@ import { v4 } from 'uuid' import { Transaction, TransactionOrKnex } from 'objection' -import { JWK } from 'http-signature-utils' import { BaseService } from '../shared/baseService' import { generateToken } from '../shared/utils' @@ -13,7 +12,7 @@ import { Access } from '../access/model' export interface AccessTokenService { get(token: string): Promise getByManagementId(managementId: string): Promise - introspect(token: string): Promise + introspect(token: string): Promise revoke(id: string, tokenValue: string): Promise create(grantId: string, opts?: AccessTokenOpts): Promise rotate(managementId: string, tokenValue: string): Promise @@ -25,16 +24,6 @@ interface ServiceDependencies extends BaseService { config: IAppConfig } -export interface KeyInfo { - proof: string - jwk: JWK -} - -export interface Introspection { - grant: Grant - jwk: JWK -} - interface AccessTokenOpts { expiresIn?: number trx?: Transaction @@ -104,7 +93,7 @@ async function getByManagementId( async function introspect( deps: ServiceDependencies, value: string -): Promise { +): Promise { const token = await AccessToken.query(deps.knex) .findOne({ value }) .withGraphFetched('grant.access') @@ -117,19 +106,7 @@ async function introspect( return undefined } - const jwk = await deps.clientService.getKey({ - client: token.grant.client, - keyId: token.grant.clientKeyId - }) - - if (!jwk) { - return undefined - } - - return { - grant: token.grant, - jwk - } + return token.grant } } diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index a702e66454..53d2342c3c 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -45,8 +45,6 @@ export interface AppContextData extends DefaultContext { params: { [key: string]: string } // Set by koa-generic-session session: { [key: string]: string } - // TODO: define separate Context used in routes that include httpsig - clientKeyId?: string } export type AppContext = Koa.ParameterizedContext diff --git a/packages/auth/src/grant/model.ts b/packages/auth/src/grant/model.ts index 7656e06ff1..0760fca85b 100644 --- a/packages/auth/src/grant/model.ts +++ b/packages/auth/src/grant/model.ts @@ -55,7 +55,6 @@ export class Grant extends BaseModel { public finishUri?: string public client!: string public clientNonce?: string // client-generated nonce for post-interaction hash - public clientKeyId!: string public interactId?: string public interactRef?: string diff --git a/packages/auth/src/grant/routes.test.ts b/packages/auth/src/grant/routes.test.ts index 160546d5e4..edf551e1f4 100644 --- a/packages/auth/src/grant/routes.test.ts +++ b/packages/auth/src/grant/routes.test.ts @@ -27,7 +27,6 @@ export const TEST_CLIENT_DISPLAY = { } const CLIENT = faker.internet.url() -const CLIENT_KEY_ID = v4() const BASE_GRANT_ACCESS = { type: AccessType.IncomingPayment, @@ -75,7 +74,6 @@ describe('Grant Routes', (): void => { finishUri: 'https://example.com', clientNonce: generateNonce(), client: CLIENT, - clientKeyId: CLIENT_KEY_ID, interactId: v4(), interactRef: v4(), interactNonce: generateNonce() @@ -84,11 +82,7 @@ describe('Grant Routes', (): void => { const createContext = ( reqOpts: httpMocks.RequestOptions, params: Record - ) => { - const ctx = createAppContext(reqOpts, params) - ctx.clientKeyId = CLIENT_KEY_ID - return ctx - } + ) => createAppContext(reqOpts, params) beforeEach(async (): Promise => { grant = await Grant.query().insert(generateBaseGrant()) diff --git a/packages/auth/src/grant/routes.ts b/packages/auth/src/grant/routes.ts index 80f849b843..3e9a4efdb7 100644 --- a/packages/auth/src/grant/routes.ts +++ b/packages/auth/src/grant/routes.ts @@ -39,7 +39,6 @@ type GrantContext = Exclude< 'request' > & { request: GrantRequest - clientKeyId: string } export type CreateContext = GrantContext @@ -145,7 +144,6 @@ async function createGrantInitiation( ): Promise { const { body } = ctx.request const { grantService, config } = deps - const clientKeyId = ctx.clientKeyId if ( !deps.config.incomingPaymentInteraction && @@ -159,13 +157,7 @@ async function createGrantInitiation( let grant: Grant let accessToken: AccessToken try { - grant = await grantService.create( - { - ...body, - clientKeyId - }, - trx - ) + grant = await grantService.create(body, trx) accessToken = await deps.accessTokenService.create(grant.id, { trx }) @@ -194,10 +186,7 @@ async function createGrantInitiation( if (!client) { ctx.throw(400, 'invalid_client', { error: 'invalid_client' }) } else { - const grant = await grantService.create({ - ...body, - clientKeyId - }) + const grant = await grantService.create(body) ctx.status = 200 const redirectUri = new URL( diff --git a/packages/auth/src/grant/service.test.ts b/packages/auth/src/grant/service.test.ts index 16b05834b4..28ec217f3c 100644 --- a/packages/auth/src/grant/service.test.ts +++ b/packages/auth/src/grant/service.test.ts @@ -28,7 +28,6 @@ describe('Grant Service', (): void => { }) const CLIENT = faker.internet.url() - const CLIENT_KEY_ID = v4() beforeEach(async (): Promise => { grant = await Grant.query().insert({ @@ -40,7 +39,6 @@ describe('Grant Service', (): void => { finishUri: 'https://example.com', clientNonce: generateNonce(), client: CLIENT, - clientKeyId: CLIENT_KEY_ID, interactId: v4(), interactRef: v4(), interactNonce: generateNonce() @@ -82,7 +80,6 @@ describe('Grant Service', (): void => { test('Can initiate a grant', async (): Promise => { const grantRequest: GrantRequest = { ...BASE_GRANT_REQUEST, - clientKeyId: CLIENT_KEY_ID, access_token: { access: [ { @@ -106,7 +103,6 @@ describe('Grant Service', (): void => { finishUri: BASE_GRANT_REQUEST.interact.finish.uri, clientNonce: BASE_GRANT_REQUEST.interact.finish.nonce, client: CLIENT, - clientKeyId: CLIENT_KEY_ID, startMethod: expect.arrayContaining([StartMethod.Redirect]) }) @@ -123,7 +119,6 @@ describe('Grant Service', (): void => { test('Can issue a grant without interaction', async (): Promise => { const grantRequest: GrantRequest = { ...BASE_GRANT_REQUEST, - clientKeyId: CLIENT_KEY_ID, access_token: { access: [ { @@ -140,8 +135,7 @@ describe('Grant Service', (): void => { expect(grant).toMatchObject({ state: GrantState.Granted, continueId: expect.any(String), - continueToken: expect.any(String), - clientKeyId: CLIENT_KEY_ID + continueToken: expect.any(String) }) await expect( diff --git a/packages/auth/src/grant/service.ts b/packages/auth/src/grant/service.ts index f5f22e72ef..fa60f72029 100644 --- a/packages/auth/src/grant/service.ts +++ b/packages/auth/src/grant/service.ts @@ -43,7 +43,6 @@ export interface GrantRequest { access: AccessRequest[] } client: string - clientKeyId: string interact?: { start: StartMethod[] finish?: { @@ -145,8 +144,7 @@ async function create( const { access_token: { access }, interact, - client, - clientKeyId + client } = grantRequest const grantTrx = trx || (await Grant.startTransaction(knex)) @@ -158,7 +156,6 @@ async function create( finishUri: interact?.finish?.uri, clientNonce: interact?.finish?.nonce, client, - clientKeyId, interactId: interact ? v4() : undefined, interactRef: interact ? v4() : undefined, interactNonce: interact ? generateNonce() : undefined, diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index b6f5712685..2ee96939b2 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -16,7 +16,6 @@ import { createGrantRoutes } from './grant/routes' import { createOpenAPI } from 'openapi' import { createUnauthenticatedClient as createOpenPaymentsClient } from 'open-payments' -export { KeyInfo } from './accessToken/service' const container = initIocContainer(Config) const app = new App(container) diff --git a/packages/auth/src/signature/middleware.test.ts b/packages/auth/src/signature/middleware.test.ts index 546e0eaf56..64961fade9 100644 --- a/packages/auth/src/signature/middleware.test.ts +++ b/packages/auth/src/signature/middleware.test.ts @@ -91,10 +91,7 @@ describe('Signature Service', (): void => { }) beforeEach(async (): Promise => { - grant = await Grant.query(trx).insertAndFetch({ - ...BASE_GRANT, - clientKeyId: testKeys.publicKey.kid - }) + grant = await Grant.query(trx).insertAndFetch(BASE_GRANT) await Access.query(trx).insertAndFetch({ grantId: grant.id, ...BASE_ACCESS @@ -144,7 +141,6 @@ describe('Signature Service', (): void => { await grantInitiationHttpsigMiddleware(ctx, next) expect(ctx.response.status).toEqual(200) - expect(ctx.clientKeyId).toEqual(testKeys.publicKey.kid) expect(next).toHaveBeenCalled() scope.done() @@ -175,7 +171,6 @@ describe('Signature Service', (): void => { await grantContinueHttpsigMiddleware(ctx, next) expect(ctx.response.status).toEqual(200) - expect(ctx.clientKeyId).toEqual(testKeys.publicKey.kid) expect(next).toHaveBeenCalled() scope.done() @@ -211,7 +206,6 @@ describe('Signature Service', (): void => { expect(next).toHaveBeenCalled() expect(ctx.response.status).toEqual(200) - expect(ctx.clientKeyId).toEqual(testKeys.publicKey.kid) scope.done() }) diff --git a/packages/auth/src/signature/middleware.ts b/packages/auth/src/signature/middleware.ts index c3737cd928..21c019840d 100644 --- a/packages/auth/src/signature/middleware.ts +++ b/packages/auth/src/signature/middleware.ts @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { + getKeyId, validateSignature, validateSignatureHeaders, RequestLike } from 'http-signature-utils' import { AppContext } from '../app' -import { Grant } from '../grant/model' import { ContinueContext, CreateContext, DeleteContext } from '../grant/routes' function contextToRequestLike(ctx: AppContext): RequestLike { @@ -21,13 +21,18 @@ function contextToRequestLike(ctx: AppContext): RequestLike { async function verifySigFromClient( client: string, - clientKeyId: string, ctx: AppContext ): Promise { + const sigInput = ctx.headers['signature-input'] as string + const keyId = getKeyId(sigInput) + if (!keyId) { + ctx.throw(401, 'invalid signature input', { error: 'invalid_request' }) + } + const clientService = await ctx.container.use('clientService') const clientKey = await clientService.getKey({ client, - keyId: clientKeyId + keyId }) if (!clientKey) { @@ -36,29 +41,6 @@ async function verifySigFromClient( return validateSignature(clientKey, contextToRequestLike(ctx)) } -async function verifySigFromBoundKey( - grant: Grant, - ctx: AppContext -): Promise { - const sigInput = ctx.headers['signature-input'] as string - ctx.clientKeyId = getSigInputKeyId(sigInput) - if (ctx.clientKeyId !== grant.clientKeyId) { - ctx.throw(401, 'invalid signature input', { error: 'invalid_request' }) - } - - return verifySigFromClient(grant.client, ctx.clientKeyId, ctx) -} - -const KEY_ID_PREFIX = 'keyid="' - -function getSigInputKeyId(sigInput: string): string | undefined { - const keyIdParam = sigInput - .split(';') - .find((param) => param.startsWith(KEY_ID_PREFIX)) - // Trim prefix and quotes - return keyIdParam?.slice(KEY_ID_PREFIX.length, -1) -} - export async function grantContinueHttpsigMiddleware( ctx: ContinueContext | DeleteContext, next: () => Promise @@ -101,7 +83,7 @@ export async function grantContinueHttpsigMiddleware( return } - const sigVerified = await verifySigFromBoundKey(grant, ctx) + const sigVerified = await verifySigFromClient(grant.client, ctx) if (!sigVerified) { ctx.throw(401, 'invalid signature') } @@ -118,18 +100,7 @@ export async function grantInitiationHttpsigMiddleware( const { body } = ctx.request - const sigInput = ctx.headers['signature-input'] as string - const clientKeyId = getSigInputKeyId(sigInput) - if (!clientKeyId) { - ctx.throw(401, 'invalid signature input', { error: 'invalid_request' }) - } - ctx.clientKeyId = clientKeyId - - const sigVerified = await verifySigFromClient( - body.client, - ctx.clientKeyId, - ctx - ) + const sigVerified = await verifySigFromClient(body.client, ctx) if (!sigVerified) { ctx.throw(401, 'invalid signature') } @@ -165,7 +136,7 @@ export async function tokenHttpsigMiddleware( ctx.throw(500, 'internal server error', { error: 'internal_server_error' }) } - const sigVerified = await verifySigFromBoundKey(accessToken.grant, ctx) + const sigVerified = await verifySigFromClient(accessToken.grant.client, ctx) if (!sigVerified) { ctx.throw(401, 'invalid signature') } diff --git a/packages/backend/migrations/20220908085845_create_incoming_payments_table.js b/packages/backend/migrations/20220908085845_create_incoming_payments_table.js index d9b8544fc2..392118757e 100644 --- a/packages/backend/migrations/20220908085845_create_incoming_payments_table.js +++ b/packages/backend/migrations/20220908085845_create_incoming_payments_table.js @@ -12,7 +12,7 @@ exports.up = function (knex) { table.string('externalRef').nullable() table.uuid('connectionId').nullable() - table.string('clientId').nullable() + table.string('client').nullable() table.uuid('assetId').notNullable() table.foreign('assetId').references('assets.id') diff --git a/packages/backend/migrations/20221012205150_create_quotes_table.js b/packages/backend/migrations/20221012205150_create_quotes_table.js index 208cf2fb90..1a73783251 100644 --- a/packages/backend/migrations/20221012205150_create_quotes_table.js +++ b/packages/backend/migrations/20221012205150_create_quotes_table.js @@ -26,7 +26,7 @@ exports.up = function (knex) { table.uuid('assetId').notNullable() table.foreign('assetId').references('assets.id') - table.string('clientId').nullable() + table.string('client').nullable() table.timestamp('createdAt').defaultTo(knex.fn.now()) table.timestamp('updatedAt').defaultTo(knex.fn.now()) diff --git a/packages/backend/migrations/20221129213751_create_outgoing_payments_table.js b/packages/backend/migrations/20221129213751_create_outgoing_payments_table.js index ebdf419a56..989672cfa2 100644 --- a/packages/backend/migrations/20221129213751_create_outgoing_payments_table.js +++ b/packages/backend/migrations/20221129213751_create_outgoing_payments_table.js @@ -9,7 +9,7 @@ exports.up = function (knex) { table.string('description').nullable() table.string('externalRef').nullable() - table.string('clientId').nullable() + table.string('client').nullable() table.string('grantId').nullable() table.foreign('grantId').references('outgoingPaymentGrants.id') diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 029b816278..824d27b26b 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -4,7 +4,6 @@ import { EventEmitter } from 'events' import { ParsedUrlQuery } from 'querystring' import { IocContract } from '@adonisjs/fold' -import { JWK } from 'http-signature-utils' import { Knex } from 'knex' import Koa, { DefaultState } from 'koa' import bodyParser from 'koa-bodyparser' @@ -47,7 +46,7 @@ import { PageQueryParams } from './shared/pagination' import { IlpPlugin, IlpPluginOptions } from './shared/ilp_plugin' import { createValidatorMiddleware, HttpMethod, isHttpMethod } from 'openapi' import { PaymentPointerKeyService } from './open_payments/payment_pointer/key/service' -import { AccessType, AuthenticatedClient } from 'open-payments' +import { AccessAction, AccessType, AuthenticatedClient } from 'open-payments' import { RemoteIncomingPaymentService } from './open_payments/payment/incoming_remote/service' import { ReceiverService } from './open_payments/receiver/service' import { Client as TokenIntrospectionClient } from 'token-introspection' @@ -78,8 +77,8 @@ export type AppRequest = Omit< export interface PaymentPointerContext extends AppContext { paymentPointer: PaymentPointer grant?: Grant - clientId?: string - clientKey?: JWK + client?: string + accessAction?: AccessAction } export type PaymentPointerKeysContext = Omit< @@ -98,9 +97,7 @@ type HttpSigRequest = Omit & { export type HttpSigContext = AppContext & { request: HttpSigRequest headers: HttpSigHeaders - grant: Grant - clientKey: JWK - clientId?: string + client: string } // Payment pointer subresources @@ -114,17 +111,24 @@ type CollectionRequest = Omit< type CollectionContext = Omit< PaymentPointerContext, - 'request' + 'request' | 'client' | 'accessAction' > & { request: CollectionRequest + client: NonNullable + accessAction: NonNullable } type SubresourceRequest = Omit & { params: Record<'id', string> } -type SubresourceContext = Omit & { +type SubresourceContext = Omit< + PaymentPointerContext, + 'request' | 'grant' | 'client' | 'accessAction' +> & { request: SubresourceRequest + client: NonNullable + accessAction: NonNullable } export type CreateContext = CollectionContext @@ -366,11 +370,11 @@ export class App { router.get( PAYMENT_POINTER_PATH + '/jwks.json', createPaymentPointerMiddleware(), - createValidatorMiddleware(resourceServerSpec, { + createValidatorMiddleware(resourceServerSpec, { path: '/jwks.json', method: HttpMethod.GET }), - async (ctx: PaymentPointerContext): Promise => + async (ctx: PaymentPointerKeysContext): Promise => await paymentPointerKeyRoutes.getKeysByPaymentPointerId(ctx) ) diff --git a/packages/backend/src/open_payments/auth/middleware.test.ts b/packages/backend/src/open_payments/auth/middleware.test.ts index e1e0205b34..c0f8bd55b4 100644 --- a/packages/backend/src/open_payments/auth/middleware.test.ts +++ b/packages/backend/src/open_payments/auth/middleware.test.ts @@ -1,12 +1,9 @@ import { generateKeyPairSync } from 'crypto' import { faker } from '@faker-js/faker' +import nock from 'nock' import { Client, ActiveTokenInfo } from 'token-introspection' import { v4 as uuid } from 'uuid' -import { - generateJwk, - generateTestKeys, - createHeaders -} from 'http-signature-utils' +import { generateJwk, createHeaders, JWK } from 'http-signature-utils' import { createTokenIntrospectionMiddleware, @@ -37,10 +34,6 @@ describe('Auth Middleware', (): void => { let middleware: AppMiddleware let ctx: PaymentPointerContext let tokenIntrospectionClient: Client - const key: ActiveTokenInfo['key'] = { - jwk: generateTestKeys().publicKey, - proof: 'httpsig' - } const type = AccessType.IncomingPayment const action: AccessAction = 'create' @@ -156,15 +149,14 @@ describe('Auth Middleware', (): void => { ): ActiveTokenInfo => ({ active: true, grant: uuid(), - client_id: uuid(), + client: faker.internet.url(), access: access ?? [ { type: 'incoming-payment', actions: [action], identifier } - ], - key + ] }) test.each` @@ -207,8 +199,7 @@ describe('Auth Middleware', (): void => { access_token: token }) expect(next).toHaveBeenCalled() - expect(ctx.clientId).toEqual(tokenInfo.client_id) - expect(ctx.clientKey).toEqual(tokenInfo.key.jwk) + expect(ctx.client).toEqual(tokenInfo.client) expect(ctx.grant).toBeUndefined() }) @@ -217,7 +208,7 @@ describe('Auth Middleware', (): void => { ${AccessAction.ReadAll} | ${AccessAction.Read} ${AccessAction.ListAll} | ${AccessAction.List} `('$subAction/$superAction', ({ superAction, subAction }): void => { - test("calls next (but doesn't restrict ctx.clientId) for sub-action request", async (): Promise => { + test("calls next (but doesn't designate client filtering) for sub-action request", async (): Promise => { const middleware = createTokenIntrospectionMiddleware({ requestType: type, requestAction: subAction @@ -238,8 +229,8 @@ describe('Auth Middleware', (): void => { access_token: token }) expect(next).toHaveBeenCalled() - expect(ctx.clientId).toBeUndefined() - expect(ctx.clientKey).toEqual(tokenInfo.key.jwk) + expect(ctx.client).toEqual(tokenInfo.client) + expect(ctx.accessAction).toBe(superAction) expect(ctx.grant).toBeUndefined() }) @@ -320,8 +311,8 @@ describe('Auth Middleware', (): void => { access_token: token }) expect(next).toHaveBeenCalled() - expect(ctx.clientId).toEqual(tokenInfo.client_id) - expect(ctx.clientKey).toEqual(tokenInfo.key.jwk) + expect(ctx.client).toEqual(tokenInfo.client) + expect(ctx.accessAction).toBe(action) expect(ctx.grant).toEqual( ctxGrant ? { @@ -357,6 +348,7 @@ describe('HTTP Signature Middleware', (): void => { let deps: IocContract let appContainer: TestContainer let ctx: HttpSigContext + let key: JWK beforeAll(async (): Promise => { deps = await initIocContainer(Config) @@ -403,52 +395,85 @@ describe('HTTP Signature Middleware', (): void => { url }) ctx.container = deps - ctx.clientKey = generateJwk({ + ctx.client = faker.internet.url() + key = generateJwk({ keyId, privateKey }) }) test('calls next with valid http signature', async (): Promise => { + const scope = nock(ctx.client) + .get('/jwks.json') + .reply(200, { + keys: [key] + }) await expect(httpsigMiddleware(ctx, next)).resolves.toBeUndefined() expect(next).toHaveBeenCalled() + scope.done() }) - test('returns 401 for invalid http signature', async (): Promise => { - ctx.request.headers['signature'] = 'aaaaaaaaaa=' + test('returns 401 for missing keyid', async (): Promise => { + ctx.request.headers['signature-input'] = 'aaaaaaaaaa' await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({ status: 401, - message: 'Invalid signature' + message: 'Invalid signature input' }) expect(next).not.toHaveBeenCalled() }) - test('returns 401 for invalid key type', async (): Promise => { - ctx.clientKey.kty = 'EC' as 'OKP' + test('returns 401 for failed client key request', async (): Promise => { + await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({ + status: 401, + message: 'Invalid signature input' + }) + expect(next).not.toHaveBeenCalled() + }) + + test('returns 401 for invalid http signature', async (): Promise => { + const scope = nock(ctx.client) + .get('/jwks.json') + .reply(200, { + keys: [key] + }) + ctx.request.headers['signature'] = 'aaaaaaaaaa=' await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({ status: 401, message: 'Invalid signature' }) expect(next).not.toHaveBeenCalled() + scope.done() }) - // TODO: remove with - // https://github.com/interledger/rafiki/issues/737 - test.skip('returns 401 if any signature keyid does not match the jwk key id', async (): Promise => { - ctx.clientKey.kid = 'mismatched-key' - await expect(httpsigMiddleware(ctx, next)).resolves.toBeUndefined() - expect(ctx.status).toBe(401) + test('returns 401 for invalid key type', async (): Promise => { + key.kty = 'EC' as 'OKP' + const scope = nock(ctx.client) + .get('/jwks.json') + .reply(200, { + keys: [key] + }) + await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({ + status: 401, + message: 'Invalid signature input' + }) expect(next).not.toHaveBeenCalled() + scope.done() }) if (body) { test('returns 401 if content-digest does not match the body', async (): Promise => { + const scope = nock(ctx.client) + .get('/jwks.json') + .reply(200, { + keys: [key] + }) ctx.request.headers['content-digest'] = 'aaaaaaaaaa=' await expect(httpsigMiddleware(ctx, next)).rejects.toMatchObject({ status: 401, message: 'Invalid signature' }) expect(next).not.toHaveBeenCalled() + scope.done() }) } }) diff --git a/packages/backend/src/open_payments/auth/middleware.ts b/packages/backend/src/open_payments/auth/middleware.ts index b99251a85d..cf1067c6db 100644 --- a/packages/backend/src/open_payments/auth/middleware.ts +++ b/packages/backend/src/open_payments/auth/middleware.ts @@ -1,8 +1,8 @@ -import { RequestLike, validateSignature } from 'http-signature-utils' +import { getKeyId, RequestLike, validateSignature } from 'http-signature-utils' import Koa from 'koa' import { Limits, parseLimits } from '../payment/outgoing/limits' import { HttpSigContext, PaymentPointerContext } from '../../app' -import { AccessAction, AccessType } from 'open-payments' +import { AccessAction, AccessType, JWKS } from 'open-payments' import { TokenInfo } from 'token-introspection' import { isActiveTokenInfo } from 'token-introspection' @@ -66,8 +66,6 @@ export function createTokenIntrospectionMiddleware({ ctx.throw(403, 'Inactive Token') } - ctx.clientKey = tokenInfo.key.jwk - // TODO // https://github.com/interledger/rafiki/issues/835 const access = tokenInfo.access.find((access: Access) => { @@ -81,19 +79,19 @@ export function createTokenIntrospectionMiddleware({ requestAction === AccessAction.Read && access.actions.includes(AccessAction.ReadAll) ) { + ctx.accessAction = AccessAction.ReadAll return true } if ( requestAction === AccessAction.List && access.actions.includes(AccessAction.ListAll) ) { + ctx.accessAction = AccessAction.ListAll return true } return access.actions.find((tokenAction: AccessAction) => { if (isActiveTokenInfo(tokenInfo) && tokenAction === requestAction) { - // Unless the relevant token action is ReadAll/ListAll add the - // clientId to ctx for Read/List filtering - ctx.clientId = tokenInfo.client_id + ctx.accessAction = requestAction return true } return false @@ -103,6 +101,7 @@ export function createTokenIntrospectionMiddleware({ if (!access) { ctx.throw(403, 'Insufficient Grant') } + ctx.client = tokenInfo.client if ( requestType === AccessType.OutgoingPayment && requestAction === AccessAction.Create @@ -129,20 +128,34 @@ export const httpsigMiddleware = async ( ctx: HttpSigContext, next: () => Promise ): Promise => { - // TODO: look up client jwks.json - // https://github.com/interledger/rafiki/issues/737 - if (!ctx.clientKey) { + const keyId = getKeyId(ctx.request.headers['signature-input']) + if (!keyId) { + ctx.throw(401, 'Invalid signature input') + } + // TODO + // cache client key(s) + let jwks: JWKS | undefined + try { + const openPaymentsClient = await ctx.container.use('openPaymentsClient') + jwks = await openPaymentsClient.paymentPointer.getKeys({ + url: ctx.client + }) + } catch (error) { const logger = await ctx.container.use('logger') - logger.warn( + logger.debug( { - grant: ctx.grant + error, + client: ctx.client }, - 'missing grant key' + 'retrieving client key' ) - ctx.throw(500) + } + const key = jwks?.keys.find((key) => key.kid === keyId) + if (!key) { + ctx.throw(401, 'Invalid signature input') } try { - if (!(await validateSignature(ctx.clientKey, contextToRequestLike(ctx)))) { + if (!(await validateSignature(key, contextToRequestLike(ctx)))) { ctx.throw(401, 'Invalid signature') } } catch (err) { diff --git a/packages/backend/src/open_payments/payment/incoming/routes.test.ts b/packages/backend/src/open_payments/payment/incoming/routes.test.ts index 1e41a57c77..40410ea4b5 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.test.ts @@ -1,5 +1,5 @@ +import { faker } from '@faker-js/faker' import jestOpenAPI from 'jest-openapi' -import { v4 as uuid } from 'uuid' import { Amount, AmountJSON, parseAmount, serializeAmount } from '../../amount' import { PaymentPointer } from '../../payment_pointer/model' @@ -72,10 +72,10 @@ describe('Incoming Payment Routes', (): void => { describe('get/list', (): void => { getRouteTests({ getPaymentPointer: async () => paymentPointer, - createModel: async ({ clientId }) => + createModel: async ({ client }) => createIncomingPayment(deps, { paymentPointerId: paymentPointer.id, - clientId, + client, description, expiresAt, incomingAmount, @@ -155,13 +155,13 @@ describe('Incoming Payment Routes', (): void => { }) test.each` - clientId | incomingAmount | description | externalRef | expiresAt - ${uuid()} | ${true} | ${'text'} | ${'#123'} | ${new Date(Date.now() + 30_000).toISOString()} - ${undefined} | ${false} | ${undefined} | ${undefined} | ${undefined} + client | incomingAmount | description | externalRef | expiresAt + ${faker.internet.url()} | ${true} | ${'text'} | ${'#123'} | ${new Date(Date.now() + 30_000).toISOString()} + ${undefined} | ${false} | ${undefined} | ${undefined} | ${undefined} `( 'returns the incoming payment on success', async ({ - clientId, + client, incomingAmount, description, externalRef, @@ -179,7 +179,7 @@ describe('Incoming Payment Routes', (): void => { url: `/incoming-payments` }, paymentPointer, - clientId + client }) const incomingPaymentService = await deps.use('incomingPaymentService') const createSpy = jest.spyOn(incomingPaymentService, 'create') @@ -190,7 +190,7 @@ describe('Incoming Payment Routes', (): void => { description, externalRef, expiresAt: expiresAt ? new Date(expiresAt) : undefined, - clientId + client }) expect(ctx.response).toSatisfyApiSpec() const incomingPaymentId = ( diff --git a/packages/backend/src/open_payments/payment/incoming/routes.ts b/packages/backend/src/open_payments/payment/incoming/routes.ts index 00464d2da5..439a155b28 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.ts @@ -1,3 +1,4 @@ +import { AccessAction } from 'open-payments' import { Logger } from 'pino' import { ReadContext, @@ -61,7 +62,7 @@ async function getIncomingPayment( try { incomingPayment = await deps.incomingPaymentService.get({ id: ctx.params.id, - clientId: ctx.clientId, + client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, paymentPointerId: ctx.paymentPointer.id }) } catch (err) { @@ -94,7 +95,7 @@ async function createIncomingPayment( const incomingPaymentOrError = await deps.incomingPaymentService.create({ paymentPointerId: ctx.paymentPointer.id, - clientId: ctx.clientId, + client: ctx.client, description: body.description, externalRef: body.externalRef, expiresAt, diff --git a/packages/backend/src/open_payments/payment/incoming/service.test.ts b/packages/backend/src/open_payments/payment/incoming/service.test.ts index b4421ca07a..409a07b88b 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.test.ts @@ -1,4 +1,5 @@ import assert from 'assert' +import { faker } from '@faker-js/faker' import { Knex } from 'knex' import { v4 as uuid } from 'uuid' @@ -68,9 +69,9 @@ describe('Incoming Payment Service', (): void => { }) test.each` - clientId | incomingAmount | expiresAt | description | externalRef - ${undefined} | ${false} | ${undefined} | ${undefined} | ${undefined} - ${uuid()} | ${true} | ${new Date(Date.now() + 30_000)} | ${'Test incoming payment'} | ${'#123'} + client | incomingAmount | expiresAt | description | externalRef + ${undefined} | ${false} | ${undefined} | ${undefined} | ${undefined} + ${faker.internet.url()} | ${true} | ${new Date(Date.now() + 30_000)} | ${'Test incoming payment'} | ${'#123'} `('An incoming payment can be created', async (options): Promise => { const incomingPayment = await incomingPaymentService.create({ paymentPointerId, @@ -178,10 +179,10 @@ describe('Incoming Payment Service', (): void => { describe('get/getPaymentPointerPage', (): void => { getTests({ - createModel: ({ clientId }) => + createModel: ({ client }) => createIncomingPayment(deps, { paymentPointerId, - clientId, + client, incomingAmount: { value: BigInt(123), assetCode: asset.code, diff --git a/packages/backend/src/open_payments/payment/incoming/service.ts b/packages/backend/src/open_payments/payment/incoming/service.ts index a9fb9fdb53..f3d9c0f0db 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.ts @@ -28,7 +28,7 @@ export const EXPIRY = parse('P90D') // 90 days in future export interface CreateIncomingPaymentOptions { paymentPointerId: string - clientId?: string + client?: string description?: string expiresAt?: Date incomingAmount?: Amount @@ -99,7 +99,7 @@ async function createIncomingPayment( deps: ServiceDependencies, { paymentPointerId, - clientId, + client, description, expiresAt, incomingAmount, @@ -130,7 +130,7 @@ async function createIncomingPayment( const incomingPayment = await IncomingPayment.query(trx || deps.knex) .insertAndFetch({ paymentPointerId, - clientId, + client, assetId: paymentPointer.asset.id, description, expiresAt, diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts index c946de6f62..a7f154b55b 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts @@ -1,3 +1,4 @@ +import { faker } from '@faker-js/faker' import jestOpenAPI from 'jest-openapi' import { Knex } from 'knex' import { v4 as uuid } from 'uuid' @@ -35,7 +36,7 @@ describe('Outgoing Payment Routes', (): void => { const receivingPaymentPointer = `https://wallet.example/${uuid()}` const createPayment = async (options: { - clientId?: string + client?: string grant?: Grant description?: string externalRef?: string @@ -85,9 +86,9 @@ describe('Outgoing Payment Routes', (): void => { `('get/list$description outgoing payment', ({ failed }): void => { getRouteTests({ getPaymentPointer: async () => paymentPointer, - createModel: async ({ clientId }) => { + createModel: async ({ client }) => { const outgoingPayment = await createPayment({ - clientId, + client, description: 'rent', externalRef: '202201' }) @@ -146,15 +147,15 @@ describe('Outgoing Payment Routes', (): void => { body: options }, paymentPointer, - clientId: options.clientId, + client: options.client, grant: options.grant }) describe.each` - grant | clientId | description - ${{ id: uuid() }} | ${uuid()} | ${'grant'} - ${undefined} | ${undefined} | ${'no grant'} - `('create ($description)', ({ grant, clientId }): void => { + grant | client | description + ${{ id: uuid() }} | ${faker.internet.url()} | ${'grant'} + ${undefined} | ${undefined} | ${'no grant'} + `('create ($description)', ({ grant, client }): void => { test.each` description | externalRef | desc ${'rent'} | ${undefined} | ${'description'} @@ -163,14 +164,14 @@ describe('Outgoing Payment Routes', (): void => { 'returns the outgoing payment on success ($desc)', async ({ description, externalRef }): Promise => { const payment = await createPayment({ - clientId, + client, grant, description, externalRef }) const options = { quoteId: `${paymentPointer.url}/quotes/${payment.quote.id}`, - clientId, + client, grant, description, externalRef @@ -187,7 +188,7 @@ describe('Outgoing Payment Routes', (): void => { quoteId: payment.quote.id, description, externalRef, - clientId, + client, grant }) expect(ctx.response).toSatisfyApiSpec() diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.ts b/packages/backend/src/open_payments/payment/outgoing/routes.ts index fcf817c06e..21e3c529a4 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.ts @@ -1,3 +1,4 @@ +import { AccessAction } from 'open-payments' import { Logger } from 'pino' import { ReadContext, CreateContext, ListContext } from '../../../app' import { IAppConfig } from '../../../config/app' @@ -42,7 +43,7 @@ async function getOutgoingPayment( try { outgoingPayment = await deps.outgoingPaymentService.get({ id: ctx.params.id, - clientId: ctx.clientId, + client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, paymentPointerId: ctx.paymentPointer.id }) } catch (_) { @@ -76,7 +77,7 @@ async function createOutgoingPayment( quoteId, description: body.description, externalRef: body.externalRef, - clientId: ctx.clientId, + client: ctx.client, grant: ctx.grant }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 9b31410047..6fa087e86d 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -1,4 +1,5 @@ import assert from 'assert' +import { faker } from '@faker-js/faker' import nock from 'nock' import { Knex } from 'knex' import * as Pay from '@interledger/pay' @@ -282,10 +283,10 @@ describe('OutgoingPaymentService', (): void => { describe('get/getPaymentPointerPage', (): void => { getTests({ - createModel: ({ clientId }) => + createModel: ({ client }) => createOutgoingPayment(deps, { paymentPointerId, - clientId, + client, receiver, sendAmount, validDestination: false @@ -309,14 +310,14 @@ describe('OutgoingPaymentService', (): void => { ${GrantOption.None} `('$grantOption grant', ({ grantOption }): void => { let grant: Grant | undefined - let clientId: string | undefined + let client: string | undefined beforeEach(async (): Promise => { if (grantOption !== GrantOption.None) { grant = { id: uuid() } - clientId = uuid() + client = faker.internet.url() if (grantOption === GrantOption.Existing) { await OutgoingPaymentGrant.query(knex).insertAndFetch({ id: grant.id @@ -564,7 +565,7 @@ describe('OutgoingPaymentService', (): void => { quoteId: quote.id, description: 'rent', externalRef: '202201', - clientId + client } const start = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000) interval = `R0/${start.toISOString()}/P1M` @@ -747,7 +748,7 @@ describe('OutgoingPaymentService', (): void => { }/${uuid()}/incoming-payments/${uuid()}`, sendAmount: sendAmount ? paymentAmount : undefined, receiveAmount: sendAmount ? undefined : paymentAmount, - clientId, + client, grant, validDestination: false }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index bcb0bb0b55..f8c8c36edb 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -83,7 +83,7 @@ async function getOutgoingPayment( export interface CreateOutgoingPaymentOptions { paymentPointerId: string quoteId: string - clientId?: string + client?: string grant?: Grant description?: string externalRef?: string @@ -112,7 +112,7 @@ async function createOutgoingPayment( description: options.description, externalRef: options.externalRef, state: OutgoingPaymentState.Funding, - clientId: options.clientId, + client: options.client, grantId }) .withGraphFetched('[quote.asset]') diff --git a/packages/backend/src/open_payments/payment_pointer/model.test.ts b/packages/backend/src/open_payments/payment_pointer/model.test.ts index 0066d4f75d..5ae2ae0bd1 100644 --- a/packages/backend/src/open_payments/payment_pointer/model.test.ts +++ b/packages/backend/src/open_payments/payment_pointer/model.test.ts @@ -1,4 +1,6 @@ +import { faker } from '@faker-js/faker' import * as httpMocks from 'node-mocks-http' +import { AccessAction } from 'open-payments' import { v4 as uuid } from 'uuid' import { @@ -17,7 +19,8 @@ export interface SetupOptions { params?: Record paymentPointer: PaymentPointer grant?: Grant - clientId?: string + client?: string + accessAction?: AccessAction } export const setup = ( @@ -35,7 +38,8 @@ export const setup = ( ) ctx.paymentPointer = options.paymentPointer ctx.grant = options.grant - ctx.clientId = options.clientId + ctx.client = options.client + ctx.accessAction = options.accessAction return ctx } @@ -44,7 +48,7 @@ interface TestGetOptions extends GetOptions { } interface BaseTestsOptions { - createModel: (options: { clientId?: string }) => Promise + createModel: (options: { client?: string }) => Promise testGet: (options: TestGetOptions, expectedMatch?: M) => void testList?: (options: ListOptions, expectedMatch?: M) => void } @@ -61,29 +65,29 @@ const baseGetTests = ({ } describe.each` - withClientId | description - ${true} | ${'with clientId'} - ${false} | ${'without clientId'} + withClient | description + ${true} | ${'with client'} + ${false} | ${'without client'} `( 'Common PaymentPointerSubresource get/getPaymentPointerPage ($description)', - ({ withClientId }): void => { - const resourceClientId = uuid() + ({ withClient }): void => { + const resourceClient = faker.internet.url() describe.each` - clientId | match | description - ${resourceClientId} | ${true} | ${GetOption.Matching} - ${uuid()} | ${false} | ${GetOption.Conflicting} - ${undefined} | ${true} | ${GetOption.Unspecified} - `('$description clientId', ({ clientId, match, description }): void => { - // Do not test matching clientId if model has no clientId - if (withClientId || description !== GetOption.Matching) { + client | match | description + ${resourceClient} | ${true} | ${GetOption.Matching} + ${faker.internet.url()} | ${false} | ${GetOption.Conflicting} + ${undefined} | ${true} | ${GetOption.Unspecified} + `('$description client', ({ client, match, description }): void => { + // Do not test matching client if model has no client + if (withClient || description !== GetOption.Matching) { let model: M // This beforeEach needs to be inside the above if statement to avoid: // Invalid: beforeEach() may not be used in a describe block containing no tests. beforeEach(async (): Promise => { model = await createModel({ - clientId: withClientId ? resourceClientId : undefined + client: withClient ? resourceClient : undefined }) }) describe.each` @@ -122,7 +126,7 @@ const baseGetTests = ({ await testGet( { id, - clientId, + client, paymentPointerId }, match ? model : undefined @@ -136,7 +140,7 @@ const baseGetTests = ({ await testList( { paymentPointerId, - clientId + client }, match ? model : undefined ) @@ -163,7 +167,7 @@ export const getTests = ({ createModel, testGet: (options, expectedMatch) => expect(get(options)).resolves.toEqual(expectedMatch), - // tests paymentPointerId / clientId filtering + // tests paymentPointerId / client filtering testList: (options, expectedMatch) => expect(list(options)).resolves.toEqual([expectedMatch]) }) @@ -204,7 +208,7 @@ export const getRouteTests = ({ urlPath }: RouteTestsOptions): void => { const testList = async ( - { paymentPointerId, clientId }: ListOptions, + { paymentPointerId, client }: ListOptions, expectedMatch?: M ) => { const paymentPointer = await getPaymentPointer() @@ -216,7 +220,8 @@ export const getRouteTests = ({ url: urlPath }, paymentPointer, - clientId + client, + accessAction: client ? AccessAction.List : AccessAction.ListAll }) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await expect(list!(ctx)).resolves.toBeUndefined() @@ -237,7 +242,7 @@ export const getRouteTests = ({ baseGetTests({ createModel, - testGet: async ({ id, paymentPointerId, clientId }, expectedMatch) => { + testGet: async ({ id, paymentPointerId, client }, expectedMatch) => { const paymentPointer = await getPaymentPointer() paymentPointer.id = paymentPointerId const ctx = setup({ @@ -250,7 +255,8 @@ export const getRouteTests = ({ id }, paymentPointer, - clientId + client, + accessAction: client ? AccessAction.Read : AccessAction.ReadAll }) if (expectedMatch) { await expect(get(ctx)).resolves.toBeUndefined() @@ -263,7 +269,7 @@ export const getRouteTests = ({ }) } }, - // tests paymentPointerId / clientId filtering + // tests paymentPointerId / client filtering testList: list && testList }) @@ -306,7 +312,8 @@ export const getRouteTests = ({ query, url: urlPath }, - paymentPointer: await getPaymentPointer() + paymentPointer: await getPaymentPointer(), + accessAction: AccessAction.ListAll }) await expect(list(ctx)).resolves.toBeUndefined() expect(ctx.response).toSatisfyApiSpec() diff --git a/packages/backend/src/open_payments/payment_pointer/model.ts b/packages/backend/src/open_payments/payment_pointer/model.ts index aa4221bc50..26df46bf9a 100644 --- a/packages/backend/src/open_payments/payment_pointer/model.ts +++ b/packages/backend/src/open_payments/payment_pointer/model.ts @@ -119,13 +119,13 @@ export class PaymentPointerEvent extends WebhookEvent { export interface GetOptions { id: string - clientId?: string + client?: string paymentPointerId?: string } export interface ListOptions { paymentPointerId: string - clientId?: string + client?: string pagination?: Pagination } @@ -139,21 +139,21 @@ class SubresourceQueryBuilder< NumberQueryBuilderType!: SubresourceQueryBuilder PageQueryBuilderType!: SubresourceQueryBuilder> - get({ id, paymentPointerId, clientId }: GetOptions) { + get({ id, paymentPointerId, client }: GetOptions) { if (paymentPointerId) { this.where( `${this.modelClass().tableName}.paymentPointerId`, paymentPointerId ) } - if (clientId) { - this.where({ clientId }) + if (client) { + this.where({ client }) } return this.findById(id) } - list({ paymentPointerId, clientId, pagination }: ListOptions) { - if (clientId) { - this.where({ clientId }) + list({ paymentPointerId, client, pagination }: ListOptions) { + if (client) { + this.where({ client }) } return this.getPage(pagination).where( `${this.modelClass().tableName}.paymentPointerId`, @@ -171,7 +171,7 @@ export abstract class PaymentPointerSubresource extends BaseModel { public abstract readonly assetId: string public abstract asset: Asset - public readonly clientId?: string + public readonly client?: string static get relationMappings() { return { diff --git a/packages/backend/src/open_payments/payment_pointer/routes.ts b/packages/backend/src/open_payments/payment_pointer/routes.ts index ac1c47129d..e095e9a016 100644 --- a/packages/backend/src/open_payments/payment_pointer/routes.ts +++ b/packages/backend/src/open_payments/payment_pointer/routes.ts @@ -1,3 +1,4 @@ +import { AccessAction } from 'open-payments' import { PaymentPointerSubresource } from './model' import { PaymentPointerSubresourceService } from './service' import { PaymentPointerContext, ListContext } from '../../app' @@ -48,17 +49,18 @@ export const listSubresource = async ({ toBody }: ListSubresourceOptions) => { const pagination = parsePaginationQueryParameters(ctx.request.query) + const client = ctx.accessAction === AccessAction.List ? ctx.client : undefined const page = await getPaymentPointerPage({ paymentPointerId: ctx.paymentPointer.id, pagination, - clientId: ctx.clientId + client }) const pageInfo = await getPageInfo( (pagination) => getPaymentPointerPage({ paymentPointerId: ctx.paymentPointer.id, pagination, - clientId: ctx.clientId + client }), page ) diff --git a/packages/backend/src/open_payments/quote/routes.test.ts b/packages/backend/src/open_payments/quote/routes.test.ts index d3ed0a8d91..4088916bbe 100644 --- a/packages/backend/src/open_payments/quote/routes.test.ts +++ b/packages/backend/src/open_payments/quote/routes.test.ts @@ -1,4 +1,5 @@ import assert from 'assert' +import { faker } from '@faker-js/faker' import jestOpenAPI from 'jest-openapi' import { v4 as uuid } from 'uuid' import { IocContract } from '@adonisjs/fold' @@ -39,10 +40,10 @@ describe('Quote Routes', (): void => { const createPaymentPointerQuote = async ({ paymentPointerId, - clientId + client }: { paymentPointerId: string - clientId?: string + client?: string }): Promise => { return await createQuote(deps, { paymentPointerId, @@ -52,7 +53,7 @@ describe('Quote Routes', (): void => { assetCode: asset.code, assetScale: asset.scale }, - clientId, + client, validDestination: false }) } @@ -89,10 +90,10 @@ describe('Quote Routes', (): void => { describe('get', (): void => { getRouteTests({ getPaymentPointer: async () => paymentPointer, - createModel: async ({ clientId }) => + createModel: async ({ client }) => createPaymentPointerQuote({ paymentPointerId: paymentPointer.id, - clientId + client }), get: (ctx) => quoteRoutes.get(ctx), getBody: (quote) => ({ @@ -112,9 +113,9 @@ describe('Quote Routes', (): void => { let options: CreateBody const setup = ({ - clientId + client }: { - clientId?: string + client?: string }): CreateContext => setupContext>({ reqOpts: { @@ -123,7 +124,7 @@ describe('Quote Routes', (): void => { url: `/quotes` }, paymentPointer, - clientId + client }) test('returns error on invalid sendAmount asset', async (): Promise => { @@ -154,10 +155,10 @@ describe('Quote Routes', (): void => { }) describe.each` - clientId | description - ${uuid()} | ${'clientId'} - ${undefined} | ${'no clientId'} - `('returns the quote on success ($description)', ({ clientId }): void => { + client | description + ${faker.internet.url()} | ${'client'} + ${undefined} | ${'no client'} + `('returns the quote on success ($description)', ({ client }): void => { test.each` sendAmount | receiveAmount | description ${'123'} | ${undefined} | ${'sendAmount'} @@ -180,7 +181,7 @@ describe('Quote Routes', (): void => { assetCode: asset.code, assetScale: asset.scale } - const ctx = setup({ clientId }) + const ctx = setup({ client }) let quote: Quote | undefined const quoteSpy = jest .spyOn(quoteService, 'create') @@ -188,7 +189,7 @@ describe('Quote Routes', (): void => { quote = await createQuote(deps, { ...opts, validDestination: false, - clientId + client }) return quote }) @@ -204,7 +205,7 @@ describe('Quote Routes', (): void => { ...options.receiveAmount, value: BigInt(options.receiveAmount.value) }, - clientId + client }) expect(ctx.response).toSatisfyApiSpec() const quoteId = ( @@ -235,7 +236,7 @@ describe('Quote Routes', (): void => { options = { receiver } - const ctx = setup({ clientId }) + const ctx = setup({ client }) let quote: Quote | undefined const quoteSpy = jest .spyOn(quoteService, 'create') @@ -243,7 +244,7 @@ describe('Quote Routes', (): void => { quote = await createQuote(deps, { ...opts, validDestination: false, - clientId + client }) return quote }) @@ -251,7 +252,7 @@ describe('Quote Routes', (): void => { expect(quoteSpy).toHaveBeenCalledWith({ paymentPointerId: paymentPointer.id, receiver, - clientId + client }) expect(ctx.response).toSatisfyApiSpec() const quoteId = ( diff --git a/packages/backend/src/open_payments/quote/routes.ts b/packages/backend/src/open_payments/quote/routes.ts index 6e578a79dc..8495477499 100644 --- a/packages/backend/src/open_payments/quote/routes.ts +++ b/packages/backend/src/open_payments/quote/routes.ts @@ -1,3 +1,4 @@ +import { AccessAction } from 'open-payments' import { Logger } from 'pino' import { ReadContext, CreateContext } from '../../app' import { IAppConfig } from '../../config/app' @@ -35,7 +36,7 @@ async function getQuote( ): Promise { const quote = await deps.quoteService.get({ id: ctx.params.id, - clientId: ctx.clientId, + client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, paymentPointerId: ctx.paymentPointer.id }) if (!quote) return ctx.throw(404) @@ -67,7 +68,7 @@ async function createQuote( const options: CreateQuoteOptions = { paymentPointerId: ctx.paymentPointer.id, receiver: body.receiver, - clientId: ctx.clientId + client: ctx.client } if (body.sendAmount) options.sendAmount = parseAmount(body.sendAmount) if (body.receiveAmount) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index eef6fcceab..0eadf95b66 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -1,4 +1,5 @@ import assert from 'assert' +import { faker } from '@faker-js/faker' import nock, { Definition } from 'nock' import { Knex } from 'knex' import * as Pay from '@interledger/pay' @@ -122,7 +123,7 @@ describe('QuoteService', (): void => { describe('get/getPaymentPointerPage', (): void => { getTests({ - createModel: ({ clientId }) => + createModel: ({ client }) => createQuote(deps, { paymentPointerId, receiver: `${ @@ -133,7 +134,7 @@ describe('QuoteService', (): void => { assetCode: asset.code, assetScale: asset.scale }, - clientId, + client, validDestination: false }), get: (options) => quoteService.get(options), @@ -234,7 +235,7 @@ describe('QuoteService', (): void => { let options: CreateQuoteOptions let incomingPayment: IncomingPayment let expected: ExpectedQuote - const clientId = uuid() + const client = faker.internet.url() beforeEach(async (): Promise => { incomingPayment = await createIncomingPayment(deps, { @@ -265,18 +266,18 @@ describe('QuoteService', (): void => { } else { if (sendAmount || receiveAmount) { it.each` - clientId | description - ${clientId} | ${'with a clientId'} - ${undefined} | ${'without a clientId'} + client | description + ${client} | ${'with a client'} + ${undefined} | ${'without a client'} `( 'creates a Quote $description', - async ({ clientId }): Promise => { + async ({ client }): Promise => { const walletScope = mockWalletQuote({ expected }) const quote = await quoteService.create({ ...options, - clientId + client }) assert.ok(!isQuoteError(quote)) walletScope.done() @@ -309,7 +310,7 @@ describe('QuoteService', (): void => { expiresAt: new Date( quote.createdAt.getTime() + config.quoteLifespan ), - clientId: clientId || null + client: client || null }) expect(quote.minExchangeRate.valueOf()).toBe( 0.5 * (1 - config.slippage) @@ -350,18 +351,18 @@ describe('QuoteService', (): void => { } else { if (incomingAmount) { it.each` - clientId | description - ${clientId} | ${'with a clientId'} - ${undefined} | ${'without a clientId'} + client | description + ${client} | ${'with a client'} + ${undefined} | ${'without a client'} `( 'creates a Quote $description', - async ({ clientId }): Promise => { + async ({ client }): Promise => { const scope = mockWalletQuote({ expected }) const quote = await quoteService.create({ ...options, - clientId + client }) scope.done() assert.ok(!isQuoteError(quote)) @@ -384,7 +385,7 @@ describe('QuoteService', (): void => { expiresAt: new Date( quote.createdAt.getTime() + config.quoteLifespan ), - clientId: clientId || null + client: client || null }) expect(quote.minExchangeRate.valueOf()).toBe( 0.5 * (1 - config.slippage) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index b87a6f936b..34d3a0ea31 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -64,7 +64,7 @@ async function getQuote( interface QuoteOptionsBase { paymentPointerId: string receiver: string - clientId?: string + client?: string } interface QuoteOptionsWithSendAmount extends QuoteOptionsBase { @@ -143,7 +143,7 @@ async function createQuote( highEstimatedExchangeRate: ilpQuote.highEstimatedExchangeRate, // Patch using createdAt below expiresAt: new Date(0), - clientId: options.clientId + client: options.client }) .withGraphFetched('asset') diff --git a/packages/backend/src/tests/outgoingPayment.ts b/packages/backend/src/tests/outgoingPayment.ts index 4bb5957fdd..df1fe9d184 100644 --- a/packages/backend/src/tests/outgoingPayment.ts +++ b/packages/backend/src/tests/outgoingPayment.ts @@ -17,7 +17,7 @@ export async function createOutgoingPayment( ): Promise { const quoteOptions: CreateTestQuoteOptions = { paymentPointerId: options.paymentPointerId, - clientId: options.clientId, + client: options.client, receiver: options.receiver, validDestination: options.validDestination } diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index c87331252a..64f5c0b2ab 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -18,7 +18,7 @@ export async function createQuote( receiver: receiverUrl, sendAmount, receiveAmount, - clientId, + client, validDestination = true }: CreateTestQuoteOptions ): Promise { @@ -121,7 +121,7 @@ export async function createQuote( Pay.Int.from(1000n) as Pay.PositiveInt ), expiresAt: new Date(Date.now() + config.quoteLifespan), - clientId + client }) .withGraphFetched('asset') } diff --git a/packages/http-signature-utils/src/index.ts b/packages/http-signature-utils/src/index.ts index d326119e75..9ff99a21ec 100644 --- a/packages/http-signature-utils/src/index.ts +++ b/packages/http-signature-utils/src/index.ts @@ -1,4 +1,4 @@ -export { createHeaders, Headers } from './utils/headers' +export { createHeaders, getKeyId, Headers } from './utils/headers' export { generateJwk, JWK } from './utils/jwk' export { parseOrProvisionKey } from './utils/key' export { createSignatureHeaders } from './utils/signatures' diff --git a/packages/http-signature-utils/src/utils/headers.test.ts b/packages/http-signature-utils/src/utils/headers.test.ts new file mode 100644 index 0000000000..ee2bca44b9 --- /dev/null +++ b/packages/http-signature-utils/src/utils/headers.test.ts @@ -0,0 +1,22 @@ +import { getKeyId } from './headers' + +describe('headers', (): void => { + describe('getKeyId', (): void => { + test('extracts key id from signature input', async (): Promise => { + const keyId = 'gnap-rsa' + const sigInput = `sig1=("@method" "@target-uri" "authorization");created=1618884473;keyid="${keyId}";nonce="NAOEJF12ER2";tag="gnap"` + expect(getKeyId(sigInput)).toEqual(keyId) + }) + + test('returns undefined for missing key id', async (): Promise => { + const sigInput = + 'sig1=("@method" "@target-uri" "authorization");created=1618884473;nonce="NAOEJF12ER2";tag="gnap"' + expect(getKeyId(sigInput)).toBeUndefined() + }) + + test('returns undefined for invalid signature input', async (): Promise => { + const sigInput = 'invalid signature input' + expect(getKeyId(sigInput)).toBeUndefined() + }) + }) +}) diff --git a/packages/http-signature-utils/src/utils/headers.ts b/packages/http-signature-utils/src/utils/headers.ts index 6942a4fd46..81bfd3b0f1 100644 --- a/packages/http-signature-utils/src/utils/headers.ts +++ b/packages/http-signature-utils/src/utils/headers.ts @@ -47,3 +47,13 @@ export const createHeaders = async ({ ...signatureHeaders } } + +const KEY_ID_PREFIX = 'keyid="' + +export const getKeyId = (signatureInput: string): string | undefined => { + const keyIdParam = signatureInput + .split(';') + .find((param) => param.startsWith(KEY_ID_PREFIX)) + // Trim prefix and quotes + return keyIdParam?.slice(KEY_ID_PREFIX.length, -1) +} diff --git a/packages/token-introspection/src/openapi/generated/types.ts b/packages/token-introspection/src/openapi/generated/types.ts index 7c9ab152f0..9ee7e756e4 100644 --- a/packages/token-introspection/src/openapi/generated/types.ts +++ b/packages/token-introspection/src/openapi/generated/types.ts @@ -13,36 +13,22 @@ export interface paths { export interface components { schemas: { - /** - * key - * @description A key presented by value MUST be a public key. - */ - key: { - /** @description The form of proof that the client instance will use when presenting the key. */ - proof: "httpsig"; - /** - * Ed25519 Public Key - * @description A JWK representation of an Ed25519 Public Key - */ - jwk: { - kid: string; - /** @description The cryptographic algorithm family used with the key. The only allowed value is `EdDSA`. */ - alg: "EdDSA"; - use?: "sig"; - kty: "OKP"; - crv: "Ed25519"; - /** @description The base64 url-encoded public key. */ - x: string; - }; - }; /** token-info */ "token-info": { active: true; grant: string; access: external["schemas.yaml"]["components"]["schemas"]["access"]; - key: components["schemas"]["key"]; - /** @description Opaque client identifier. */ - client_id: string; + /** + * client + * @description Payment pointer of the client instance that is making this request. + * + * When sending a non-continuation request to the AS, the client instance MUST identify itself by including the client field of the request and by signing the request. + * + * A JSON Web Key Set document, including the public key that the client instance will use to protect this request and any continuation requests at the AS and any user-facing information about the client instance used in interactions, MUST be available at the payment pointer + `/jwks.json` url. + * + * If sending a grant initiation request that requires RO interaction, the payment pointer MUST serve necessary client display information. + */ + client: string; }; }; } diff --git a/packages/token-introspection/src/openapi/token-introspection.yaml b/packages/token-introspection/src/openapi/token-introspection.yaml index b409959258..228ef39d61 100644 --- a/packages/token-introspection/src/openapi/token-introspection.yaml +++ b/packages/token-introspection/src/openapi/token-introspection.yaml @@ -52,16 +52,7 @@ paths: value: '500' assetCode: USD assetScale: 2 - key: - proof: httpsig - jwk: - alg: EdDSA - kid: 20f24ce2-a5f6-4f28-bf7d-ed52d0490187 - kty: OKP - use: sig - crv: Ed25519 - x: AAAAC3NzaC1lZDI1NTE5AAAAIK0wmN/Cr3JXqmLW7u+g9pTh+wyqDHpSQEIQczXkVx9q - client_id: 02e930483ff82fb7bdd8972242f820464fed145647094a8f3fe00a2c0bc6352d + client: 'https://webmonize.com/.well-known/pay' '404': description: Not Found description: Introspect an access token to get grant details. @@ -86,64 +77,6 @@ paths: - introspection components: schemas: - key: - title: key - type: object - description: A key presented by value MUST be a public key. - properties: - proof: - type: string - enum: - - httpsig - description: The form of proof that the client instance will use when presenting the key. - jwk: - type: object - properties: - kid: - type: string - alg: - type: string - description: 'The cryptographic algorithm family used with the key. The only allowed value is `EdDSA`. ' - enum: - - EdDSA - use: - type: string - enum: - - sig - kty: - type: string - enum: - - OKP - crv: - type: string - enum: - - Ed25519 - x: - type: string - pattern: '^[a-zA-Z0-9-_]+$' - description: The base64 url-encoded public key. - required: - - kid - - alg - - kty - - crv - - x - title: Ed25519 Public Key - description: A JWK representation of an Ed25519 Public Key - examples: - - kid: key-1 - use: sig - kty: OKP - crv: Ed25519 - x: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo - - kid: '2022-09-02' - use: sig - kty: OKP - crv: Ed25519 - x: oy0L_vTygNE4IogRyn_F5GmHXdqYVjIXkWs2jky7zsI - required: - - proof - - jwk token-info: title: token-info type: object @@ -155,17 +88,22 @@ components: type: string access: $ref: ./schemas.yaml#/components/schemas/access - key: - $ref: '#/components/schemas/key' - client_id: + client: + title: client type: string - description: Opaque client identifier. + description: |- + Payment pointer of the client instance that is making this request. + + When sending a non-continuation request to the AS, the client instance MUST identify itself by including the client field of the request and by signing the request. + + A JSON Web Key Set document, including the public key that the client instance will use to protect this request and any continuation requests at the AS and any user-facing information about the client instance used in interactions, MUST be available at the payment pointer + `/jwks.json` url. + + If sending a grant initiation request that requires RO interaction, the payment pointer MUST serve necessary client display information. required: - active - grant - access - - key - - client_id + - client securitySchemes: GNAP: name: Authorization