Skip to content

Commit

Permalink
feat(auth): add interaction model (#1797)
Browse files Browse the repository at this point in the history
Co-authored-by: Nathan Lie <nathanlie@Nathans-MacBook-Pro.local>
  • Loading branch information
njlie and Nathan Lie authored Sep 15, 2023
1 parent d80c23f commit 2ffdc44
Show file tree
Hide file tree
Showing 23 changed files with 1,018 additions and 463 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return Promise.all([
knex.schema.createTable('interactions', function (table) {
table.uuid('id').notNullable().primary()

table.string('ref').notNullable().unique()
table.string('nonce').notNullable()
table.string('state').notNullable()

table.uuid('grantId').notNullable()
table.foreign('grantId').references('grants.id').onDelete('CASCADE')

table.integer('expiresIn').notNullable()

table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())
}),
knex.schema.alterTable('grants', function (table) {
table.dropColumn('interactId')
table.dropColumn('interactRef')
table.dropColumn('interactNonce')
})
])
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return Promise.all([
knex.schema.dropTableIfExists('interactions'),
knex.schema.alterTable('grants', function (table) {
table.string('interactId').unique()
table.string('interactRef').unique()
table.string('interactNonce').unique()
})
])
}
43 changes: 16 additions & 27 deletions packages/auth/src/access/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ describe('Access Service', (): void => {
let appContainer: TestContainer
let accessService: AccessService
let trx: Knex.Transaction
let grant: Grant

const generateBaseGrant = () => ({
state: GrantState.Pending,
startMethod: [StartMethod.Redirect],
continueToken: generateToken(),
continueId: v4(),
finishMethod: FinishMethod.Redirect,
finishUri: 'https://example.com/finish',
clientNonce: generateNonce(),
client: faker.internet.url({ appendSlash: false })
})

beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch(generateBaseGrant())
})

beforeAll(async (): Promise<void> => {
deps = initIocContainer(Config)
Expand All @@ -36,26 +52,8 @@ describe('Access Service', (): void => {
await appContainer.shutdown()
})

const BASE_GRANT = {
state: GrantState.Processing,
startMethod: [StartMethod.Redirect],
continueToken: generateToken(),
continueId: v4(),
finishMethod: FinishMethod.Redirect,
finishUri: 'https://example.com/finish',
clientNonce: generateNonce(),
client: faker.internet.url({ appendSlash: false }),
interactId: v4(),
interactRef: generateNonce(),
interactNonce: generateNonce()
}

describe('create', (): void => {
test('Can create incoming payment access', async (): Promise<void> => {
const grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT
})

const incomingPaymentAccess: IncomingPaymentRequest = {
type: 'incoming-payment',
actions: [AccessAction.Create, AccessAction.Read, AccessAction.List]
Expand Down Expand Up @@ -92,8 +90,6 @@ describe('Access Service', (): void => {
limits: outgoingPaymentLimit
}

const grant = await Grant.query(trx).insertAndFetch(BASE_GRANT)

const access = await accessService.createAccess(grant.id, [
outgoingPaymentAccess
])
Expand All @@ -107,10 +103,6 @@ describe('Access Service', (): void => {

describe('getByGrant', (): void => {
test('gets access', async () => {
const grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT
})

const incomingPaymentAccess: IncomingPaymentRequest = {
type: 'incoming-payment',
actions: [AccessAction.Create, AccessAction.Read, AccessAction.List]
Expand All @@ -136,9 +128,6 @@ describe('Access Service', (): void => {

describe('revoke access', (): void => {
test('revokeByGrantId', async () => {
const grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT
})
const access = await Access.query(trx).insert({
grantId: grant.id,
type: 'incoming-payment',
Expand Down
5 changes: 1 addition & 4 deletions packages/auth/src/accessToken/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ describe('Access Token Routes', (): void => {
finishMethod: FinishMethod.Redirect,
finishUri: 'https://example.com/finish',
clientNonce: generateNonce(),
client: CLIENT,
interactId: v4(),
interactRef: generateNonce(),
interactNonce: generateNonce()
client: CLIENT
}

const BASE_ACCESS = {
Expand Down
63 changes: 19 additions & 44 deletions packages/auth/src/accessToken/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { faker } from '@faker-js/faker'
import nock from 'nock'
import { Knex } from 'knex'
import { v4 } from 'uuid'
Expand All @@ -10,18 +9,13 @@ import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '..'
import { AppServices } from '../app'
import { truncateTables } from '../tests/tableManager'
import {
FinishMethod,
Grant,
GrantState,
GrantFinalization,
StartMethod
} from '../grant/model'
import { Grant, GrantState, GrantFinalization } from '../grant/model'
import { AccessToken } from './model'
import { AccessTokenService } from './service'
import { Access } from '../access/model'
import { generateNonce, generateToken } from '../shared/utils'
import { generateToken } from '../shared/utils'
import { AccessType, AccessAction } from '@interledger/open-payments'
import { generateBaseGrant } from '../tests/grant'

describe('Access Token Service', (): void => {
let deps: IocContract<AppServices>
Expand All @@ -45,17 +39,6 @@ describe('Access Token Service', (): void => {
await appContainer.shutdown()
})

const CLIENT = faker.internet.url({ appendSlash: false })

const BASE_GRANT = {
state: GrantState.Processing,
startMethod: [StartMethod.Redirect],
finishMethod: FinishMethod.Redirect,
finishUri: 'https://example.com/finish',
clientNonce: generateNonce(),
client: CLIENT
}

const BASE_ACCESS = {
type: AccessType.OutgoingPayment,
actions: [AccessAction.Read, AccessAction.Create],
Expand All @@ -76,14 +59,9 @@ describe('Access Token Service', (): void => {

let grant: Grant
beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
continueToken: generateToken(),
continueId: v4(),
interactId: v4(),
interactRef: generateNonce(),
interactNonce: generateNonce()
})
grant = await Grant.query(trx).insertAndFetch(
generateBaseGrant({ state: GrantState.Approved })
)
grant.access = [
await Access.query(trx).insertAndFetch({
grantId: grant.id,
Expand Down Expand Up @@ -190,14 +168,13 @@ describe('Access Token Service', (): void => {
let grant: Grant
let token: AccessToken
beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
continueToken: generateToken(),
continueId: v4(),
interactId: v4(),
interactRef: generateNonce(),
interactNonce: generateNonce()
})
grant = await Grant.query(trx).insertAndFetch(
generateBaseGrant({
state: GrantState.Finalized,
finalizationReason: GrantFinalization.Issued
})
)

token = await AccessToken.query(trx).insertAndFetch({
grantId: grant.id,
...BASE_TOKEN,
Expand Down Expand Up @@ -287,14 +264,12 @@ describe('Access Token Service', (): void => {
let token: AccessToken
let originalTokenValue: string
beforeEach(async (): Promise<void> => {
grant = await Grant.query(trx).insertAndFetch({
...BASE_GRANT,
continueToken: generateToken(),
continueId: v4(),
interactId: v4(),
interactRef: generateNonce(),
interactNonce: generateNonce()
})
grant = await Grant.query(trx).insertAndFetch(
generateBaseGrant({
state: GrantState.Finalized,
finalizationReason: GrantFinalization.Issued
})
)
await Access.query(trx).insertAndFetch({
grantId: grant.id,
...BASE_ACCESS
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { loadSchemaSync } from '@graphql-tools/load'
import { resolvers } from './graphql/resolvers'
import { ClientService } from './client/service'
import { GrantService } from './grant/service'
import { InteractionService } from './interaction/service'
import {
CreateContext,
ContinueContext,
Expand Down Expand Up @@ -89,6 +90,7 @@ export interface AppServices {
config: Promise<IAppConfig>
clientService: Promise<ClientService>
grantService: Promise<GrantService>
interactionService: Promise<InteractionService>
accessService: Promise<AccessService>
accessTokenRoutes: Promise<AccessTokenRoutes>
accessTokenService: Promise<AccessTokenService>
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const Config = {
),
waitTimeSeconds: envInt('WAIT_SECONDS', 5),
cookieKey: envString('COOKIE_KEY', crypto.randomBytes(32).toString('hex')),
interactionExpirySeconds: envInt('INTERACTION_EXPIRY_SECONDS', 10 * 60), // Default 10 minutes
accessTokenExpirySeconds: envInt('ACCESS_TOKEN_EXPIRY_SECONDS', 10 * 60), // Default 10 minutes
databaseCleanupWorkers: envInt('DATABASE_CLEANUP_WORKERS', 1),
accessTokenDeletionDays: envInt('ACCESS_TOKEN_DELETION_DAYS', 30),
Expand Down
37 changes: 15 additions & 22 deletions packages/auth/src/grant/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Grant as OpenPaymentsGrant
} from '@interledger/open-payments'
import { AccessToken, toOpenPaymentsAccessToken } from '../accessToken/model'
import { Interaction } from '../interaction/model'

export enum StartMethod {
Redirect = 'redirect'
Expand Down Expand Up @@ -51,6 +52,14 @@ export class Grant extends BaseModel {
from: 'grants.id',
to: 'accesses.grantId'
}
},
interaction: {
relation: Model.HasManyRelation,
modelClass: join(__dirname, '../interaction/model'),
join: {
from: 'grants.id',
to: 'interactions.grantId'
}
}
})
public access!: Access[]
Expand All @@ -67,10 +76,6 @@ export class Grant extends BaseModel {
public finishUri?: string
public client!: string
public clientNonce?: string // client-generated nonce for post-interaction hash

public interactId?: string
public interactRef?: string
public interactNonce?: string // AS-generated nonce for post-interaction hash
}

interface ToOpenPaymentsPendingGrantArgs {
Expand All @@ -84,16 +89,13 @@ interface ToOpenPaymentsPendingGrantArgs {

export function toOpenPaymentPendingGrant(
grant: Grant,
interaction: Interaction,
args: ToOpenPaymentsPendingGrantArgs
): OpenPaymentsPendingGrant {
if (!isInteractiveGrant(grant)) {
throw new Error('Expected pending/interactive grant')
}

const { authServerUrl, client, waitTimeSeconds } = args

const redirectUri = new URL(
authServerUrl + `/interact/${grant.interactId}/${grant.interactNonce}`
authServerUrl + `/interact/${interaction.id}/${interaction.nonce}`
)

redirectUri.searchParams.set('clientName', client.name)
Expand All @@ -102,7 +104,7 @@ export function toOpenPaymentPendingGrant(
return {
interact: {
redirect: redirectUri.toString(),
finish: grant.interactNonce
finish: interaction.nonce
},
continue: {
access_token: {
Expand Down Expand Up @@ -137,22 +139,13 @@ export function toOpenPaymentsGrant(
}
}

export interface InteractiveGrant extends Grant {
export interface FinishableGrant extends Grant {
finishMethod: NonNullable<Grant['finishMethod']>
finishUri: NonNullable<Grant['finishUri']>
interactId: NonNullable<Grant['interactId']>
interactRef: NonNullable<Grant['interactRef']>
interactNonce: NonNullable<Grant['interactNonce']> // AS-generated nonce for post-interaction hash
}

export function isInteractiveGrant(grant: Grant): grant is InteractiveGrant {
return !!(
grant.finishMethod &&
grant.finishUri &&
grant.interactId &&
grant.interactRef &&
grant.interactNonce
)
export function isFinishableGrant(grant: Grant): grant is FinishableGrant {
return !!(grant.finishMethod && grant.finishUri)
}

export function isRejectedGrant(grant: Grant): boolean {
Expand Down
Loading

0 comments on commit 2ffdc44

Please sign in to comment.