Skip to content

Commit

Permalink
chore: Implemented credential_offer decoding and added full flow tests
Browse files Browse the repository at this point in the history
  • Loading branch information
zoemaas committed May 7, 2024
1 parent 4eaa40a commit 578ee2c
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 24 deletions.
8 changes: 3 additions & 5 deletions packages/client/lib/CredentialOfferClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,20 @@ export class CredentialOfferClient {
const version = determineSpecVersionFromURI(uri);
let credentialOffer: CredentialOffer;
let credentialOfferPayload: CredentialOfferPayload;
// credential offer was introduced in draft 9 and credential_offer_uri in draft 11
if (version < OpenId4VCIVersion.VER_1_0_11) {
credentialOfferPayload = convertURIToJsonObject(uri, {
arrayTypeProperties: ['credential_type'],
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['issuer', 'credential_type'],
requiredProperties: uri.includes('credential_offer=') ? ['credential_offer'] : ['issuer', 'credential_type'],
}) as CredentialOfferPayloadV1_0_09;
credentialOffer = {
credential_offer: credentialOfferPayload,
};
} else {
credentialOffer = convertURIToJsonObject(uri, {
arrayTypeProperties: ['credentials'],
arrayTypeProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['credential_offer'],
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['credential_offer'],
}) as CredentialOfferV1_0_11;
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
}
}

const request = await toUniformCredentialOfferRequest(credentialOffer, {
Expand Down
44 changes: 29 additions & 15 deletions packages/client/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ describe('OID4VCI-Client should', () => {
const INITIATE_QR =
'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true';
const OFFER_QR =
'openid-credential-offer://credential_offer=%7B%22credential_issuer%22:%22https://issuer.research.identiproof.io%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D';
const HTTPS = 'https://issuer.research.identiproof.io?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true'
'openid-credential-offer://?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D';
const HTTPS_INITIATE_QR = 'https://issuer.research.identiproof.io?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true'
const HTTPS_OFFER_QR_AUTHORIZATION_CODE =
'https://issuer.research.identiproof.io?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D';
const HTTPS_OFFER_QR_PRE_AUTHORIZED = 'https://issuer.research.identiproof.io?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D'

function succeedWithAFullFlowWithClientSetup() {
nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA));
Expand Down Expand Up @@ -79,7 +82,7 @@ describe('OID4VCI-Client should', () => {
await assertionOfsucceedWithAFullFlowWithClient(client);
});

test.skip('succeed with a full flow with the client using OpenID4VCI version 11 and deeplink', async () => {
it('succeed with a full flow with the client using OpenID4VCI version 11 and deeplink', async () => {
succeedWithAFullFlowWithClientSetup();
const client = await OpenID4VCIClient.fromURI({
uri: OFFER_QR,
Expand All @@ -90,21 +93,32 @@ describe('OID4VCI-Client should', () => {
await assertionOfsucceedWithAFullFlowWithClient(client);
});

/*
openid-initiate-issuance://?issuer=https://issuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true
####################################################################################################################################
openid-credential-offer://credential_offer={"credential_issuer":"https://issuer.research.identiproof.io","credentials":[{"format":"jwt_vc_json","types":["VerifiableCredential","UniversityDegreeCredential"]}],"issuer_state":"eyJhbGciOiJSU0Et...FYUaBy"}
it('succeed with a full flow with the client using OpenID4VCI draft < 9 and https', async () => {
succeedWithAFullFlowWithClientSetup()
const client = await OpenID4VCIClient.fromURI({
uri: HTTPS_INITIATE_QR,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId'
})
await assertionOfsucceedWithAFullFlowWithClient(client);
})

####################################################################################################################################
it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and authorization_code flow', async () => {
succeedWithAFullFlowWithClientSetup()
const client = await OpenID4VCIClient.fromURI({
uri: HTTPS_OFFER_QR_AUTHORIZATION_CODE,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId'
})
await assertionOfsucceedWithAFullFlowWithClient(client);
})

https://issuer.research.identiproof.io?issuer=https://issuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true
*/
it('succeed with a full flow with the client using OpenID4VCI version 11 and https', async () => {
it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and preauthorized_code flow', async () => {
succeedWithAFullFlowWithClientSetup()
const client = await OpenID4VCIClient.fromURI({
uri: HTTPS,
uri: HTTPS_OFFER_QR_PRE_AUTHORIZED,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId'
Expand All @@ -119,7 +133,7 @@ https://issuer.research.identiproof.io?issuer=https://issuer.research.identiproo
expect(client.getCredentialEndpoint()).toEqual('https://issuer.research.identiproof.io/credential');
expect(client.getAccessTokenEndpoint()).toEqual('https://auth.research.identiproof.io/oauth2/token');

const accessToken = await client.acquireAccessToken({ pin: '1234' });
const accessToken = await client.acquireAccessToken({ pin: '1234', code: 'ABCD' });
expect(accessToken).toEqual(mockedAccessTokenResponse);

const credentialResponse = await client.acquireCredentials({
Expand Down
37 changes: 33 additions & 4 deletions packages/common/lib/functions/Encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,46 @@ export function convertJsonToURI(
}

/**
* @function decodeUriAsJson decodes a URI into a Json object
* @type {(uri: string, opts?: DecodeURIAsJsonOpts): unknown} convertURIToJsonObject converts an URI into a Json object decoding its properties
* @param uri string
* @param opts:
* @param opts
* - requiredProperties: the required properties
* - arrayTypeProperties: properties that can show up more that once
* @returns JSON object
*/
export function convertURIToJsonObject(uri: string, opts?: DecodeURIAsJsonOpts): unknown {
if (!uri || (opts?.requiredProperties && !opts.requiredProperties?.every((p) => uri.includes(p)))) {
throw new Error(BAD_PARAMS);
}

if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer_uri')) {
const value = uri.includes('?') ? uri.split('?')[1].split('=') : uri
if (!Array.isArray(value) || value[0] !== 'credential_offer_uri') {
throw new Error(`URL does not include credential_offer_uri`)
}
return {
credential_offer_uri: value[1]
}
}

if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer')) {
const decodedUri = decodeURIComponent(uri)
const value = decodedUri.includes('?') ? decodedUri.split('?')[1].split('=') : decodedUri
if (!Array.isArray(value) || value[0] !== 'credential_offer') {
throw new Error(`URL does not include credential_offer`)
}
try {
const json = JSON.parse(value[1])
return {
credential_offer: json
}
} catch(e) {
console.log(e)
}
}

const uriComponents = getURIComponentsAsArray(uri, opts?.arrayTypeProperties);

return decodeJsonProperties(uriComponents);
}

Expand Down Expand Up @@ -128,8 +157,8 @@ function decodeJsonProperties(parts: string[] | string[][]): unknown {

/**
* @function get URI Components as Array
* @param uri string
* @param arrayTypes array of string containing array like keys
* @param {string} uri uri
* @param {string[]} [arrayTypes] array of string containing array like keys
*/
function getURIComponentsAsArray(uri: string, arrayTypes?: string[]): string[] | string[][] {
const parts = uri.includes('?') ? uri.split('?')[1] : uri.includes('://') ? uri.split('://')[1] : uri;
Expand Down

0 comments on commit 578ee2c

Please sign in to comment.