Skip to content

Commit

Permalink
662/mk/use open payments types auth (#1061)
Browse files Browse the repository at this point in the history
* feat(open-payments): export the different IncomingPayment types

* feat(backend): use open-payment types for IncomingPayment

* feat(backend): use open-payment types for Connection

* feat(backend): use open-payment types for OutgoingPayment

* feat(backend): use open-payment types for Quote

* chore(backend): fix IncomingPayments.toOpenPaymentsType

* chore(open-payments): update mocks

* chore(backend): formatting

* chore(backend): add better typing for incomingPayment.toOpenPaymentsType

* chore(backend): remove QuoteJSON

* chore(open-payments): fix mocks

* chore(backend): fix IncomingPayment types

* chore(open-payments): fix incoming payment types

* chore(backend): fix incoming payment method

* chore(backend): make toOpenPaymentsType async

* chore(backend): getUrl for OutgoingPayment

* chore(backend): fix tests with async toOpenPaymentsType

* chore(backend): return undefined instead of null

* chore(backend): add awaits where necessary

* chore(open-payments): make getBody async

* chore(backend): fix outgoingPayment model method

* chore(backend): fix list tests

* chore(backend): update receiver tests

* chore(backend): make toOpenPaymentType sync (#1056)

* chore(backend): make toOpenPaymentType sync

* chore(backend): make quote.toOpenPaymentsType sync

* chore(backend): convert tests to sync

* chore(backend): update tests

* chore(open-payments): updating AccessType type & exporting AccessToken

* chore(auth): adding toOpenPayments type methods for grants & access & accessTokens

* chore(auth): remove unused type

* chore(backend): remove $formatJson where applicable

* chore(backend): remove $formatJson where applicable

* chore(backend): add test for incomingPayment model

* chore(auth): await createGrantInitiation functions

* chore(open-payments): export AccessItem

* chore(auth): update  toOpenPaymentsAccess

* chore(open-payments): update AccessAction type

* chore(backend): fix incomingPayment model test

* chore(auth): exclude instead of Omit to give us ctx types

* chore(open-payments): use suggestion

* chore(backend): fix test

* chore(backend): use suggestion

* chore(auth): updates types for routes tests

* chore(auth): update toOpenPaymentsGrant name

* chore(auth): update accessToken tests

* chore(auth): address feedback

* feat(auth): lock grant when rotating accessToken

* chore(auth): add tests for revoked accessToken

* chore(auth): remove locking for now
  • Loading branch information
mkurapov authored Feb 6, 2023
1 parent f701717 commit 8687571
Show file tree
Hide file tree
Showing 15 changed files with 435 additions and 285 deletions.
18 changes: 17 additions & 1 deletion packages/auth/src/access/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Model } from 'objection'
import { BaseModel } from '../shared/baseModel'
import { LimitData } from './types'
import { join } from 'path'
import { AccessType, AccessAction } from 'open-payments'
import {
AccessType,
AccessAction,
AccessItem as OpenPaymentsAccessItem
} from 'open-payments'

export class Access extends BaseModel {
public static get tableName(): string {
Expand All @@ -28,3 +32,15 @@ export class Access extends BaseModel {
public identifier?: string
public limits?: LimitData
}

export function toOpenPaymentsAccess(
accessItem: Access
): OpenPaymentsAccessItem {
return {
actions: accessItem.actions,
identifier: accessItem.identifier ?? undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: accessItem.type as any,
limits: accessItem.limits ?? undefined
}
}
19 changes: 19 additions & 0 deletions packages/auth/src/accessToken/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Model } from 'objection'
import { BaseModel } from '../shared/baseModel'
import { join } from 'path'
import { Grant } from '../grant/model'
import { AccessToken as OpenPaymentsAccessToken } from 'open-payments'
import { Access, toOpenPaymentsAccess } from '../access/model'

// https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol#section-3.2.1
export class AccessToken extends BaseModel {
Expand All @@ -26,3 +28,20 @@ export class AccessToken extends BaseModel {
public grantId!: string
public expiresIn!: number
}

interface ToOpenPaymentsAccessTokenArgs {
authServerUrl: string
}

export function toOpenPaymentsAccessToken(
accessToken: AccessToken,
accessItems: Access[],
args: ToOpenPaymentsAccessTokenArgs
): OpenPaymentsAccessToken['access_token'] {
return {
access: accessItems.map(toOpenPaymentsAccess),
value: accessToken.value,
manage: `${args.authServerUrl}/token/${accessToken.managementId}`,
expires_in: accessToken.expiresIn
}
}
17 changes: 9 additions & 8 deletions packages/auth/src/accessToken/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Knex } from 'knex'
import { v4 } from 'uuid'
import jestOpenAPI from 'jest-openapi'

import { createContext } from '../tests/context'
import { createTestApp, TestContainer } from '../tests/app'
import { Config } from '../config/app'
import { IocContract } from '@adonisjs/fold'
Expand All @@ -13,8 +14,7 @@ import { truncateTables } from '../tests/tableManager'
import { FinishMethod, Grant, GrantState, StartMethod } from '../grant/model'
import { AccessToken } from './model'
import { Access } from '../access/model'
import { AccessTokenRoutes } from './routes'
import { createContext } from '../tests/context'
import { AccessTokenRoutes, IntrospectContext } from './routes'
import { generateNonce, generateToken } from '../shared/utils'
import { AccessType, AccessAction } from 'open-payments'

Expand All @@ -25,7 +25,7 @@ describe('Access Token Routes', (): void => {
let accessTokenRoutes: AccessTokenRoutes

beforeAll(async (): Promise<void> => {
deps = await initIocContainer(Config)
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
accessTokenRoutes = await deps.use('accessTokenRoutes')
const openApi = await deps.use('openApi')
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('Access Token Routes', (): void => {
jestOpenAPI(openApi.tokenIntrospectionSpec)
})
test('Cannot introspect fake token', async (): Promise<void> => {
const ctx = createContext(
const ctx = createContext<IntrospectContext>(
{
headers: {
Accept: 'application/json'
Expand All @@ -127,7 +127,7 @@ describe('Access Token Routes', (): void => {
})

test('Successfully introspects valid token', async (): Promise<void> => {
const ctx = createContext(
const ctx = createContext<IntrospectContext>(
{
headers: {
Accept: 'application/json'
Expand All @@ -141,6 +141,7 @@ describe('Access Token Routes', (): void => {
ctx.request.body = {
access_token: token.value
}

await expect(accessTokenRoutes.introspect(ctx)).resolves.toBeUndefined()
expect(ctx.response).toSatisfyApiSpec()
expect(ctx.status).toBe(200)
Expand Down Expand Up @@ -170,7 +171,7 @@ describe('Access Token Routes', (): void => {
)
jest.useFakeTimers({ now })

const ctx = createContext(
const ctx = createContext<IntrospectContext>(
{
headers: {
Accept: 'application/json'
Expand Down Expand Up @@ -324,7 +325,7 @@ describe('Access Token Routes', (): void => {

await expect(accessTokenRoutes.rotate(ctx)).rejects.toMatchObject({
status: 404,
message: 'token not found'
message: 'Token not found'
})
})

Expand All @@ -343,7 +344,7 @@ describe('Access Token Routes', (): void => {

await expect(accessTokenRoutes.rotate(ctx)).rejects.toMatchObject({
status: 404,
message: 'token not found'
message: 'Token not found'
})
})

Expand Down
54 changes: 31 additions & 23 deletions packages/auth/src/accessToken/routes.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { Logger } from 'pino'
import { ActiveTokenInfo, TokenInfo } from 'token-introspection'
import { Access } from '../access/model'
import { TokenInfo } from 'token-introspection'
import { toOpenPaymentsAccess } from '../access/model'
import { AppContext } from '../app'
import { IAppConfig } from '../config/app'
import { AccessTokenService } from './service'
import { accessToBody } from '../shared/utils'
import { ClientService } from '../client/service'
import { Grant } from '../grant/model'
import { toOpenPaymentsAccessToken } from './model'
import { AccessService } from '../access/service'

type TokenRequest<BodyT> = Omit<AppContext['request'], 'body'> & {
type TokenRequest<BodyT> = Exclude<AppContext['request'], 'body'> & {
body: BodyT
}

type TokenContext<BodyT> = Omit<AppContext, 'request'> & {
type TokenContext<BodyT> = Exclude<AppContext, 'request'> & {
request: TokenRequest<BodyT>
}

type ManagementRequest = Omit<AppContext['request'], 'params'> & {
type ManagementRequest = Exclude<AppContext['request'], 'params'> & {
params?: Record<'id', string>
}

type ManagementContext = Omit<AppContext, 'request'> & {
type ManagementContext = Exclude<AppContext, 'request'> & {
request: ManagementRequest
}

Expand All @@ -35,6 +36,7 @@ interface ServiceDependencies {
config: IAppConfig
logger: Logger
accessTokenService: AccessTokenService
accessService: AccessService
clientService: ClientService
}

Expand Down Expand Up @@ -79,9 +81,7 @@ function grantToTokenInfo(grant?: Grant): TokenInfo {
return {
active: true,
grant: grant.id,
access: grant.access.map((a: Access) =>
accessToBody(a)
) as ActiveTokenInfo['access'],
access: grant.access.map(toOpenPaymentsAccess),
client: grant.client
}
}
Expand All @@ -102,18 +102,26 @@ async function rotateToken(
): Promise<void> {
const { id: managementId } = ctx.params
const token = (ctx.headers['authorization'] ?? '').replace('GNAP ', '')
const result = await deps.accessTokenService.rotate(managementId, token)
if (result.success == true) {
ctx.status = 200
ctx.body = {
access_token: {
access: result.access.map((a) => accessToBody(a)),
value: result.value,
manage: deps.config.authServerDomain + `/token/${result.managementId}`,
expires_in: result.expiresIn
}
}
} else {
ctx.throw(404, { message: result.error.message })

let newToken
try {
newToken = await deps.accessTokenService.rotate(managementId, token)
} catch (error) {
const errorMessage = 'Could not rotate token'
deps.logger.error({ error: error && error['message'] }, errorMessage)
ctx.throw(400, { message: errorMessage })
}

if (!newToken) {
ctx.throw(404, { message: 'Token not found' })
}

const accessItems = await deps.accessService.getByGrant(newToken.grantId)

ctx.status = 200
ctx.body = {
access_token: toOpenPaymentsAccessToken(newToken, accessItems, {
authServerUrl: deps.config.authServerDomain
})
}
}
Loading

0 comments on commit 8687571

Please sign in to comment.