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

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

Merged
merged 6 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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