@sphereon/oid4vci-client
Version:
OpenID for Verifiable Credential Issuance (OpenID4VCI) client
168 lines (148 loc) • 7.03 kB
text/typescript
import { KeyObject } from 'crypto';
import { Alg, CredentialIssuerMetadataV1_0_13, Jwt, JwtVerifyResult, OpenId4VCIVersion, ProofOfPossession } from '@sphereon/oid4vci-common';
import * as jose from 'jose';
import { CredentialRequestOpts, ProofOfPossessionBuilder } from '..';
import { CredentialRequestClientBuilder } from '../CredentialRequestClientBuilder';
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' },
payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL },
};*/
const jwtv1_0_11: Jwt = {
header: { alg: Alg.ES256, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'openid4vci-proof+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 () => {
const { privateKey, publicKey } = await jose.generateKeyPair('ES256');
keypair = { publicKey: publicKey as KeyObject, privateKey: privateKey as KeyObject };
});
async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
if (!args.payload.aud) {
throw Error('aud required');
} else if (!kid) {
throw Error('kid required');
}
return await new jose.SignJWT({ ...args.payload })
.setProtectedHeader({ alg: 'ES256' })
.setIssuedAt()
.setIssuer(kid)
.setAudience(args.payload.aud)
.setExpirationTime('2h')
.sign(keypair.privateKey);
}
interface KeyPair {
publicKey: KeyObject;
privateKey: KeyObject;
}
async function proofOfPossessionVerifierCallbackFunction(args: { jwt: string; kid?: string }): Promise<JwtVerifyResult> {
const result = await jose.jwtVerify(args.jwt, keypair.publicKey);
const kid = result.protectedHeader.kid ?? args.kid;
const did = kid!.split('#')[0];
const didDocument = {};
const alg = result.protectedHeader.alg;
return {
alg,
did,
kid,
didDocument,
jwt: { header: result.protectedHeader, payload: result.payload },
};
}
describe('Credential Request Client Builder', () => {
it('should build correctly provided with correct params', async function () {
const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI }))
.withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential')
.withFormat('jwt_vc')
.withCredentialIdentifier('credentialType')
.withToken('token')
.build();
expect(credReqClient.credentialRequestOpts.credentialEndpoint).toBe('https://oidc4vci.demo.spruceid.com/credential');
expect(credReqClient.credentialRequestOpts.format).toBe('jwt_vc');
expect((credReqClient.credentialRequestOpts as CredentialRequestOpts).credentialIdentifier).toStrictEqual('credentialType');
expect(credReqClient.credentialRequestOpts.token).toBe('token');
});
it('should build credential request correctly', async () => {
const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI }))
.withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential')
.withFormat('jwt_vc')
.withCredentialIdentifier('OpenBadgeCredential')
.build();
const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({
jwt: jwtv1_0_11,
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
verifyCallback: proofOfPossessionVerifierCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_13,
})
.withClientId('sphereon:wallet')
.withKid(kid)
.build();
await proofOfPossessionVerifierCallbackFunction({ ...proof, kid });
const credentialRequest = await credReqClient.createCredentialRequest({
proofInput: proof,
credentialIdentifier: 'OpenBadgeCredential',
version: OpenId4VCIVersion.VER_1_0_13,
});
expect(credentialRequest.proof?.jwt).toContain(partialJWT);
expect('credential_identifier' in credentialRequest).toBe(true);
if ('types' in credentialRequest) {
expect(credentialRequest.types).toStrictEqual(['https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential']);
}
});
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 = 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({
uri: INITIATION_TEST_URI,
metadata: WALT_OID4VCI_METADATA,
})
)
.withFormat('jwt_vc')
.build();
expect(credReqClient.credentialRequestOpts.credentialEndpoint).toBe(`${WALT_ISSUER_URL}/credential`);
});
it('should build correctly with endpoint from metadata', async () => {
const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI }))
.withFormat('jwt_vc')
.withCredentialEndpointFromMetadata(IDENTIPROOF_OID4VCI_METADATA as unknown as CredentialIssuerMetadataV1_0_13)
.build();
expect(credReqClient.credentialRequestOpts.credentialEndpoint).toBe(`${IDENTIPROOF_ISSUER_URL}/credential`);
});
});