Skip to content

Commit

Permalink
chore(token-introspection): return client payment pointer, not key (#…
Browse files Browse the repository at this point in the history
…1007)

* chore(http-signature-utils): add getKeyId util

* chore(token-introspection): return client payment pointer, not key

* chore(auth): unbind grant from client key

* chore(backend): rename clientId as client

* chore(backend): add accessAction to PaymentPointerContext
  • Loading branch information
wilsonianb authored Jan 25, 2023
1 parent a447e42 commit 515a525
Show file tree
Hide file tree
Showing 44 changed files with 323 additions and 456 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 0 additions & 3 deletions packages/auth/seeds/development/01_grants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down
1 change: 0 additions & 1 deletion packages/auth/src/access/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
35 changes: 4 additions & 31 deletions packages/auth/src/accessToken/routes.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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'

Expand All @@ -25,16 +23,13 @@ describe('Access Token Routes', (): void => {
let appContainer: TestContainer
let trx: Knex.Transaction
let accessTokenRoutes: AccessTokenRoutes
let testClientKey: JWK

beforeAll(async (): Promise<void> => {
deps = await initIocContainer(Config)
appContainer = await createTestApp(deps)
accessTokenRoutes = await deps.use('accessTokenRoutes')
const openApi = await deps.use('openApi')
jestOpenAPI(openApi.authServerSpec)

testClientKey = generateTestKeys().publicKey
})

afterEach(async (): Promise<void> => {
Expand Down Expand Up @@ -93,10 +88,7 @@ describe('Access Token Routes', (): void => {
const method = 'POST'

beforeEach(async (): Promise<void> => {
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
Expand Down Expand Up @@ -135,14 +127,6 @@ describe('Access Token Routes', (): void => {
})

test('Successfully introspects valid token', async (): Promise<void> => {
const clientId = crypto.createHash('sha256').update(CLIENT).digest('hex')

const scope = nock(CLIENT)
.get('/jwks.json')
.reply(200, {
keys: [testClientKey]
})

const ctx = createContext(
{
headers: {
Expand Down Expand Up @@ -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<void> => {
Expand Down Expand Up @@ -226,10 +205,7 @@ describe('Access Token Routes', (): void => {
const method = 'DELETE'

beforeEach(async (): Promise<void> => {
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
Expand Down Expand Up @@ -317,10 +293,7 @@ describe('Access Token Routes', (): void => {
let managementId: string

beforeEach(async (): Promise<void> => {
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
Expand Down
19 changes: 7 additions & 12 deletions packages/auth/src/accessToken/routes.ts
Original file line number Diff line number Diff line change
@@ -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<BodyT> = Omit<AppContext['request'], 'body'> & {
body: BodyT
Expand Down Expand Up @@ -63,31 +63,26 @@ async function introspectToken(
ctx: IntrospectContext
): Promise<void> {
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
}
}

Expand Down
25 changes: 1 addition & 24 deletions packages/auth/src/accessToken/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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<void> => {
deps = await initIocContainer(Config)
appContainer = await createTestApp(deps)
accessTokenService = await deps.use('accessTokenService')

testClientKey = generateTestKeys().publicKey
})

afterEach(async (): Promise<void> => {
Expand Down Expand Up @@ -76,7 +72,6 @@ describe('Access Token Service', (): void => {
beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
clientKeyId: testClientKey.kid,
continueToken: generateToken(),
continueId: v4(),
interactId: v4(),
Expand Down Expand Up @@ -145,19 +140,9 @@ describe('Access Token Service', (): void => {

describe('Introspect', (): void => {
test('Can introspect active token', async (): Promise<void> => {
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<void> => {
Expand All @@ -181,12 +166,6 @@ describe('Access Token Service', (): void => {
test('Cannot introspect non-existing token', async (): Promise<void> => {
expect(accessTokenService.introspect('uuid')).resolves.toBeUndefined()
})

test('Cannot introspect with non-existing key', async (): Promise<void> => {
await expect(
accessTokenService.introspect(token.value)
).resolves.toBeUndefined()
})
})

describe('Revoke', (): void => {
Expand All @@ -195,7 +174,6 @@ describe('Access Token Service', (): void => {
beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
clientKeyId: testClientKey.kid,
continueToken: generateToken(),
continueId: v4(),
interactId: v4(),
Expand Down Expand Up @@ -248,7 +226,6 @@ describe('Access Token Service', (): void => {
beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
clientKeyId: testClientKey.kid,
continueToken: generateToken(),
continueId: v4(),
interactId: v4(),
Expand Down
29 changes: 3 additions & 26 deletions packages/auth/src/accessToken/service.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -13,7 +12,7 @@ import { Access } from '../access/model'
export interface AccessTokenService {
get(token: string): Promise<AccessToken | undefined>
getByManagementId(managementId: string): Promise<AccessToken | undefined>
introspect(token: string): Promise<Introspection | undefined>
introspect(token: string): Promise<Grant | undefined>
revoke(id: string, tokenValue: string): Promise<void>
create(grantId: string, opts?: AccessTokenOpts): Promise<AccessToken>
rotate(managementId: string, tokenValue: string): Promise<Rotation>
Expand All @@ -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
Expand Down Expand Up @@ -104,7 +93,7 @@ async function getByManagementId(
async function introspect(
deps: ServiceDependencies,
value: string
): Promise<Introspection | undefined> {
): Promise<Grant | undefined> {
const token = await AccessToken.query(deps.knex)
.findOne({ value })
.withGraphFetched('grant.access')
Expand All @@ -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
}
}

Expand Down
2 changes: 0 additions & 2 deletions packages/auth/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DefaultState, AppContextData>
Expand Down
1 change: 0 additions & 1 deletion packages/auth/src/grant/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 1 addition & 7 deletions packages/auth/src/grant/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -84,11 +82,7 @@ describe('Grant Routes', (): void => {
const createContext = (
reqOpts: httpMocks.RequestOptions,
params: Record<string, unknown>
) => {
const ctx = createAppContext(reqOpts, params)
ctx.clientKeyId = CLIENT_KEY_ID
return ctx
}
) => createAppContext(reqOpts, params)

beforeEach(async (): Promise<void> => {
grant = await Grant.query().insert(generateBaseGrant())
Expand Down
Loading

0 comments on commit 515a525

Please sign in to comment.