Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/SSISDK-5_credential_offer_uri #180

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 20 additions & 29 deletions packages/client/lib/CredentialOfferClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@
CredentialOfferV1_0_11,
CredentialOfferV1_0_13,
determineSpecVersionFromURI,
getClientIdFromCredentialOfferPayload,
OpenId4VCIVersion,
PRE_AUTH_CODE_LITERAL,
PRE_AUTH_GRANT_LITERAL,
toUniformCredentialOfferRequest,
} from '@sphereon/oid4vci-common';
import Debug from 'debug';
toUniformCredentialOfferRequest
} from '@sphereon/oid4vci-common'
import Debug from 'debug'

import { LOG } from './types';
import { LOG } from './types'
import { constructBaseResponse, handleCredentialOfferUri } from './functions'

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

Expand All @@ -43,40 +42,32 @@
credential_offer: credentialOfferPayload,
};
} else {
credentialOffer = convertURIToJsonObject(uri, {
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
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 | CredentialOfferV1_0_13;
if (uri.includes('credential_offer_uri')) {
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13
} else {
credentialOffer = convertURIToJsonObject(uri, {
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
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 | CredentialOfferV1_0_13
}
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri) // cannot be reached since convertURIToJsonObject will check the params

Check warning on line 55 in packages/client/lib/CredentialOfferClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClient.ts#L55

Added line #L55 was not covered by tests
}
}

const request = await toUniformCredentialOfferRequest(credentialOffer, {
...opts,
version,
});
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
const grants = request.credential_offer?.grants;
version
})

return {
scheme,
baseUrl,
...(clientId && { clientId }),
...request,
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
}),
...constructBaseResponse(request, scheme, baseUrl),
userPinRequired:
request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.user_pin_required ??
!!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ??
false,
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
}),
};
false

Check warning on line 69 in packages/client/lib/CredentialOfferClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClient.ts#L69

Added line #L69 was not covered by tests
}
}

public static toURI(
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/CredentialOfferClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
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);
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri); // cannot be reached since convertURIToJsonObject will check the params

Check warning on line 47 in packages/client/lib/CredentialOfferClientV1_0_11.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_11.ts#L47

Added line #L47 was not covered by tests
}
}

Expand Down
65 changes: 29 additions & 36 deletions packages/client/lib/CredentialOfferClientV1_0_13.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,54 @@
import {
convertJsonToURI,
convertURIToJsonObject,
CredentialOffer,
CredentialOfferRequestWithBaseUrl,
CredentialOfferV1_0_13,
determineSpecVersionFromURI,
getClientIdFromCredentialOfferPayload,
OpenId4VCIVersion,
PRE_AUTH_CODE_LITERAL,
PRE_AUTH_GRANT_LITERAL,
toUniformCredentialOfferRequest,
} from '@sphereon/oid4vci-common';
import Debug from 'debug';
toUniformCredentialOfferRequest
} from '@sphereon/oid4vci-common'
import Debug from 'debug'
import { constructBaseResponse, handleCredentialOfferUri } from './functions'

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

export class CredentialOfferClientV1_0_13 {
public static async fromURI(uri: string, opts?: { resolve?: boolean }): Promise<CredentialOfferRequestWithBaseUrl> {
debug(`Credential Offer URI: ${uri}`);
debug(`Credential Offer URI: ${uri}`)

Check warning on line 19 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L19

Added line #L19 was not covered by tests
if (!uri.includes('?') || !uri.includes('://')) {
debug(`Invalid Credential Offer URI: ${uri}`);
throw Error(`Invalid Credential Offer Request`);
debug(`Invalid Credential Offer URI: ${uri}`)
throw Error(`Invalid Credential Offer Request`)

Check warning on line 22 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L21-L22

Added lines #L21 - L22 were not covered by tests
}
const scheme = uri.split('://')[0]
const baseUrl = uri.split('?')[0]
const version = determineSpecVersionFromURI(uri)

Check warning on line 26 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L24-L26

Added lines #L24 - L26 were not covered by tests
let credentialOffer: CredentialOffer
if (uri.includes('credential_offer_uri')) { // FIXME deduplicate
credentialOffer = await handleCredentialOfferUri(uri) as CredentialOfferV1_0_13
} else {
credentialOffer = convertURIToJsonObject(uri, {

Check warning on line 31 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L29-L31

Added lines #L29 - L31 were not covered by tests
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
arrayTypeProperties: uri.includes('credential_offer_uri=')
? ['credential_configuration_ids', 'credential_offer_uri=']
: ['credential_configuration_ids', 'credential_offer='],

Check warning on line 35 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L34-L35

Added lines #L34 - L35 were not covered by tests
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer=']
}) as CredentialOfferV1_0_13
}
const scheme = uri.split('://')[0];
const baseUrl = uri.split('?')[0];
const version = determineSpecVersionFromURI(uri);
const credentialOffer = convertURIToJsonObject(uri, {
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
arrayTypeProperties: uri.includes('credential_offer_uri=')
? ['credential_configuration_ids', 'credential_offer_uri=']
: ['credential_configuration_ids', 'credential_offer='],
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
}) as CredentialOfferV1_0_13;
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri) // cannot be reached since convertURIToJsonObject will check the params

Check warning on line 40 in packages/client/lib/CredentialOfferClientV1_0_13.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/lib/CredentialOfferClientV1_0_13.ts#L40

Added line #L40 was not covered by tests
}

const request = await toUniformCredentialOfferRequest(credentialOffer, {
...opts,
version,
});
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
const grants = request.credential_offer?.grants;
version
})

return {
scheme,
baseUrl,
...(clientId && { clientId }),
...request,
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
...(grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] && {
preAuthorizedCode: grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL],
}),
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false,
...(request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code && {
txCode: request.credential_offer.grants[PRE_AUTH_GRANT_LITERAL].tx_code,
}),
};
...constructBaseResponse(request, scheme, baseUrl),
userPinRequired: !!request.credential_offer?.grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code ?? false
}
}

public static toURI(
Expand Down
59 changes: 59 additions & 0 deletions packages/client/lib/__tests__/CredentialRequestClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,62 @@ describe('Credential Request Client with different issuers ', () => {
});
});
});

describe('Credential Offer Client error handling', () => {
beforeEach(() => {
nock.cleanAll()
})

afterEach(() => {
nock.cleanAll()
})

it('should handle non-200 response from credential offer URI endpoint', async () => {
const IRR_URI = 'openid-credential-offer://?credential_offer_uri=https%3A%2F%2Ftest.example.com%2Foffer'

nock('https://test.example.com')
.get('/offer')
.reply(404, 'Not Found')

await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toMatch(
/the credential offer URI endpoint call was not successful. http code 404 - reason Not Found/
)
})

it('should handle invalid content type from credential offer URI endpoint', async () => {
const IRR_URI = 'openid-credential-offer://?credential_offer_uri=https%3A%2F%2Ftest.example.com%2Foffer'

nock('https://test.example.com')
.get('/offer')
.reply(200, 'plain text response', { 'Content-Type': 'text/plain' })

await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toMatch(
'the credential offer URI endpoint did not return content type application/json'
)
})

it('should handle missing required credential offer properties', async () => {
const IRR_URI = 'openid-credential-offer://?invalid_param=test'

await expect(CredentialOfferClient.fromURI(IRR_URI)).rejects.toThrow('Wrong parameters provided')
})

it('should handle credential offer URI with credential_offer param', async () => {
const IRR_URI = 'openid-credential-offer://?credential_offer=%7B%22test%22%3A%22value%22%7D'

const client = await CredentialOfferClient.fromURI(IRR_URI)
expect(client.credential_offer).toBeDefined()
})

it('should handle URL encoded credential offer URI properly', async () => {
const encodedUri = 'https%3A%2F%2Ftest.example.com%2Foffer'
const IRR_URI = `openid-credential-offer://?credential_offer_uri=${encodedUri}`

nock('https://test.example.com')
.get('/offer')
.reply(200, { test: 'value' }, { 'Content-Type': 'application/json' })

const client = await CredentialOfferClient.fromURI(IRR_URI)
expect(client.credential_offer).toBeDefined()
})
})
26 changes: 25 additions & 1 deletion packages/client/lib/__tests__/MetadataClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { getIssuerFromCredentialOfferPayload, PRE_AUTH_GRANT_LITERAL, WellKnownEndpoints } from '@sphereon/oid4vci-common';
import {
AuthorizationServerMetadata,
getIssuerFromCredentialOfferPayload,
PRE_AUTH_GRANT_LITERAL,
WellKnownEndpoints
} from '@sphereon/oid4vci-common'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import nock from 'nock';
Expand Down Expand Up @@ -299,6 +304,25 @@ describe.skip('Metadataclient with SpruceId should', () => {
});

describe('Metadataclient with Credenco should', () => {
beforeEach(() => {
const mockData = getMockData('credenco')
if (!mockData?.metadata?.openid4vci_metadata) {
throw new Error('Credenco mock data not found or invalid structure')
}
nock('https://mijnkvk.acc.credenco.com')
.get('/.well-known/openid-credential-issuer')
.reply(200, mockData.metadata.openid4vci_metadata)
nock('https://mijnkvk.acc.credenco.com').get('/.well-known/openid-configuration').reply(404)
const authMetadata: AuthorizationServerMetadata = {
authorization_endpoint: 'https://mijnkvk.acc.credenco.com',
"pre-authorized_grant_anonymous_access_supported": true,
issuer: 'https://issuer.research.identiproof.io',
token_endpoint: 'https://mijnkvk.acc.credenco.com/token',
response_types_supported: ['token']
}
nock('https://mijnkvk.acc.credenco.com').get('/.well-known/oauth-authorization-server').reply(200, JSON.stringify(authMetadata));
})

it('succeed without OID4VCI and with OIDC metadata', async () => {
const metadata = await MetadataClient.retrieveAllMetadata('https://mijnkvk.acc.credenco.com/');
expect(metadata.credential_endpoint).toEqual('https://mijnkvk.acc.credenco.com/credential');
Expand Down
12 changes: 6 additions & 6 deletions packages/client/lib/__tests__/OpenID4VCIClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
determineSpecVersionFromOffer,
determineSpecVersionFromURI,
OpenId4VCIVersion,
WellKnownEndpoints,
} from '@sphereon/oid4vci-common';
WellKnownEndpoints
} from '@sphereon/oid4vci-common'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import nock from 'nock';
import nock from 'nock'

import { createCredentialOfferURIFromObject } from '../../../issuer/lib';
import { OpenID4VCIClient } from '../OpenID4VCIClient';
import { createCredentialOfferURIFromObject } from '../../../issuer/lib'
import { OpenID4VCIClient } from '../OpenID4VCIClient'

const MOCK_URL = 'https://server.example.com/';

Expand Down Expand Up @@ -268,7 +268,7 @@ it('determine to be version 13', async () => {
credential_configuration_ids: ['Omzetbelasting'],
credential_issuer: 'https://example.com',
} satisfies CredentialOfferPayloadV1_0_13;
const offerUri = createCredentialOfferURIFromObject({ credential_offer: offer });
const offerUri = createCredentialOfferURIFromObject({ credential_offer: offer }, 'VALUE');

expect(determineSpecVersionFromOffer(offer)).toEqual(OpenId4VCIVersion.VER_1_0_13);
expect(determineSpecVersionFromURI(offerUri)).toEqual(OpenId4VCIVersion.VER_1_0_13);
Expand Down
17 changes: 12 additions & 5 deletions packages/client/lib/__tests__/SdJwt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import {
AccessTokenRequest,
CredentialConfigurationSupportedSdJwtVcV1_0_13,
CredentialConfigurationSupportedV1_0_13,
CredentialSupportedSdJwtVc,
} from '@sphereon/oid4vci-common';
CredentialSupportedSdJwtVc
} from '@sphereon/oid4vci-common'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import nock from 'nock';
import nock from 'nock'

import { OpenID4VCIClientV1_0_13 } from '..';
import { AuthorizationServerMetadataBuilder, createAccessTokenResponse, IssuerMetadataBuilderV1_13, VcIssuerBuilder } from '../../../issuer';
import { OpenID4VCIClientV1_0_13 } from '..'
import {
AuthorizationServerMetadataBuilder,
createAccessTokenResponse,
IssuerMetadataBuilderV1_13,
VcIssuerBuilder
} from '../../../issuer'

export const UNIT_TEST_TIMEOUT = 30000;

Expand Down Expand Up @@ -79,6 +84,7 @@ describe('sd-jwt vc', () => {
'succeed with a full flow',
async () => {
const offerUri = await vcIssuer.createCredentialOfferURI({
offerMode: 'VALUE',
grants: {
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
tx_code: {
Expand Down Expand Up @@ -182,6 +188,7 @@ describe('sd-jwt vc', () => {
'succeed with a full flow without did',
async () => {
const offerUri = await vcIssuer.createCredentialOfferURI({
offerMode: 'VALUE',
grants: {
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
tx_code: {
Expand Down
Loading