@sphereon/oid4vci-client
Version:
OpenID for Verifiable Credential Issuance (OpenID4VCI) client
173 lines (151 loc) • 5.96 kB
text/typescript
import * as crypto from 'crypto';
import { uuidv4 } from '@sphereon/oid4vc-common';
import { Alg, Jwt, ProofOfPossessionCallbacks } from '@sphereon/oid4vci-common';
import { CredentialMapper } from '@sphereon/ssi-types';
import * as didts from '@transmute/did-key.js';
import { fetch } from 'cross-fetch';
import debug from 'debug';
import { importJWK, JWK, SignJWT } from 'jose';
import { OpenID4VCIClientV1_0_11 } from '..';
export const UNIT_TEST_TIMEOUT = 60000;
const ISSUER_URL = 'https://ssi.sphereon.com/pf3';
const jwk: JWK = {
crv: 'Ed25519',
d: 'kTRm0aONHYwNPA-w_DtjMHUIWjE3K70qgCIhWojZ0eU',
x: 'NeA0d8sp86xRh3DczU4m5wPNIbl0HCSwOBcMN3sNmdk',
kty: 'OKP',
};
// pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9
// priv hex: 913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5
const did = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
const kid = `${did}#z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
// Sphereon infra down rn
describe.skip('OID4VCI-Client using Sphereon issuer should', () => {
async function test(format: 'ldp_vc' | 'jwt_vc_json') {
debug.enable('*');
const offer = await getCredentialOffer(format);
const client = await OpenID4VCIClientV1_0_11.fromURI({
uri: offer.uri,
kid,
alg: Alg.EdDSA,
});
expect(client.credentialOffer).toBeDefined();
expect(client.endpointMetadata).toBeDefined();
expect(client.getCredentialEndpoint()).toEqual(`${ISSUER_URL}/credentials`);
expect(client.getAccessTokenEndpoint()).toEqual(`${ISSUER_URL}/token`);
const accessToken = await client.acquireAccessToken();
// console.log(accessToken);
expect(accessToken).toMatchObject({
expires_in: 300,
// scope: 'GuestCredential',
token_type: 'bearer',
});
const credentialResponse = await client.acquireCredentials({
credentialTypes: 'GuestCredential',
format,
proofCallbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
});
expect(credentialResponse.credential).toBeDefined();
const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credentialResponse.credential!);
expect(format.startsWith(wrappedVC.format)).toEqual(true);
}
it(
'succeed in a full flow with the client using OpenID4VCI version 11 and ldp_vc',
async () => {
await test('ldp_vc');
},
UNIT_TEST_TIMEOUT,
);
it(
'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
async () => {
await test('jwt_vc_json');
},
UNIT_TEST_TIMEOUT,
);
});
interface CreateCredentialOfferResponse {
uri: string;
userPinRequired: boolean;
}
async function getCredentialOffer(format: 'ldp_vc' | 'jwt_vc_json'): Promise<CreateCredentialOfferResponse> {
const credentialOffer = await fetch('https://ssi.sphereon.com/pf3/webapp/credential-offers', {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
//make sure to serialize your JSON body
body: JSON.stringify({
credentials: ['GuestCredential'],
grants: {
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
'pre-authorized_code': uuidv4().substring(0, 10),
user_pin_required: false,
},
},
credentialDataSupplierInput: { firstName: 'Hello', lastName: 'World', email: 'hello.world@example.com' },
}),
});
return (await credentialOffer.json()) as CreateCredentialOfferResponse;
}
async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
const importedJwk = await importJWK(jwk, 'EdDSA');
return await new SignJWT({ ...args.payload })
.setProtectedHeader({ ...args.header, kid: kid! })
.setIssuer(kid!)
.setIssuedAt()
.setExpirationTime('2h')
.sign(importedJwk);
}
//fixme: bring back this test
describe('ismapolis bug report #63, https://github.com/Sphereon-Opensource/OID4VC-demo/issues/63, should', () => {
// Sphereon infra is not working currently
it.skip('work as expected provided a correct JWT is supplied', async () => {
debug.enable('*');
const { uri } = await getCredentialOffer('jwt_vc_json');
const client = await OpenID4VCIClientV1_0_11.fromURI({ uri: uri, clientId: 'test-clientID' });
const metadata = await client.retrieveServerMetadata();
console.log(JSON.stringify(metadata));
//2. Adquire acces token from authorization server endpoint
const accessToken = await client.acquireAccessToken({});
console.log(`Access token: ${JSON.stringify(accessToken)}`);
//3. Create DID needed for later proof of possession
const { keys, didDocument } = await didts.jwk.generate({
type: 'secp256k1', // 'P-256', 'P-384', 'X25519', 'secp256k1'
accept: 'application/did+json',
secureRandom: () => {
return crypto.randomBytes(32);
},
});
const edPrivateKey = await importJWK(keys[0].privateKeyJwk);
async function signCallback(args: Jwt, kid?: string): Promise<string> {
if (!args.payload.aud) {
throw Error('aud required');
} else if (!kid) {
throw Error('kid required');
}
return await new SignJWT({ ...args.payload })
.setProtectedHeader({ alg: args.header.alg, kid, typ: 'openid4vci-proof+jwt' })
.setIssuedAt()
.setIssuer(kid)
.setAudience(args.payload.aud)
.setExpirationTime('2h')
.sign(edPrivateKey);
}
const callbacks: ProofOfPossessionCallbacks = {
signCallback: signCallback,
};
const credentialResponse = await client.acquireCredentials({
credentialTypes: 'GuestCredential',
proofCallbacks: callbacks,
format: 'jwt_vc_json',
alg: Alg.ES256K,
kid: didDocument.verificationMethod[0].id,
jti: uuidv4(),
});
console.log(JSON.stringify(credentialResponse.credential));
});
});