Skip to content

Commit 2fb0a36

Browse files
authored
fix(auth): add tenantId in grant request route in hash generation (#3728)
* fix(auth): add tenantId in grant request route in hash generation * chore(backend): remove unused urlWithoutTenantId function * test(integration): add hash verification as part of interaction test
1 parent 568df42 commit 2fb0a36

File tree

8 files changed

+86
-32
lines changed

8 files changed

+86
-32
lines changed

packages/auth/src/interaction/routes.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
import { Interaction, InteractionState } from './model'
2323
import { Grant, GrantState, GrantFinalization } from '../grant/model'
2424
import { Access } from '../access/model'
25-
import { generateNonce } from '../shared/utils'
25+
import { ensureTrailingSlash, generateNonce } from '../shared/utils'
2626
import { GNAPErrorCode } from '../shared/gnapErrors'
2727
import { generateBaseGrant } from '../tests/grant'
2828
import { generateBaseInteraction } from '../tests/interaction'
@@ -418,13 +418,14 @@ describe('Interaction Routes', (): void => {
418418
const { clientNonce } = grant
419419
const { nonce: interactNonce, ref: interactRef } = interaction
420420

421-
const grantRequestUrl = config.authServerUrl + `/`
422-
421+
const grantRequestUrl =
422+
ensureTrailingSlash(config.authServerUrl) + grant.tenantId
423423
const data = `${clientNonce}\n${interactNonce}\n${interactRef}\n${grantRequestUrl}`
424424
const hash = crypto
425425
.createHash('sha-256')
426426
.update(data)
427427
.digest('base64')
428+
428429
clientRedirectUri.searchParams.set('hash', hash)
429430
assert.ok(interactRef)
430431
clientRedirectUri.searchParams.set('interact_ref', interactRef)
@@ -434,6 +435,7 @@ describe('Interaction Routes', (): void => {
434435
await expect(interactionRoutes.finish(ctx)).resolves.toBeUndefined()
435436
expect(ctx.response).toSatisfyApiSpec()
436437
expect(ctx.status).toBe(302)
438+
437439
expect(redirectSpy).toHaveBeenCalledWith(clientRedirectUri.toString())
438440

439441
const issuedGrant = await Grant.query().findById(grant.id)

packages/auth/src/interaction/routes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '../grant/model'
1919
import { toOpenPaymentsAccess } from '../access/model'
2020
import { GNAPErrorCode, GNAPServerRouteError } from '../shared/gnapErrors'
21-
import { generateRouteLogs } from '../shared/utils'
21+
import { ensureTrailingSlash, generateRouteLogs } from '../shared/utils'
2222
import { SubjectService } from '../subject/service'
2323
import { toOpenPaymentsSubject } from '../subject/model'
2424
import { TenantService } from '../tenant/service'
@@ -377,7 +377,8 @@ async function handleFinishableGrant(
377377

378378
const { clientNonce } = grant
379379
const { nonce: interactNonce, ref: interactRef } = interaction
380-
const grantRequestUrl = config.authServerUrl + `/`
380+
const grantRequestUrl =
381+
ensureTrailingSlash(config.authServerUrl) + grant.tenantId
381382

382383
// https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol#section-4.2.3
383384
const data = `${clientNonce}\n${interactNonce}\n${interactRef}\n${grantRequestUrl}`

packages/auth/src/shared/utils.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isValidDateString } from './utils'
1+
import { ensureTrailingSlash, isValidDateString } from './utils'
22

33
describe('utils', (): void => {
44
describe('isValidDateString', () => {
@@ -15,4 +15,13 @@ describe('utils', (): void => {
1515
expect(isValidDateString(input!)).toBe(expected)
1616
})
1717
})
18+
19+
describe('ensureTrailingSlash', (): void => {
20+
test('test ensuring trailing slash', async (): Promise<void> => {
21+
const path = '/utils'
22+
23+
expect(ensureTrailingSlash(path)).toBe(`${path}/`)
24+
expect(ensureTrailingSlash(`${path}/`)).toBe(`${path}/`)
25+
})
26+
})
1827
})

packages/auth/src/shared/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ export function generateRouteLogs(ctx: AppContext): {
2929
export function isValidDateString(date: string): boolean {
3030
return !isNaN(Date.parse(date))
3131
}
32+
33+
export function ensureTrailingSlash(str: string): string {
34+
if (!str.endsWith('/')) return `${str}/`
35+
return str
36+
}

packages/backend/src/shared/utils.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {
1010
requestWithTimeout,
1111
sleep,
1212
getTenantFromApiSignature,
13-
ensureTrailingSlash,
14-
urlWithoutTenantId
13+
ensureTrailingSlash
1514
} from './utils'
1615
import { AppServices, AppContext } from '../app'
1716
import { TestContainer, createTestApp } from '../tests/app'
@@ -457,13 +456,4 @@ describe('utils', (): void => {
457456
expect(ensureTrailingSlash(path)).toBe(`${path}/`)
458457
expect(ensureTrailingSlash(`${path}/`)).toBe(`${path}/`)
459458
})
460-
461-
test('test tenant id stripped from url', async (): Promise<void> => {
462-
expect(
463-
urlWithoutTenantId(
464-
'http://happy-life-bank-test-auth:4106/cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d'
465-
)
466-
).toBe('http://happy-life-bank-test-auth:4106')
467-
expect(urlWithoutTenantId('http://happy-life')).toBe('http://happy-life')
468-
})
469459
})

packages/backend/src/shared/utils.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,3 @@ export function ensureTrailingSlash(str: string): string {
241241
if (!str.endsWith('/')) return `${str}/`
242242
return str
243243
}
244-
245-
/**
246-
* @param url remove the tenant id from the {url}
247-
*/
248-
export function urlWithoutTenantId(url: string): string {
249-
if (url.length > 36 && validateId(url.slice(-36))) return url.slice(0, -37)
250-
return url
251-
}

test/integration/integration.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,18 +199,27 @@ describe('Integration tests', (): void => {
199199
quoteGrant.access_token.value,
200200
incomingPayment
201201
)
202+
203+
const clientNonce = crypto.randomUUID()
204+
const finishUri = 'https://example.com'
205+
202206
const outgoingPaymentGrant = await grantRequestOutgoingPayment(
203207
senderWalletAddress,
204208
{ receiveAmount: quote.receiveAmount },
205209
{
206210
method: 'redirect',
207-
uri: 'https://example.com',
208-
nonce: '456'
211+
uri: finishUri,
212+
nonce: clientNonce
209213
}
210214
)
211215
const interactRef = await consentInteractionWithInteractRef(
212216
outgoingPaymentGrant,
213-
senderWalletAddress
217+
senderWalletAddress,
218+
{
219+
clientNonce,
220+
finishUri,
221+
initialGrantUrl: senderWalletAddress.authServer
222+
}
214223
)
215224
const finalizedGrant = await grantContinue(
216225
outgoingPaymentGrant,

test/integration/lib/test-actions/index.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from 'assert'
2+
import { createHash } from 'crypto'
23
import type { MockASE } from 'test-lib'
34
import { parseCookies, urlWithoutTenantId } from '../utils'
45
import { WalletAddress, PendingGrant } from '@interledger/open-payments'
@@ -10,14 +11,21 @@ export interface TestActionsDeps {
1011
receivingASE: MockASE
1112
}
1213

14+
interface InteractionArgs {
15+
clientNonce: string
16+
initialGrantUrl: string
17+
finishUri: string
18+
}
19+
1320
export interface TestActions {
1421
consentInteraction(
1522
outgoingPaymentGrant: PendingGrant,
1623
senderWalletAddress: WalletAddress
1724
): Promise<void>
1825
consentInteractionWithInteractRef(
1926
outgoingPaymentGrant: PendingGrant,
20-
senderWalletAddress: WalletAddress
27+
senderWalletAddress: WalletAddress,
28+
args: InteractionArgs
2129
): Promise<string>
2230
admin: AdminActions
2331
openPayments: OpenPaymentsActions
@@ -29,12 +37,14 @@ export function createTestActions(deps: TestActionsDeps): TestActions {
2937
consentInteraction(deps, outgoingPaymentGrant, senderWalletAddress),
3038
consentInteractionWithInteractRef: (
3139
outgoingPaymentGrant,
32-
senderWalletAddress
40+
senderWalletAddress,
41+
args
3342
) =>
3443
consentInteractionWithInteractRef(
3544
deps,
3645
outgoingPaymentGrant,
37-
senderWalletAddress
46+
senderWalletAddress,
47+
args
3848
),
3949
admin: createAdminActions(deps),
4050
openPayments: createOpenPaymentsActions(deps)
@@ -70,7 +80,8 @@ async function consentInteraction(
7080
async function consentInteractionWithInteractRef(
7181
deps: TestActionsDeps,
7282
outgoingPaymentGrant: PendingGrant,
73-
senderWalletAddress: WalletAddress
83+
senderWalletAddress: WalletAddress,
84+
interactionArgs: InteractionArgs
7485
): Promise<string> {
7586
const { idpSecret } = deps.sendingASE.config
7687
const { interactId, nonce, cookie } = await _startAndAcceptInteraction(
@@ -95,14 +106,49 @@ async function consentInteractionWithInteractRef(
95106

96107
const redirectURI = finishResponse.headers.get('location')
97108
assert(redirectURI)
109+
expect(redirectURI.startsWith(interactionArgs.finishUri))
98110

99111
const url = new URL(redirectURI)
100112
const interact_ref = url.searchParams.get('interact_ref')
113+
const hash = url.searchParams.get('hash')
114+
115+
assert(hash)
116+
assert(interact_ref)
117+
118+
verifyHash({
119+
initialGrantUrl: interactionArgs.initialGrantUrl,
120+
clientNonce: interactionArgs.clientNonce,
121+
interactNonce: nonce,
122+
receivedHash: hash,
123+
interactRef: interact_ref
124+
})
101125
assert(interact_ref)
102126

103127
return interact_ref
104128
}
105129

130+
interface VerifyHashArgs {
131+
clientNonce: string
132+
initialGrantUrl: string
133+
receivedHash: string
134+
interactNonce: string
135+
interactRef: string
136+
}
137+
138+
async function verifyHash(args: VerifyHashArgs) {
139+
const {
140+
clientNonce,
141+
interactNonce,
142+
interactRef,
143+
initialGrantUrl,
144+
receivedHash
145+
} = args
146+
const data = `${clientNonce}\n${interactNonce}\n${interactRef}\n${initialGrantUrl}`
147+
const hash = createHash('sha-256').update(data).digest('base64')
148+
149+
expect(hash).toBe(receivedHash)
150+
}
151+
106152
async function _startAndAcceptInteraction(
107153
deps: TestActionsDeps,
108154
outgoingPaymentGrant: PendingGrant,

0 commit comments

Comments
 (0)