Skip to content

Commit

Permalink
feat: added x5c support and made sure that we support request-respons…
Browse files Browse the repository at this point in the history
…es without dids
  • Loading branch information
sksadjad committed Jun 20, 2024
1 parent 53e027b commit 27bc1d9
Show file tree
Hide file tree
Showing 9 changed files with 792 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CredentialRequestClientBuilder, ProofOfPossessionBuilder } from '..';
import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST_URI, WALT_ISSUER_URL, WALT_OID4VCI_METADATA } from './MetadataMocks';

const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN';
const partialJWT_withoutDid = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlYmZlYjFmNzEyZWJjNmYxYzI3N';

/*const jwtv1_0_08: Jwt = {
header: { alg: Alg.ES256, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'jwt' },
Expand All @@ -27,8 +28,15 @@ const jwtv1_0_11: Jwt = {
payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL },
};

const jwtv1_0_13_withoutDid: Jwt = {
header: { alg: Alg.ES256, kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'openid4vci-proof+jwt' },
payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL },
};

const kid = 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1';

const kid_withoutDid = 'ebfeb1f712ebc6f1c276e12ec21/keys/1';

let keypair: KeyPair;

beforeAll(async () => {
Expand Down Expand Up @@ -115,6 +123,35 @@ describe('Credential Request Client Builder', () => {
}
});

it('should build credential request correctly without did', async () => {
const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI }))
.withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential')
.withFormat('jwt_vc')
.withCredentialType('OpenBadgeCredential')
.build();
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt: jwtv1_0_13_withoutDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
verifyCallback: proofOfPossessionVerifierCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_13,
})
.withClientId('sphereon:wallet')
.withKid(kid_withoutDid)
.build();
await proofOfPossessionVerifierCallbackFunction({ ...proof, kid: kid_withoutDid });
const credentialRequest: CredentialRequestV1_0_13 = await credReqClient.createCredentialRequest({
proofInput: proof,
credentialTypes: 'OpenBadgeCredential',
version: OpenId4VCIVersion.VER_1_0_13,
});
expect(credentialRequest.proof?.jwt).toContain(partialJWT_withoutDid);
if ('types' in credentialRequest) {
expect(credentialRequest.types).toStrictEqual(['OpenBadgeCredential']);
}
});

it('should build correctly from metadata', async () => {
const credReqClient = (
await CredentialRequestClientBuilder.fromURI({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,22 @@ import {
import { getMockData } from './data/VciDataFixtures';

const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN';
const partialJWT_withoutDid = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlYmZlYjFmNzEyZWJjNmYxYzI3N';

const jwt: Jwt = {
header: { alg: Alg.ES256, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'jwt' },
payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL },
};

const jwt_withoutDid: Jwt = {
header: { alg: Alg.ES256, kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'jwt' },
payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL },
};

const kid = 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1';

const kid_withoutDid = 'ebfeb1f712ebc6f1c276e12ec21/keys/1';

let keypair: KeyPair;

async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
Expand Down Expand Up @@ -102,6 +110,36 @@ describe('Credential Request Client ', () => {
expect(result?.errorBody?.error).toBe('unsupported_format');
});

it('should get a failed credential response with an unsupported format and without did', async function () {
const basePath = 'https://sphereonjunit2022101301.com/';
nock(basePath).post(/.*/).reply(500, {
error: 'unsupported_format',
error_description: 'This is a mock error message',
});

const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: INITIATION_TEST_V1_0_08 })
.withCredentialEndpoint(basePath + '/credential')
.withFormat('ldp_vc')
.withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
.build();
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt: jwt_withoutDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
// .withEndpointMetadata(metadata)
.withClientId('sphereon:wallet')
.withKid(kid_withoutDid)
.build();
expect(credReqClient.getCredentialEndpoint()).toEqual(basePath + '/credential');
const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof, version: OpenId4VCIVersion.VER_1_0_08 });
expect(credentialRequest.proof?.jwt?.includes(partialJWT_withoutDid)).toBeTruthy();
const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest);
expect(result?.errorBody?.error).toBe('unsupported_format');
});

it('should get success credential response', async function () {
const mockedVC =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA';
Expand Down Expand Up @@ -138,6 +176,42 @@ describe('Credential Request Client ', () => {
expect(result?.successBody?.credential).toEqual(mockedVC);
});

it('should get success credential response without did', async function () {
const mockedVC =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJlYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImlhdCI6MTcxODM1NzcxOH0.7iiOTuIjQRyrIincYyDW6m0nBYmDoYfXcTYFrywsKEY';
nock('https://oidc4vci.demo.spruceid.com')
.post(/credential/)
.reply(200, {
format: 'jwt-vc',
credential: mockedVC,
});
const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest({ request: INITIATION_TEST })
.withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential')
.withFormat('jwt_vc')
.withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
.build();
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt: jwt_withoutDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
// .withEndpointMetadata(metadata)
.withKid(kid_withoutDid)
.withClientId('sphereon:wallet')
.build();
const credentialRequest = await credReqClient.createCredentialRequest({
proofInput: proof,
format: 'jwt',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialRequest.proof?.jwt?.includes(partialJWT_withoutDid)).toBeTruthy();
expect(credentialRequest.format).toEqual('jwt_vc');
const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest);
expect(result?.successBody?.credential).toEqual(mockedVC);
});

it('should fail with invalid url', async () => {
const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest({ request: INITIATION_TEST })
.withCredentialEndpoint('httpsf://oidc4vci.demo.spruceid.com/credential')
Expand All @@ -159,6 +233,28 @@ describe('Credential Request Client ', () => {
Error(URL_NOT_VALID),
);
});

it('should fail with invalid url without did', async () => {
const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest({ request: INITIATION_TEST })
.withCredentialEndpoint('httpsf://oidc4vci.demo.spruceid.com/credential')
.withFormat('jwt_vc')
.withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential')
.build();
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt: jwt_withoutDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
// .withEndpointMetadata(metadata)
.withKid(kid_withoutDid)
.withClientId('sphereon:wallet')
.build();
await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json', types: ['random'], proof })).rejects.toThrow(
Error(URL_NOT_VALID),
);
});
});

describe('Credential Request Client with Walt.id ', () => {
Expand Down
138 changes: 135 additions & 3 deletions packages/client/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ import { IDENTIPROOF_AS_METADATA, IDENTIPROOF_AS_URL, IDENTIPROOF_ISSUER_URL, ID
export const UNIT_TEST_TIMEOUT = 30000;

const ISSUER_URL = 'https://issuer.research.identiproof.io';
const jwt = {
const jwtDid = {
header: { alg: Alg.ES256, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'openid4vci-proof+jwt' },
payload: { iss: 'test-clientId', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: ISSUER_URL },
};

const jwtWithoutDid = {
header: { alg: Alg.ES256, kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'openid4vci-proof+jwt' },
payload: { iss: 'test-clientId', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: ISSUER_URL },
};

describe('OID4VCI-Client should', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function proofOfPossessionCallbackFunction(_args: Jwt, _kid?: string): Promise<string> {
Expand Down Expand Up @@ -202,7 +207,7 @@ describe('OID4VCI-Client should', () => {
// Types of parameters 'args' and 'args' are incompatible.
// Property 'kid' is missing in type '{ header: unknown; payload: unknown; }' but required in type 'ProofOfPossessionCallbackArgs'.
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt,
jwt: jwtDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
Expand All @@ -221,6 +226,66 @@ describe('OID4VCI-Client should', () => {
UNIT_TEST_TIMEOUT,
);

it(
'succeed with a full flow with a not-did-kid without the client v1_0_11',
async () => {
/* Convert the URI into an object */
const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClientV1_0_11.fromURI(INITIATE_QR_V1_0_08);

expect(credentialOffer.baseUrl).toEqual('openid-initiate-issuance://');
expect(credentialOffer.original_credential_offer).toEqual({
credential_type: ['OpenBadgeCredentialUrl'],
issuer: ISSUER_URL,
'pre-authorized_code':
'4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp',
user_pin_required: 'true',
});

nock(ISSUER_URL)
.post(/token.*/)
.reply(200, JSON.stringify(mockedAccessTokenResponse));

/* The actual access token calls */
const accessTokenClient: AccessTokenClientV1_0_11 = new AccessTokenClientV1_0_11();
const accessTokenResponse = await accessTokenClient.acquireAccessToken({ credentialOffer: credentialOffer, pin: '1234' });
expect(accessTokenResponse.successBody).toEqual(mockedAccessTokenResponse);
// Get the credential
nock(ISSUER_URL)
.post(/credential/)
.reply(200, {
format: 'jwt-vc',
credential: mockedVC,
});
const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: credentialOffer })
.withFormat('jwt_vc')

.withTokenFromResponse(accessTokenResponse.successBody!)
.build();

//TS2322: Type '(args: ProofOfPossessionCallbackArgs) => Promise<string>'
// is not assignable to type 'ProofOfPossessionCallback'.
// Types of parameters 'args' and 'args' are incompatible.
// Property 'kid' is missing in type '{ header: unknown; payload: unknown; }' but required in type 'ProofOfPossessionCallbackArgs'.
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt: jwtWithoutDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_11,
})
.withEndpointMetadata({
issuer: 'https://issuer.research.identiproof.io',
credential_endpoint: 'https://issuer.research.identiproof.io/credential',
token_endpoint: 'https://issuer.research.identiproof.io/token',
})
.withKid('ebfeb1f712ebc6f1c276e12ec21/keys/1')
.build();
const credResponse = await credReqClient.acquireCredentialsUsingProof({ proofInput: proof });
expect(credResponse.successBody?.credential).toEqual(mockedVC);
},
UNIT_TEST_TIMEOUT,
);

it(
'succeed with a full flow without the client v1_0_13',
async () => {
Expand Down Expand Up @@ -266,7 +331,7 @@ describe('OID4VCI-Client should', () => {
// Types of parameters 'args' and 'args' are incompatible.
// Property 'kid' is missing in type '{ header: unknown; payload: unknown; }' but required in type 'ProofOfPossessionCallbackArgs'.
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt,
jwt: jwtDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
Expand All @@ -287,6 +352,73 @@ describe('OID4VCI-Client should', () => {
},
UNIT_TEST_TIMEOUT,
);

it(
'succeed with a full flow with a not-did-kid without the client v1_0_13',
async () => {
/* Convert the URI into an object */
const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR_V1_0_13);
const preAuthorizedCode = 'oaKazRN8I0IbtZ0C7JuMn5';
expect(credentialOffer.baseUrl).toEqual('openid-credential-offer://');
expect((credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13).credential_configuration_ids).toEqual(['OpenBadgeCredentialUrl']);
expect(credentialOffer.original_credential_offer.grants).toEqual({
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
'pre-authorized_code': preAuthorizedCode,
tx_code: {
input_mode: 'text',
description: 'Please enter the serial number of your physical drivers license',
length: preAuthorizedCode.length,
},
},
});

nock(ISSUER_URL)
.post(/token.*/)
.reply(200, JSON.stringify(mockedAccessTokenResponse));

/* The actual access token calls */
const accessTokenClient: AccessTokenClient = new AccessTokenClient();
const accessTokenResponse = await accessTokenClient.acquireAccessToken({ credentialOffer: credentialOffer, pin: '1234' });
expect(accessTokenResponse.successBody).toEqual(mockedAccessTokenResponse);
// Get the credential
nock(ISSUER_URL)
.post(/credential/)
.reply(200, {
format: 'jwt-vc',
credential: mockedVC,
});
const credReqClient = CredentialRequestClientBuilder.fromCredentialOffer({ credentialOffer: credentialOffer })
.withFormat('jwt_vc')

.withTokenFromResponse(accessTokenResponse.successBody!)
.build();

//TS2322: Type '(args: ProofOfPossessionCallbackArgs) => Promise<string>'
// is not assignable to type 'ProofOfPossessionCallback'.
// Types of parameters 'args' and 'args' are incompatible.
// Property 'kid' is missing in type '{ header: unknown; payload: unknown; }' but required in type 'ProofOfPossessionCallbackArgs'.
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt: jwtWithoutDid,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_11,
})
.withEndpointMetadata({
issuer: 'https://issuer.research.identiproof.io',
credential_endpoint: 'https://issuer.research.identiproof.io/credential',
token_endpoint: 'https://issuer.research.identiproof.io/token',
})
.withKid('ebfeb1f712ebc6f1c276e12ec21/keys/1')
.build();
const credResponse = await credReqClient.acquireCredentialsUsingProof({
proofInput: proof,
credentialTypes: credentialOffer.original_credential_offer.credential_configuration_ids,
});
expect(credResponse.successBody?.credential).toEqual(mockedVC);
},
UNIT_TEST_TIMEOUT,
);
});

describe('OIDVCI-Client for v1_0_13 should', () => {
Expand Down
Loading

0 comments on commit 27bc1d9

Please sign in to comment.