Skip to content

Commit e1c8bc2

Browse files
committed
chore: SSISDK-5 refactor and additional test coverage
1 parent 342f440 commit e1c8bc2

11 files changed

Lines changed: 2533 additions & 2112 deletions

packages/client/lib/CredentialOfferClient.ts

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,15 @@ import {
77
CredentialOfferRequestWithBaseUrl,
88
CredentialOfferV1_0_11,
99
CredentialOfferV1_0_13,
10-
decodeJsonProperties,
1110
determineSpecVersionFromURI,
12-
getClientIdFromCredentialOfferPayload,
13-
getURIComponentsAsArray,
1411
OpenId4VCIVersion,
15-
PRE_AUTH_CODE_LITERAL,
1612
PRE_AUTH_GRANT_LITERAL,
1713
toUniformCredentialOfferRequest
1814
} from '@sphereon/oid4vci-common'
1915
import Debug from 'debug'
2016

2117
import { LOG } from './types'
22-
import { fetch } from 'cross-fetch'
23-
import { isUrlEncoded } from './functions'
18+
import { constructBaseResponse, handleCredentialOfferUri } from './functions'
2419

2520
const debug = Debug('sphereon:oid4vci:offer');
2621

@@ -48,20 +43,7 @@ export class CredentialOfferClient {
4843
};
4944
} else {
5045
if (uri.includes('credential_offer_uri')) {
51-
const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string> // FIXME
52-
const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri'])
53-
const decodedUri = isUrlEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri
54-
const response = await fetch(decodedUri)
55-
if (!(response && response.status >= 200 && response.status < 400)) {
56-
return Promise.reject(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)
57-
}
58-
59-
if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
60-
return Promise.reject('the credential offer URI endpoint did not return content type application/json')
61-
}
62-
credentialOffer = {
63-
credential_offer: decodeJsonProperties(await response.json())
64-
} as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
46+
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
6547
} else {
6648
credentialOffer = convertURIToJsonObject(uri, {
6749
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
@@ -70,34 +52,22 @@ export class CredentialOfferClient {
7052
}) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
7153
}
7254
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
73-
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri)
55+
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri) // cannot be reached since convertURIToJsonObject will check the params
7456
}
7557
}
7658

7759
const request = await toUniformCredentialOfferRequest(credentialOffer, {
7860
...opts,
79-
version,
80-
});
81-
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
82-
const grants = request.credential_offer?.grants;
61+
version
62+
})
8363

8464
return {
85-
scheme,
86-
baseUrl,
87-
...(clientId && { clientId }),
88-
...request,
89-
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
90-
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
91-
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
92-
}),
65+
...constructBaseResponse(request, scheme, baseUrl),
9366
userPinRequired:
9467
request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.user_pin_required ??
9568
!!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ??
96-
false,
97-
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
98-
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
99-
}),
100-
};
69+
false
70+
}
10171
}
10272

10373
public static toURI(

packages/client/lib/CredentialOfferClientV1_0_11.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class CredentialOfferClientV1_0_11 {
4444
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
4545
}) as CredentialOfferV1_0_11;
4646
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
47-
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
47+
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri); // cannot be reached since convertURIToJsonObject will check the params
4848
}
4949
}
5050

packages/client/lib/CredentialOfferClientV1_0_13.ts

Lines changed: 13 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,30 @@ import {
33
convertURIToJsonObject,
44
CredentialOffer,
55
CredentialOfferRequestWithBaseUrl,
6-
CredentialOfferV1_0_11,
76
CredentialOfferV1_0_13,
8-
decodeJsonProperties,
97
determineSpecVersionFromURI,
10-
getClientIdFromCredentialOfferPayload,
11-
getURIComponentsAsArray,
128
OpenId4VCIVersion,
13-
PRE_AUTH_CODE_LITERAL,
149
PRE_AUTH_GRANT_LITERAL,
1510
toUniformCredentialOfferRequest
1611
} from '@sphereon/oid4vci-common'
1712
import Debug from 'debug'
18-
import { fetch } from 'cross-fetch'
19-
import { isUrlEncoded } from './functions'
13+
import { constructBaseResponse, handleCredentialOfferUri } from './functions'
2014

2115
const debug = Debug('sphereon:oid4vci:offer');
2216

2317
export class CredentialOfferClientV1_0_13 {
2418
public static async fromURI(uri: string, opts?: { resolve?: boolean }): Promise<CredentialOfferRequestWithBaseUrl> {
25-
debug(`Credential Offer URI: ${uri}`);
19+
debug(`Credential Offer URI: ${uri}`)
2620
if (!uri.includes('?') || !uri.includes('://')) {
27-
debug(`Invalid Credential Offer URI: ${uri}`);
28-
throw Error(`Invalid Credential Offer Request`);
21+
debug(`Invalid Credential Offer URI: ${uri}`)
22+
throw Error(`Invalid Credential Offer Request`)
2923
}
30-
const scheme = uri.split('://')[0];
31-
const baseUrl = uri.split('?')[0];
24+
const scheme = uri.split('://')[0]
25+
const baseUrl = uri.split('?')[0]
3226
const version = determineSpecVersionFromURI(uri)
3327
let credentialOffer: CredentialOffer
3428
if (uri.includes('credential_offer_uri')) { // FIXME deduplicate
35-
const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string> // FIXME
36-
const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri'])
37-
const decodedUri = isUrlEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri
38-
const response = await fetch(decodedUri)
39-
if (!(response && response.status >= 200 && response.status < 400)) {
40-
return Promise.reject(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)
41-
}
42-
43-
if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
44-
return Promise.reject('the credential offer URI endpoint did not return content type application/json')
45-
}
46-
credentialOffer = decodeJsonProperties(await response.json()) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
29+
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_13
4730
} else {
4831
credentialOffer = convertURIToJsonObject(uri, {
4932
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
@@ -54,30 +37,18 @@ export class CredentialOfferClientV1_0_13 {
5437
}) as CredentialOfferV1_0_13
5538
}
5639
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
57-
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri)
40+
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri) // cannot be reached since convertURIToJsonObject will check the params
5841
}
5942

6043
const request = await toUniformCredentialOfferRequest(credentialOffer, {
6144
...opts,
62-
version,
63-
});
64-
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
65-
const grants = request.credential_offer?.grants;
45+
version
46+
})
6647

6748
return {
68-
scheme,
69-
baseUrl,
70-
...(clientId && { clientId }),
71-
...request,
72-
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
73-
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
74-
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
75-
}),
76-
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false,
77-
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
78-
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
79-
}),
80-
};
49+
...constructBaseResponse(request, scheme, baseUrl),
50+
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false
51+
}
8152
}
8253

8354
public static toURI(

packages/client/lib/__tests__/CredentialRequestClient.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,62 @@ describe('Credential Request Client with different issuers ', () => {
341341
});
342342
});
343343
});
344+
345+
describe('Credential Offer Client error handling', () => {
346+
beforeEach(() => {
347+
nock.cleanAll()
348+
})
349+
350+
afterEach(() => {
351+
nock.cleanAll()
352+
})
353+
354+
it('should handle non-200 response from credential offer URI endpoint', async () => {
355+
const IRR_URI = 'openid-credential-offer://?credential_offer_uri=https%3A%2F%2Ftest.example.com%2Foffer'
356+
357+
nock('https://test.example.com')
358+
.get('/offer')
359+
.reply(404, 'Not Found')
360+
361+
await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toMatch(
362+
/the credential offer URI endpoint call was not successful. http code 404 - reason Not Found/
363+
)
364+
})
365+
366+
it('should handle invalid content type from credential offer URI endpoint', async () => {
367+
const IRR_URI = 'openid-credential-offer://?credential_offer_uri=https%3A%2F%2Ftest.example.com%2Foffer'
368+
369+
nock('https://test.example.com')
370+
.get('/offer')
371+
.reply(200, 'plain text response', { 'Content-Type': 'text/plain' })
372+
373+
await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toMatch(
374+
'the credential offer URI endpoint did not return content type application/json'
375+
)
376+
})
377+
378+
it('should handle missing required credential offer properties', async () => {
379+
const IRR_URI = 'openid-credential-offer://?invalid_param=test'
380+
381+
await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toThrow('Wrong parameters provided')
382+
})
383+
384+
it('should handle credential offer URI with credential_offer param', async () => {
385+
const IRR_URI = 'openid-credential-offer://?credential_offer=%7B%22test%22%3A%22value%22%7D'
386+
387+
const client = await CredentialOfferClient.fromURI(IRR_URI)
388+
expect(client.credential_offer).toBeDefined()
389+
})
390+
391+
it('should handle URL encoded credential offer URI properly', async () => {
392+
const encodedUri = 'https%3A%2F%2Ftest.example.com%2Foffer'
393+
const IRR_URI = `openid-credential-offer://?credential_offer_uri=${encodedUri}`
394+
395+
nock('https://test.example.com')
396+
.get('/offer')
397+
.reply(200, { test: 'value' }, { 'Content-Type': 'application/json' })
398+
399+
const client = await CredentialOfferClient.fromURI(IRR_URI)
400+
expect(client.credential_offer).toBeDefined()
401+
})
402+
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
decodeJsonProperties,
3+
getClientIdFromCredentialOfferPayload,
4+
getURIComponentsAsArray,
5+
PRE_AUTH_CODE_LITERAL,
6+
PRE_AUTH_GRANT_LITERAL,
7+
UniformCredentialOfferRequest
8+
} from '@sphereon/oid4vci-common'
9+
import { fetch } from 'cross-fetch'
10+
11+
export function isUriEncoded(str: string): boolean {
12+
const pattern = /%[0-9A-F]{2}/i
13+
return pattern.test(str)
14+
}
15+
16+
export async function handleCredentialOfferUri(uri: string) {
17+
const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string>
18+
const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri'])
19+
const decodedUri = isUriEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri
20+
const response = await fetch(decodedUri)
21+
22+
if (!(response && response.status >= 200 && response.status < 400)) {
23+
return Promise.reject(`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`)
24+
}
25+
26+
if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
27+
return Promise.reject('the credential offer URI endpoint did not return content type application/json')
28+
}
29+
30+
return {
31+
credential_offer: decodeJsonProperties(await response.json())
32+
}
33+
}
34+
35+
export function constructBaseResponse(request: UniformCredentialOfferRequest, scheme: string, baseUrl: string) {
36+
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer)
37+
const grants = request.credential_offer?.grants
38+
39+
return {
40+
scheme,
41+
baseUrl,
42+
...(clientId && { clientId }),
43+
...request,
44+
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
45+
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
46+
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL]
47+
}),
48+
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
49+
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code
50+
})
51+
}
52+
}

packages/client/lib/functions/UrlUtil.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/client/lib/functions/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export * from './AuthorizationUtil';
22
export * from './notifications';
33
export * from './OpenIDUtils';
44
export * from './AccessTokenUtil';
5-
export * from './UrlUtil';
5+
export * from './CredentialOfferCommons';

0 commit comments

Comments
 (0)