Skip to content

Commit

Permalink
feat: Add VCI Issuer
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed May 28, 2023
1 parent 08be1ed commit 5cab075
Show file tree
Hide file tree
Showing 27 changed files with 330 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CredentialSupported,
IssuerCredentialSubjectDisplay,
Jwt,
OpenId4VCIVersion,
ProofOfPossession,
} from '@sphereon/oid4vci-common'
import { CredentialOfferSession } from '@sphereon/oid4vci-common/dist'
Expand Down Expand Up @@ -200,6 +201,7 @@ xdescribe('issuerCallback', () => {
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
.withClientId(clientId)
.withKid(kid)
Expand All @@ -210,6 +212,7 @@ xdescribe('issuerCallback', () => {
credentialTypes: ['VerifiableCredential'],
format: 'jwt_vc_json',
proofInput: proof,
version: OpenId4VCIVersion.VER_1_0_08,
})
expect(credentialRequest).toEqual({
format: 'jwt_vc_json',
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export class AccessTokenClient {
}

private static creatTokenURLFromURL(url: string, allowInsecureEndpoints?: boolean, tokenEndpoint?: string): string {
if (allowInsecureEndpoints !== true && url.startsWith('http://')) {
if (allowInsecureEndpoints !== true && url.startsWith('http:')) {
throw Error(`Unprotected token endpoints are not allowed ${url}. Adjust settings if you really need this (dev/test settings only!!)`);
}
const hostname = url.replace(/https?:\/\//, '').replace(/\/$/, '');
Expand Down
14 changes: 10 additions & 4 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface CredentialRequestOpts {
format?: CredentialFormat | OID4VCICredentialFormat;
proof: ProofOfPossession;
token: string;
version?: OpenId4VCIVersion;
version: OpenId4VCIVersion;
}

export class CredentialRequestClient {
Expand All @@ -48,7 +48,7 @@ export class CredentialRequestClient {
}): Promise<OpenIDResponse<CredentialResponse>> {
const { credentialTypes, proofInput, format } = opts;

const request = await this.createCredentialRequest({ proofInput, credentialTypes, format });
const request = await this.createCredentialRequest({ proofInput, credentialTypes, format, version: this.version() });
return await this.acquireCredentialsUsingRequest(request);
}

Expand Down Expand Up @@ -77,6 +77,7 @@ export class CredentialRequestClient {
proofInput: ProofOfPossessionBuilder | ProofOfPossession;
credentialTypes?: string | string[];
format?: CredentialFormat | OID4VCICredentialFormat;
version: OpenId4VCIVersion;
}): Promise<UniformCredentialRequest> {
const { proofInput } = opts;
const formatSelection = opts.format ?? this.credentialRequestOpts.format;
Expand All @@ -100,15 +101,20 @@ export class CredentialRequestClient {
}

const proof =
'proof_type' in proofInput ? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession).build() : await proofInput.build();
'proof_type' in proofInput
? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build()
: await proofInput.build();
return {
types,
format,
proof,
} as UniformCredentialRequest;
}

private version(): OpenId4VCIVersion {
return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11;
}
private isV11OrHigher(): boolean {
return !this.credentialRequestOpts.version || this.credentialRequestOpts.version >= OpenId4VCIVersion.VER_1_0_11;
return this.version() >= OpenId4VCIVersion.VER_1_0_11;
}
}
8 changes: 7 additions & 1 deletion packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ export class OpenID4VCIClient {
for (const type of types) {
let typeSupported = false;
for (const credentialSupported of metadata.credentials_supported) {
if (!credentialSupported.types || credentialSupported.types.length === 0) {
throw Error('types is required in the credentials supported');
}
if (credentialSupported.types.indexOf(type) != -1) {
typeSupported = true;
}
Expand All @@ -313,12 +316,15 @@ export class OpenID4VCIClient {
const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({
accessTokenResponse: this.accessTokenResponse,
callbacks: proofCallbacks,
version: this.version(),
})
.withIssuer(this.getIssuer())
.withAlg(this.alg)
.withClientId(this.clientId)
.withKid(this.kid);

if (this._clientId) {
proofBuilder.withClientId(this.clientId);
}
if (jti) {
proofBuilder.withJti(jti);
}
Expand Down
52 changes: 38 additions & 14 deletions packages/client/lib/ProofOfPossessionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
EndpointMetadata,
Jwt,
NO_JWT_PROVIDED,
OpenId4VCIVersion,
PROOF_CANT_BE_CONSTRUCTED,
ProofOfPossession,
ProofOfPossessionCallbacks,
Expand All @@ -16,6 +17,8 @@ export class ProofOfPossessionBuilder {
private readonly proof?: ProofOfPossession;
private readonly callbacks?: ProofOfPossessionCallbacks;

private version: OpenId4VCIVersion;

private kid?: string;
private clientId?: string;
private issuer?: string;
Expand All @@ -30,11 +33,13 @@ export class ProofOfPossessionBuilder {
callbacks,
jwt,
accessTokenResponse,
version,
}: {
proof?: ProofOfPossession;
callbacks?: ProofOfPossessionCallbacks;
accessTokenResponse?: AccessTokenResponse;
jwt?: Jwt;
version: OpenId4VCIVersion;
}) {
this.proof = proof;
this.callbacks = callbacks;
Expand All @@ -44,24 +49,35 @@ export class ProofOfPossessionBuilder {
if (accessTokenResponse) {
this.withAccessTokenResponse(accessTokenResponse);
}
this.version = version;
}

static fromJwt({ jwt, callbacks }: { jwt: Jwt; callbacks: ProofOfPossessionCallbacks }): ProofOfPossessionBuilder {
return new ProofOfPossessionBuilder({ callbacks, jwt });
static fromJwt({
jwt,
callbacks,
version,
}: {
jwt: Jwt;
callbacks: ProofOfPossessionCallbacks;
version: OpenId4VCIVersion;
}): ProofOfPossessionBuilder {
return new ProofOfPossessionBuilder({ callbacks, jwt, version });
}

static fromAccessTokenResponse({
accessTokenResponse,
callbacks,
version,
}: {
accessTokenResponse: AccessTokenResponse;
callbacks: ProofOfPossessionCallbacks;
version: OpenId4VCIVersion;
}): ProofOfPossessionBuilder {
return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse });
return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version });
}

static fromProof(proof: ProofOfPossession): ProofOfPossessionBuilder {
return new ProofOfPossessionBuilder({ proof });
static fromProof(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder {
return new ProofOfPossessionBuilder({ proof, version });
}

withClientId(clientId: string): ProofOfPossessionBuilder {
Expand Down Expand Up @@ -116,15 +132,23 @@ export class ProofOfPossessionBuilder {
throw new Error(NO_JWT_PROVIDED);
}
this.jwt = jwt;
if (jwt.header) {
if (jwt.header.kid) {
this.withKid(jwt.header.kid);
}
if (jwt.header.typ) {
this.withTyp(jwt.header.typ as Typ);
}
this.withAlg(jwt.header.alg);
if (!jwt.header) {
throw Error(`No JWT header present`);
} else if (!jwt.payload) {
throw Error(`No JWT payload present`);
}

if (jwt.header.kid) {
this.withKid(jwt.header.kid);
}
if (jwt.header.typ) {
this.withTyp(jwt.header.typ as Typ);
}
if (this.version >= OpenId4VCIVersion.VER_1_0_11) {
this.withTyp('openid4vci-proof+jwt');
}
this.withAlg(jwt.header.alg);

if (jwt.payload) {
if (jwt.payload.iss) this.withClientId(jwt.payload.iss);
if (jwt.payload.aud) this.withIssuer(jwt.payload.aud);
Expand All @@ -141,7 +165,7 @@ export class ProofOfPossessionBuilder {
return await createProofOfPossession(
this.callbacks,
{
typ: this.typ,
typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'),
kid: this.kid,
jti: this.jti,
alg: this.alg,
Expand Down
17 changes: 15 additions & 2 deletions packages/client/lib/__tests__/CredentialRequestClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EndpointMetadata,
getIssuerFromCredentialOfferPayload,
Jwt,
OpenId4VCIVersion,
ProofOfPossession,
URL_NOT_VALID,
WellKnownEndpoints,
Expand Down Expand Up @@ -82,13 +83,14 @@ describe('Credential Request Client ', () => {
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
// .withEndpointMetadata(metadata)
.withClientId('sphereon:wallet')
.withKid(kid)
.build();
expect(credReqClient.getCredentialEndpoint()).toEqual(basePath + '/credential');
const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof });
const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof, version: OpenId4VCIVersion.VER_1_0_08 });
expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy();
const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest);
expect(result?.errorBody?.error).toBe('unsupported_format');
Expand All @@ -113,12 +115,17 @@ describe('Credential Request Client ', () => {
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
// .withEndpointMetadata(metadata)
.withKid(kid)
.withClientId('sphereon:wallet')
.build();
const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof, format: 'jwt' });
const credentialRequest = await credReqClient.createCredentialRequest({
proofInput: proof,
format: 'jwt',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy();
expect(credentialRequest.format).toEqual('jwt_vc_json_ld');
const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest);
Expand All @@ -136,6 +143,7 @@ describe('Credential Request Client ', () => {
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
// .withEndpointMetadata(metadata)
.withKid(kid)
Expand Down Expand Up @@ -187,6 +195,7 @@ describe('Credential Request Client with different issuers ', () => {
},
credentialTypes: ['OpenBadgeCredential'],
format: 'jwt_vc_json_ld',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialRequest).toEqual(getMockData('spruce')?.credential.request);
});
Expand All @@ -208,6 +217,7 @@ describe('Credential Request Client with different issuers ', () => {
},
credentialTypes: ['OpenBadgeCredential'],
format: 'jwt_vc',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialOffer).toEqual(getMockData('walt')?.credential.request);
});
Expand All @@ -230,6 +240,7 @@ describe('Credential Request Client with different issuers ', () => {
},
credentialTypes: ['OpenBadgeCredential'],
format: 'jwt_vc',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialOffer).toEqual(getMockData('uniissuer')?.credential.request);
});
Expand All @@ -251,6 +262,7 @@ describe('Credential Request Client with different issuers ', () => {
},
credentialTypes: ['OpenBadgeCredential'],
format: 'ldp_vc',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialOffer).toEqual(getMockData('mattr')?.credential.request);
});
Expand All @@ -272,6 +284,7 @@ describe('Credential Request Client with different issuers ', () => {
},
credentialTypes: ['OpenBadgeCredential'],
format: 'ldp_vc',
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialOffer).toEqual(getMockData('diwala')?.credential.request);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { KeyObject } from 'crypto';

import { Alg, CredentialIssuerMetadata, Jwt, JWTPayload, ProofOfPossession, UniformCredentialRequest } from '@sphereon/oid4vci-common';
import {
Alg,
CredentialIssuerMetadata,
Jwt,
JWTPayload,
OpenId4VCIVersion,
ProofOfPossession,
UniformCredentialRequest,
} from '@sphereon/oid4vci-common';
import * as jose from 'jose';

import { CredentialRequestClientBuilder, ProofOfPossessionBuilder } from '..';
Expand Down Expand Up @@ -74,12 +82,16 @@ describe('Credential Request Client Builder', () => {
signCallback: proofOfPossessionCallbackFunction,
verifyCallback: proofOfPossessionVerifierCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_08,
})
.withClientId('sphereon:wallet')
.withKid(kid)
.build();
await proofOfPossessionVerifierCallbackFunction({ ...proof, kid });
const credentialRequest: UniformCredentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof });
const credentialRequest: UniformCredentialRequest = await credReqClient.createCredentialRequest({
proofInput: proof,
version: OpenId4VCIVersion.VER_1_0_08,
});
expect(credentialRequest.proof?.jwt).toContain(partialJWT);
expect('types' in credentialRequest).toBe(true);
if ('types' in credentialRequest) {
Expand Down
11 changes: 10 additions & 1 deletion packages/client/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { AccessTokenResponse, Alg, AuthzFlowType, CredentialOfferRequestWithBaseUrl, Jwt, ProofOfPossession } from '@sphereon/oid4vci-common';
import {
AccessTokenResponse,
Alg,
AuthzFlowType,
CredentialOfferRequestWithBaseUrl,
Jwt,
OpenId4VCIVersion,
ProofOfPossession,
} from '@sphereon/oid4vci-common';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import nock from 'nock';
Expand Down Expand Up @@ -148,6 +156,7 @@ describe('OID4VCI-Client should', () => {
callbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
version: OpenId4VCIVersion.VER_1_0_11,
})
.withEndpointMetadata({
issuer: 'https://issuer.research.identiproof.io',
Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/__tests__/IssuanceInitiation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ describe('Issuance Initiation', () => {
});

it('Should return Issuance Initiation URI from request', async () => {
expect(await CredentialOfferClient.toURI(INITIATION_TEST)).toEqual(INITIATION_TEST_URI);
expect(CredentialOfferClient.toURI(INITIATION_TEST)).toEqual(INITIATION_TEST_URI);
});

it('Should return URI from Issuance Initiation Request', async () => {
const issuanceInitiationClient = await CredentialOfferClient.fromURI(INITIATION_TEST_HTTPS_URI);
expect(await CredentialOfferClient.toURI(issuanceInitiationClient)).toEqual(INITIATION_TEST_HTTPS_URI);
expect(CredentialOfferClient.toURI(issuanceInitiationClient)).toEqual(INITIATION_TEST_HTTPS_URI);
});

it('Should throw error on invalid URI', async () => {
Expand Down
Loading

0 comments on commit 5cab075

Please sign in to comment.