Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6b05778
feat(auth): add subject to grant
cozminu May 27, 2025
c6a310e
feat(auth): add subject to grant
cozminu Jun 2, 2025
ed8411d
fix(auth): grant access when is undefined
cozminu Jun 2, 2025
fdfbba3
feat(auth): subject id validation
cozminu Jun 20, 2025
03b1ec5
fix(auth): fix tests
cozminu Jun 20, 2025
9b224b2
Update packages/auth/src/graphql/schema.graphql
cozminu Jun 25, 2025
d9d7982
feat(auth): throw GrantError instead of Error
cozminu Jun 25, 2025
7a7d8e6
fix(auth): description for subIdFormat
cozminu Jun 25, 2025
efd3042
fix(auth): trx in tests is knex
cozminu Jul 3, 2025
454195e
Merge branch 'main' into cozmin/raf-996
cozminu Jul 23, 2025
99e11b9
fix: grant service to throw only grant errors
cozminu Jul 24, 2025
a2db4d6
fix(auth): address change requests
cozminu Jul 28, 2025
223e69e
fix(auth): address change requests
cozminu Jul 28, 2025
eb28179
fix tests
cozminu Jul 28, 2025
bc07cae
access token optional in grant response
cozminu Jul 28, 2025
1b89c37
fix(auth): move accessErrorsMap to grant
cozminu Jul 30, 2025
7b0fe22
Merge branch 'main' into cozmin/raf-996
cozminu Jul 31, 2025
b823bbf
fix(auth): fix test for grant access
cozminu Jul 31, 2025
523da68
fix(auth): changed interaction generic error message
cozminu Aug 1, 2025
86a0618
use main OpenAPI spec for Auth
cozminu Aug 4, 2025
cfb3ca8
fix(auth): approved grant does not return subject
cozminu Aug 18, 2025
bc22b5b
fix(auth): remove unused import
cozminu Aug 25, 2025
8127553
fix(auth): make access_token optional in response
cozminu Aug 27, 2025
108cb6e
fix(deps): sha.js critical update
cozminu Sep 1, 2025
e9a8f08
fix(deps): critical update
cozminu Sep 1, 2025
6c735a0
Update package.json
cozminu Sep 2, 2025
69ad04f
update pnpm lock file
cozminu Sep 2, 2025
805795b
Merge branch 'main' into cozmin/raf-996
cozminu Sep 5, 2025
4b6f81c
change op specs version
cozminu Sep 9, 2025
4f7babc
feat(auth): update idp openapi spec
cozminu Sep 30, 2025
f5e9ddf
feat(auth): make idp standalone
cozminu Sep 30, 2025
763a2e9
chore(deps): update axios
cozminu Oct 1, 2025
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
2 changes: 1 addition & 1 deletion localenv/mock-account-servicing-entity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@remix-run/serve": "^2.16.4",
"@types/node": "^18.7.12",
"@types/uuid": "^9.0.8",
"axios": "^1.8.2",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"graphql": "^16.11.0",
"json-canonicalize": "^1.0.6",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"path-to-regexp@>=0.1.7": "^0.1.12",
"path-to-regexp@>=6.3.0": "^6.3.0",
"next": "^15.2.3",
"form-data": "^4.0.4"
"form-data": "^4.0.4",
"sha.js": ">=2.4.12"
}
}
}
26 changes: 26 additions & 0 deletions packages/auth/migrations/20250509114109_create_subject_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable('subjects', function (table) {
table.uuid('id').primary()

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

table.string('subId').notNullable()
table.string('subIdFormat').notNullable()

table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('subjects')
}
2 changes: 1 addition & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@koa/cors": "^5.0.0",
"@koa/router": "^12.0.2",
"ajv": "^8.12.0",
"axios": "^1.8.2",
"axios": "^1.12.0",
"dotenv": "^16.4.7",
"graphql": "^16.11.0",
"ioredis": "^5.3.2",
Expand Down
5 changes: 3 additions & 2 deletions packages/auth/src/access/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import nock from 'nock'
import { Knex } from 'knex'
import { createTestApp, TestContainer } from '../tests/app'
import { truncateTables } from '../tests/tableManager'
import { Config } from '../config/app'
Expand All @@ -13,14 +12,15 @@ import { AccessError } from './errors'
import { generateBaseGrant } from '../tests/grant'
import { AccessType, AccessAction } from '@interledger/open-payments'
import { Access } from './model'
import { TransactionOrKnex } from 'objection'
import { Tenant } from '../tenant/model'
import { generateTenant } from '../tests/tenant'

describe('Access Service', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let accessService: AccessService
let trx: Knex.Transaction
let trx: TransactionOrKnex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove the import { Knex } from 'knex' import

let grant: Grant

beforeEach(async (): Promise<void> => {
Expand All @@ -34,6 +34,7 @@ describe('Access Service', (): void => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
accessService = await deps.use('accessService')
trx = appContainer.knex
})

afterEach(async (): Promise<void> => {
Expand Down
5 changes: 3 additions & 2 deletions packages/auth/src/access/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { v4 } from 'uuid'
import { IocContract } from '@adonisjs/fold'
import { Knex } from 'knex'
import { faker } from '@faker-js/faker'
import {
AccessItem,
Expand All @@ -17,13 +16,14 @@ import { createTestApp, TestContainer } from '../tests/app'
import { truncateTables } from '../tests/tableManager'
import { generateToken, generateNonce } from '../shared/utils'
import { compareRequestAndGrantAccessItems } from './utils'
import { TransactionOrKnex } from 'objection'
import { Tenant } from '../tenant/model'
import { generateTenant } from '../tests/tenant'

describe('Access utilities', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let trx: Knex.Transaction
let trx: TransactionOrKnex
let identifier: string
let grant: Grant
let grantAccessItem: Access
Expand All @@ -35,6 +35,7 @@ describe('Access utilities', (): void => {
beforeAll(async (): Promise<void> => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
trx = appContainer.knex
})

beforeEach(async (): Promise<void> => {
Expand Down
5 changes: 3 additions & 2 deletions packages/auth/src/accessToken/routes.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { faker } from '@faker-js/faker'
import nock from 'nock'
import { Knex } from 'knex'
import { v4 } from 'uuid'
import jestOpenAPI from 'jest-openapi'

Expand All @@ -24,13 +23,14 @@ import {
import { GrantService } from '../grant/service'
import { AccessTokenService } from './service'
import { GNAPErrorCode } from '../shared/gnapErrors'
import { TransactionOrKnex } from 'objection'
import { generateTenant } from '../tests/tenant'
import { Tenant } from '../tenant/model'

describe('Access Token Routes', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let trx: Knex.Transaction
let trx: TransactionOrKnex
let accessTokenRoutes: AccessTokenRoutes
let accessTokenService: AccessTokenService
let grantService: GrantService
Expand All @@ -42,6 +42,7 @@ describe('Access Token Routes', (): void => {
grantService = await deps.use('grantService')
accessTokenService = await deps.use('accessTokenService')
const openApi = await deps.use('openApi')
trx = appContainer.knex
jestOpenAPI(openApi.authServerSpec)
})

Expand Down
9 changes: 5 additions & 4 deletions packages/auth/src/accessToken/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import nock from 'nock'
import { Knex } from 'knex'
import { v4 } from 'uuid'
import assert from 'assert'

Expand All @@ -20,19 +19,21 @@ import {
AccessItem
} from '@interledger/open-payments'
import { generateBaseGrant } from '../tests/grant'
import { TransactionOrKnex } from 'objection'
import { Tenant } from '../tenant/model'
import { generateTenant } from '../tests/tenant'

describe('Access Token Service', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let trx: Knex.Transaction
let trx: TransactionOrKnex
let accessTokenService: AccessTokenService

beforeAll(async (): Promise<void> => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
accessTokenService = await deps.use('accessTokenService')
trx = appContainer.knex
})

afterEach(async (): Promise<void> => {
Expand Down Expand Up @@ -175,7 +176,7 @@ describe('Access Token Service', (): void => {
accessTokenService.introspect(accessToken.value, [
outgoingPaymentAccess
])
).resolves.toEqual({ grant, access: [grant.access[0]] })
).resolves.toEqual({ grant, access: [grant.access?.[0]] })
})

test('Can introspect active token with partial access actions', async (): Promise<void> => {
Expand All @@ -185,7 +186,7 @@ describe('Access Token Service', (): void => {
}
await expect(
accessTokenService.introspect(accessToken.value, [access])
).resolves.toEqual({ grant, access: [grant.access[0]] })
).resolves.toEqual({ grant, access: [grant.access?.[0]] })
})

test('Introspection only returns requested access', async (): Promise<void> => {
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/accessToken/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async function introspect(
if (access) {
for (const accessItem of access) {
const { access: grantAccess } = token.grant
const foundAccessItem = grantAccess.find((grantAccessItem) =>
const foundAccessItem = grantAccess?.find((grantAccessItem) =>
compareRequestAndGrantAccessItems(
accessItem,
toOpenPaymentsAccess(grantAccessItem)
Expand Down
49 changes: 49 additions & 0 deletions packages/auth/src/grant/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { HttpStatusCode } from 'axios'
import { GNAPErrorCode } from '../shared/gnapErrors'
import { AccessError } from '../access/errors'

export enum GrantErrorCode {
InvalidRequest,
OnlyOneAccessAmountAllowed
}

export class GrantError extends Error {
code: GrantErrorCode
constructor(code: GrantErrorCode, message: string) {
super(message)
this.name = 'GrantError'
this.code = code
}
}

export function isGrantError(error: unknown): error is GrantError {
return error instanceof GrantError
}

export const errorToHTTPCode: {
[key in GrantErrorCode]: number
} = {
[GrantErrorCode.InvalidRequest]: HttpStatusCode.BadRequest,
[GrantErrorCode.OnlyOneAccessAmountAllowed]: HttpStatusCode.BadRequest
}

export const errorToGNAPCode: {
[key in GrantErrorCode]: GNAPErrorCode
} = {
[GrantErrorCode.InvalidRequest]: GNAPErrorCode.InvalidRequest,
[GrantErrorCode.OnlyOneAccessAmountAllowed]: GNAPErrorCode.InvalidRequest
}

export const errorToMessage: {
[key in GrantErrorCode]: string
} = {
[GrantErrorCode.InvalidRequest]: 'Invalid request',
[GrantErrorCode.OnlyOneAccessAmountAllowed]: 'only one access amount allowed'
}

export const accessErrorToGrantError: {
[key in AccessError]: GrantErrorCode
} = {
[AccessError.OnlyOneAccessAmountAllowed]:
GrantErrorCode.OnlyOneAccessAmountAllowed
}
35 changes: 27 additions & 8 deletions packages/auth/src/grant/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@interledger/open-payments'
import { AccessToken, toOpenPaymentsAccessToken } from '../accessToken/model'
import { Interaction } from '../interaction/model'
import { Subject, toOpenPaymentsSubject } from '../subject/model'
import { Tenant } from '../tenant/model'

export enum StartMethod {
Expand Down Expand Up @@ -55,6 +56,14 @@ export class Grant extends BaseModel {
to: 'accesses.grantId'
}
},
subjects: {
relation: Model.HasManyRelation,
modelClass: join(__dirname, '../subject/model'),
join: {
from: 'grants.id',
to: 'subjects.grantId'
}
},
interaction: {
relation: Model.HasManyRelation,
modelClass: join(__dirname, '../interaction/model'),
Expand All @@ -72,7 +81,8 @@ export class Grant extends BaseModel {
}
}
})
public access!: Access[]
public access?: Access[]
public subjects?: Subject[]
public state!: GrantState
public finalizationReason?: GrantFinalization
public startMethod!: StartMethod[]
Expand Down Expand Up @@ -167,20 +177,29 @@ export function toOpenPaymentsGrantContinuation(
export function toOpenPaymentsGrant(
grant: Grant,
args: ToOpenPaymentsGrantArgs,
accessToken: AccessToken,
accessItems: Access[]
accessToken?: AccessToken,
accessItems?: Access[],
subjectItems?: Subject[]
): OpenPaymentsGrant {
return {
access_token: toOpenPaymentsAccessToken(accessToken, accessItems, {
authServerUrl: args.authServerUrl
}),
access_token:
accessToken && accessItems
? toOpenPaymentsAccessToken(accessToken, accessItems, {
authServerUrl: args.authServerUrl
})
: undefined,
continue: {
access_token: {
value: grant.continueToken
},
uri: `${args.authServerUrl}/continue/${grant.continueId}`
}
}
},
subject: subjectItems?.length
? {
sub_ids: subjectItems.map(toOpenPaymentsSubject)
}
: undefined
} as OpenPaymentsGrant
}

export interface FinishableGrant extends Grant {
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/grant/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ describe('Grant Routes', (): void => {
message: 'internal server error'
})
})

test('Fails to initiate a grant w/o interact field', async (): Promise<void> => {
const ctx = createContext<CreateContext>(
{
Expand Down
Loading
Loading