Skip to content

Commit 8c1adab

Browse files
committed
Merge remote-tracking branch 'origin/feature/SSISDK-5_credential_offer_uri' into develop
# Conflicts: # packages/client/lib/CredentialOfferClient.ts # packages/client/lib/CredentialOfferClientV1_0_13.ts # packages/client/lib/functions/UrlUtil.ts # packages/issuer-rest/lib/oid4vci-api-functions.ts # pnpm-lock.yaml
2 parents 5b1178d + e1c8bc2 commit 8c1adab

10 files changed

Lines changed: 317 additions & 103 deletions

packages/client/lib/CredentialOfferClient.ts

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,17 @@ 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 { fetch } from 'cross-fetch';
2016
import Debug from 'debug';
2117

22-
import { isUrlEncoded } from './functions';
23-
import { LOG } from './types';
18+
import { LOG } from './types'
19+
import { fetch } from 'cross-fetch'
20+
import { constructBaseResponse, handleCredentialOfferUri, isUrlEncoded } from './functions'
2421

2522
const debug = Debug('sphereon:oid4vci:offer');
2623

@@ -48,22 +45,7 @@ export class CredentialOfferClient {
4845
};
4946
} else {
5047
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(
57-
`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`,
58-
);
59-
}
60-
61-
if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
62-
return Promise.reject('the credential offer URI endpoint did not return content type application/json');
63-
}
64-
credentialOffer = {
65-
credential_offer: decodeJsonProperties(await response.json()),
66-
} as CredentialOfferV1_0_11 | CredentialOfferV1_0_13;
48+
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
6749
} else {
6850
credentialOffer = convertURIToJsonObject(uri, {
6951
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
@@ -72,34 +54,22 @@ export class CredentialOfferClient {
7254
}) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13;
7355
}
7456
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
75-
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
57+
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri) // cannot be reached since convertURIToJsonObject will check the params
7658
}
7759
}
7860

7961
const request = await toUniformCredentialOfferRequest(credentialOffer, {
8062
...opts,
81-
version,
82-
});
83-
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
84-
const grants = request.credential_offer?.grants;
63+
version
64+
})
8565

8666
return {
87-
scheme,
88-
baseUrl,
89-
...(clientId && { clientId }),
90-
...request,
91-
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
92-
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
93-
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
94-
}),
67+
...constructBaseResponse(request, scheme, baseUrl),
9568
userPinRequired:
9669
request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.user_pin_required ??
9770
!!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ??
98-
false,
99-
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
100-
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
101-
}),
102-
};
71+
false
72+
}
10373
}
10474

10575
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: 19 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +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,
15-
toUniformCredentialOfferRequest,
16-
} from '@sphereon/oid4vci-common';
17-
import { fetch } from 'cross-fetch';
18-
import Debug from 'debug';
19-
20-
import { isUrlEncoded } from './functions';
10+
toUniformCredentialOfferRequest
11+
} from '@sphereon/oid4vci-common'
12+
import Debug from 'debug'
13+
import { constructBaseResponse, handleCredentialOfferUri } from './functions'
2114

2215
const debug = Debug('sphereon:oid4vci:offer');
2316

2417
export class CredentialOfferClientV1_0_13 {
2518
public static async fromURI(uri: string, opts?: { resolve?: boolean }): Promise<CredentialOfferRequestWithBaseUrl> {
26-
debug(`Credential Offer URI: ${uri}`);
19+
debug(`Credential Offer URI: ${uri}`)
2720
if (!uri.includes('?') || !uri.includes('://')) {
28-
debug(`Invalid Credential Offer URI: ${uri}`);
29-
throw Error(`Invalid Credential Offer Request`);
21+
debug(`Invalid Credential Offer URI: ${uri}`)
22+
throw Error(`Invalid Credential Offer Request`)
3023
}
31-
const scheme = uri.split('://')[0];
32-
const baseUrl = uri.split('?')[0];
33-
const version = determineSpecVersionFromURI(uri);
34-
let credentialOffer: CredentialOffer;
35-
if (uri.includes('credential_offer_uri')) {
36-
// FIXME deduplicate
37-
const uriObj = getURIComponentsAsArray(uri) as unknown as Record<string, string>; // FIXME
38-
const credentialOfferUri = decodeURIComponent(uriObj['credential_offer_uri']);
39-
const decodedUri = isUrlEncoded(credentialOfferUri) ? decodeURIComponent(credentialOfferUri) : credentialOfferUri;
40-
const response = await fetch(decodedUri);
41-
if (!(response && response.status >= 200 && response.status < 400)) {
42-
return Promise.reject(
43-
`the credential offer URI endpoint call was not successful. http code ${response.status} - reason ${response.statusText}`,
44-
);
45-
}
46-
47-
if (response.headers.get('Content-Type')?.startsWith('application/json') === false) {
48-
return Promise.reject('the credential offer URI endpoint did not return content type application/json');
49-
}
50-
credentialOffer = decodeJsonProperties(await response.json()) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13;
24+
const scheme = uri.split('://')[0]
25+
const baseUrl = uri.split('?')[0]
26+
const version = determineSpecVersionFromURI(uri)
27+
let credentialOffer: CredentialOffer
28+
if (uri.includes('credential_offer_uri')) { // FIXME deduplicate
29+
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_13
5130
} else {
5231
credentialOffer = convertURIToJsonObject(uri, {
5332
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
@@ -58,30 +37,18 @@ export class CredentialOfferClientV1_0_13 {
5837
}) as CredentialOfferV1_0_13;
5938
}
6039
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
61-
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
6241
}
6342

6443
const request = await toUniformCredentialOfferRequest(credentialOffer, {
6544
...opts,
66-
version,
67-
});
68-
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
69-
const grants = request.credential_offer?.grants;
45+
version
46+
})
7047

7148
return {
72-
scheme,
73-
baseUrl,
74-
...(clientId && { clientId }),
75-
...request,
76-
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
77-
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
78-
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
79-
}),
80-
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false,
81-
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
82-
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
83-
}),
84-
};
49+
...constructBaseResponse(request, scheme, baseUrl),
50+
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false
51+
}
8552
}
8653

8754
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 & 4 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)