Skip to content

Conversation

cozminu
Copy link
Contributor

@cozminu cozminu commented May 27, 2025

Changes proposed in this pull request

Context

fixes #3343

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Make sure that all checks pass
  • Bruno collection updated (if necessary)
  • Documentation issue created with user-docs label (if necessary)
  • OpenAPI specs updated (if necessary)

@cozminu cozminu self-assigned this May 27, 2025
Copy link

netlify bot commented May 27, 2025

Deploy Preview for brilliant-pasca-3e80ec ready!

Name Link
🔨 Latest commit 4b6f81c
🔍 Latest deploy log https://app.netlify.com/projects/brilliant-pasca-3e80ec/deploys/68c0374dc0e20a0008ee53d0
😎 Deploy Preview https://deploy-preview-3440--brilliant-pasca-3e80ec.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions bot added type: tests Testing related type: source Changes business logic pkg: auth Changes in the GNAP auth package. labels May 27, 2025
Copy link

github-actions bot commented Jun 2, 2025

🚀 Performance Test Results

Test Configuration:

  • VUs: 4
  • Duration: 1m0s

Test Metrics:

  • Requests/s: 38.08
  • Iterations/s: 12.72
  • Failed Requests: 0.00% (0 of 2299)
📜 Logs

> [email protected] run-tests:testenv /home/runner/work/rafiki/rafiki/test/performance
> ./scripts/run-tests.sh -e test "-k" "-q" "--vus" "4" "--duration" "1m"

Cloud Nine GraphQL API is up: http://localhost:3101/graphql
Cloud Nine Wallet Address is up: http://localhost:3100/
Happy Life Bank Address is up: http://localhost:4100/
cloud-nine-wallet-test-backend already set
cloud-nine-wallet-test-auth already set
happy-life-bank-test-backend already set
happy-life-bank-test-auth already set
     data_received..................: 830 kB 14 kB/s
     data_sent......................: 1.8 MB 29 kB/s
     http_req_blocked...............: avg=7.64µs   min=3.1µs   med=6.01µs   max=740.24µs p(90)=7.13µs   p(95)=7.73µs  
     http_req_connecting............: avg=832ns    min=0s      med=0s       max=685.9µs  p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=104.29ms min=7.77ms  med=85.63ms  max=600.3ms  p(90)=177.49ms p(95)=205.07ms
       { expected_response:true }...: avg=104.29ms min=7.77ms  med=85.63ms  max=600.3ms  p(90)=177.49ms p(95)=205.07ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 2299
     http_req_receiving.............: avg=98.73µs  min=27.22µs med=85.8µs   max=955.85µs p(90)=127.54µs p(95)=161.1µs 
     http_req_sending...............: avg=45.06µs  min=11.66µs med=31.19µs  max=6.49ms   p(90)=46.4µs   p(95)=63.84µs 
     http_req_tls_handshaking.......: avg=0s       min=0s      med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=104.15ms min=7.56ms  med=85.48ms  max=600.23ms p(90)=177.35ms p(95)=204.94ms
     http_reqs......................: 2299   38.076114/s
     iteration_duration.............: avg=313.93ms min=201.7ms med=299.83ms max=1.18s    p(90)=383.76ms p(95)=419.52ms
     iterations.....................: 768    12.719642/s
     vus............................: 4      min=4       max=4 
     vus_max........................: 4      min=4       max=4 

@cozminu cozminu requested review from njlie and mkurapov June 2, 2025 12:30
Comment on lines 31 to 35
jest.mock('../access/types', () => ({
isIncomingPaymentAccessRequest: (access: AccessRequest) =>
access.type === 'incoming-payment',
isQuoteAccessRequest: (access: AccessRequest) => access.type === 'quote'
}))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this mock?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

canSkipInteraction function uses them and the purpose of the unit test is to test only the unit and not the deps, but I see there are also benefits from using the unmocked functions. removed the mock

Copy link
Contributor

@njlie njlie left a comment

Choose a reason for hiding this comment

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

Overall LGTM, just a couple things

})

describe('getByGrant', (): void => {
test('gets access', async () => {
Copy link
Contributor

@njlie njlie Jun 16, 2025

Choose a reason for hiding this comment

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

nit: this is getting subject, not access

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, fixed

@github-actions github-actions bot added the pkg: backend Changes in the backend package. label Jun 20, 2025
Copy link
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Few comments!

Comment on lines 223 to 225
400,
GNAPErrorCode.InvalidRequest,
err.message || 'invalid request'
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 match GrantErrors to their respective HTTP codes and GNAPErrorCodes.
This is similar to how @oana-lolea maps AccessErrors to the respective code & error codes in her PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

const emptyAccess = (body.access_token?.access?.length ?? 0) === 0

if (emptySubject && emptyAccess) {
throw new Error('subject or access_token required')
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this throw a GrantError instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, adapted to throw GrantError

interface IntrospectBody {
access_token: string
access?: AccessItem[]
subjects?: SubjectItem[]
Copy link
Contributor

Choose a reason for hiding this comment

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

If we don't use subjects anywhere (given that we don't pass it in during the introspection request), then its OK to leave out

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

@cozminu cozminu requested a review from mkurapov June 25, 2025 14:08
Copy link
Contributor

@njlie njlie left a comment

Choose a reason for hiding this comment

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

Just one suggestion to be applied accross all the places where trx was cast in order to silence a Typescript error.

let appContainer: TestContainer
let accessService: AccessService
let trx: Knex.Transaction
const trx: Knex.Transaction = null as unknown as Knex.Transaction
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be healthier to actually initialize trx in the beforeAll block so that the tables get properly truncated when the test is finished, but the editor also doesn't flag a Typescript error. I assume this type casting was done to address the TS error.

beforeAll(async (): Promise<void> => {
	...
	trx = await appContainer.knex.transaction()
})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, It was a fix to a TS error

Variable 'trx' is used before being assigned.ts(2454)

Using a transaction is tricky because it can deadlock tests because some queries are done within a transaction and others are not. Managed to fix it, but in another manner.

let trx: KnexOrTransaction

beforeAll(async (): Promise<void> => {
	...
	trx = await appContainer.knex
})

Is this ok with you?

let deps: IocContract<AppServices>
let appContainer: TestContainer
let trx: Knex.Transaction
const trx: Knex.Transaction = undefined as unknown as Knex.Transaction
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to prior comment on initializing trx.

let deps: IocContract<AppServices>
let appContainer: TestContainer
let trx: Knex.Transaction
const trx: Knex.Transaction = undefined as unknown as Knex.Transaction
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to prior comment on initializing trx.

let deps: IocContract<AppServices>
let appContainer: TestContainer
let trx: Knex.Transaction
const trx: Knex.Transaction = undefined as unknown as Knex.Transaction
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to prior comment on initializing trx.

let appContainer: TestContainer
let grantService: GrantService
let trx: Knex.Transaction
const trx: Knex.Transaction = null as unknown as Knex.Transaction
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to prior comment on initializing trx.

@@ -0,0 +1,86 @@
import { AccessRequest } from '../access/types'
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding these tests! Especially since they cover logic that existed before this PR

let interaction: Interaction
let token: AccessToken
let trx: Knex.Transaction
const trx: Knex.Transaction = null as unknown as Knex.Transaction
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to prior comment on initializing trx.

let client: string
let amtDelivered: bigint
let trx: Knex.Transaction
const trx: Knex.Transaction = null as unknown as Knex.Transaction
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto to prior comment on initializing trx.

client: grant.client,
access: grant.access?.map((item) => accessToGraphql(item)),
access: grant.access?.map((item) => accessToGraphql(item)) || [],
subject: grant.subjects?.map((item) => subjectToGraphql(item)) || [],
Copy link
Contributor

Choose a reason for hiding this comment

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

grant.subjects only exists if we add it to the withGraphFetched request in the service methods OR we have subjects as a separate "child" resolver under grants, and call the subjectService.getByGrantId directly.

An example of this is the Asset > sendingFee resolver in backend.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed with withGraphFetched('subjects') in the service methods

"Wallet address of the grantee's account."
client: String!
"Details of the access provided by the grant."
access: [Access!]!
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can keep it as [Access!]!, if there is no access it can simply be an empty array.

400,
GNAPErrorCode.InvalidRequest,
'access identifier required'
err instanceof Error ? err.message : ''
Copy link
Contributor

Choose a reason for hiding this comment

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

to avoid empty string, let's provide some generic message instead at least. It's less important for the client, its more so for us to find where to debug it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed to 'unknown error while checking interaction requirement'

'subject id must be a valid https url'
)
}
if (subject.format != 'uri') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (subject.format != 'uri') {
if (subject.format !== 'uri') {


function validateSubjectRequest(subject: SubjectRequest): void {
try {
if (!subject.id.startsWith('https://')) throw 1
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure if @njlie agrees, but I think just checking that it is a valid URL is sufficient, without checking the https://. IMO it can be up to the ASE to determine how strict they want to be with the specifics, like protocol.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the record, I also agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

subjectItems: Subject[]
): OpenPaymentsGrant {
return {
access_token: toOpenPaymentsAccessToken(accessToken, accessItems, {
Copy link
Contributor

Choose a reason for hiding this comment

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

access_token could be undefined now, just like subject

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

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

ctx.body = {
grantId: interaction.grant.id,
access: access.map(toOpenPaymentsAccess),
subjects: subjects.map(toOpenPaymentsSubject),
Copy link
Contributor

Choose a reason for hiding this comment

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

there can only be one subject for a grant, but the sub_ids can be many. So this can either be a subject object with sub_ids array, (like the Open Payments spec), or we can directly return sub_ids here.

Copy link
Contributor

Choose a reason for hiding this comment

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

The same applies for the subject updates in the GraphQL schema

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed to subject object with sub_ids array.
I'm not aware of a subject updates.

@github-actions github-actions bot removed the pkg: backend Changes in the backend package. label Jul 28, 2025
const fetchedGrant = await grantService.getByIdWithAccess(grant.id)
const grantRequest: GrantRequest = {
...BASE_GRANT_REQUEST,
subject: {
Copy link
Contributor

Choose a reason for hiding this comment

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

access should still be included here so that this service call can properly test that it also retrieves an access field along with the grant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@cozminu cozminu requested a review from mkurapov August 5, 2025 13:40
Copy link
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Thinking through this a bit, I think we need to update the response types.
Since subject should only be returned after we have gone through a successful interaction, we should:

  • Make sure we do not return subject in the initial grant request
  • Return subject in the grant continue request only

Section 3.4 of GNAP:

The grant request MUST be in the approved state to return this field in the response.

This will require open-payments-specification changes, but we can work together @cozminu and I can take care of those changes

CC @njlie for a double check :)

Comment on lines 138 to 140
err instanceof Error
? err.message
: 'unknown error while checking interaction requirement'
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at what canSkipInteraction throws, we should instead use err.description, since that will always be set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

casted e as GrantError to fix this.

...
} catch (e) {
const err = e as GrantError
throw new GNAPServerRouteError(
  400,
  GNAPErrorCode.InvalidRequest,
  err.message
)
...

Other option would be to throw 500 if e is not GrantError


export class GrantError extends Error {
code: GrantErrorCode
constructor(code: GrantErrorCode, description?: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
constructor(code: GrantErrorCode, description?: string) {
constructor(code: GrantErrorCode, description: string) {

Looks like description is set everywhere already

err
errorToHTTPCode[err.code],
errorToGNAPCode[err.code],
err.message || 'invalid request'
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
err.message || 'invalid request'
err.description || 'invalid request'

same as the other comment, we can use err.description here, since it will always be set and is most useful. Or rename GrantError.description to GrantError.message

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed description to message to fit Error class

)
}
const access = await deps.accessService.getByGrant(grant.id)
const subjects = await deps.subjectService.getByGrant(grant.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since subjects always requires interaction, I think we don't need to fetch subjects for the non-interactive grant creation.

@njlie
Copy link
Contributor

njlie commented Aug 13, 2025

Thinking through this a bit, I think we need to update the response types. Since subject should only be returned after we have gone through a successful interaction, we should:

  • Make sure we do not return subject in the initial grant request
  • Return subject in the grant continue request only

Section 3.4 of GNAP:

The grant request MUST be in the approved state to return this field in the response.

This will require open-payments-specification changes, but we can work together @cozminu and I can take care of those changes

CC @njlie for a double check :)

Based on the quoted excerpt, I thought that the AS could deem subject information something noninteractive - the grant just needed to be approved. But I think ultimately the interpretation is correct, based on the following:

The AS MUST return the subject field only in cases where the AS is sure that the RO and the end user are the same party. This can be accomplished through some forms of interaction with the RO (Section 4).

Copy link
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Looks good, just a few minor comments

Comment on lines 287 to 289
if (!trx) {
await grantTrx.rollback()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (!trx) {
await grantTrx.rollback()
}
await grantTrx.rollback()

as grantTrx is always defined. Same for L281-283

Copy link
Contributor Author

@cozminu cozminu Aug 25, 2025

Choose a reason for hiding this comment

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

Tests will fail if I do this. Reason being that the transaction starts on a higher level if trx is provided and rollback or commit will be called twice: once here and once on the higher level.

@cozminu cozminu requested a review from mkurapov September 1, 2025 14:15
Copy link
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Looks good! Will just wait for RAF-1147 / #3608

mkurapov
mkurapov previously approved these changes Sep 2, 2025
@mkurapov
Copy link
Contributor

mkurapov commented Sep 5, 2025

@cozminu if you merge in latest changes from main, you should see open-payments-specifications as a submodule. You can now go to the submodule, checkout v1.1.0 tag of the specifications (the change that contains the subject additions), and finalize the PR 👍

properties:
access:
$ref: ./auth-server.yaml#/components/schemas/access
subject:
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 make this spec standalone, and move over the correct components (access/subject) into this file, similar to how it was done for token-introspection

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: auth Changes in the GNAP auth package. type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add subject field to Authorization Server
3 participants