Skip to content

Commit

Permalink
Initial commit of getting OpenID4VCIClient without URI
Browse files Browse the repository at this point in the history
  • Loading branch information
sanderPostma committed Dec 22, 2023
1 parent 1595df2 commit 14e1dee
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 42 deletions.
64 changes: 49 additions & 15 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
PushedAuthorizationResponse,
ResponseType,
} from '@sphereon/oid4vci-common';
import { getSupportedCredentials, getTypesFromCredentialSupported } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
import {
getSupportedCredentials,
getTypesFromCredentialSupported
} from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types';
import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';
Expand Down Expand Up @@ -46,36 +49,60 @@ interface AuthRequestOpts {
}

export class OpenID4VCIClient {
private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl;
private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl | undefined;
private _clientId?: string;
private _kid: string | undefined;
private _alg: Alg | string | undefined;
private _endpointMetadata: EndpointMetadataResult | undefined;
private _accessTokenResponse: AccessTokenResponse | undefined;
private _issuer: string | undefined;

private constructor(credentialOffer: CredentialOfferRequestWithBaseUrl, kid?: string, alg?: Alg | string, clientId?: string) {
private constructor(credentialOffer?: CredentialOfferRequestWithBaseUrl, issuer?: string, kid?: string, alg?: Alg | string, clientId?: string) {
this._credentialOffer = credentialOffer;
this._issuer = issuer;
this._kid = kid;
this._alg = alg;
this._clientId = clientId;
}


public static async fromURI({
uri,
kid,
alg,
retrieveServerMetadata,
clientId,
resolveOfferUri,
}: {
uri,
kid,
alg,
retrieveServerMetadata,
clientId,
resolveOfferUri
}: {
uri: string;
kid?: string;
alg?: Alg | string;
retrieveServerMetadata?: boolean;
resolveOfferUri?: boolean;
clientId?: string;
}): Promise<OpenID4VCIClient> {
const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), kid, alg, clientId);
const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), undefined, kid, alg, clientId);

if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
await client.retrieveServerMetadata();
}
return client;
}

public static async fromIssuer({
issuer,
kid,
alg,
retrieveServerMetadata,
clientId
}: {
issuer: string;
kid?: string;
alg?: Alg | string;
retrieveServerMetadata?: boolean;
clientId?: string;
}): Promise<OpenID4VCIClient> {
const client = new OpenID4VCIClient(undefined, issuer, kid, alg, clientId);

if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
await client.retrieveServerMetadata();
Expand All @@ -86,7 +113,11 @@ export class OpenID4VCIClient {
public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
this.assertIssuerData();
if (!this._endpointMetadata) {
this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
if (this._issuer) {
this._endpointMetadata = await MetadataClient.retrieveAllMetadata(this._issuer);
} else {
this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
}
}
return this.endpointMetadata;
}
Expand Down Expand Up @@ -376,13 +407,13 @@ export class OpenID4VCIClient {
// Then match the object array on server metadata
getCredentialsSupported(
restrictToInitiationTypes: boolean,
format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[],
format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[]
): CredentialSupported[] {
return getSupportedCredentials({
issuerMetadata: this.endpointMetadata.credentialIssuerMetadata,
version: this.version(),
format: format,
types: restrictToInitiationTypes ? this.getCredentialOfferTypes() : undefined,
types: restrictToInitiationTypes ? this.getCredentialOfferTypes() : undefined
});
}

Expand Down Expand Up @@ -413,6 +444,9 @@ export class OpenID4VCIClient {
}

get credentialOffer(): CredentialOfferRequestWithBaseUrl {
if (!this._credentialOffer) {
throw new Error('no active credential offer available');
}
return this._credentialOffer;
}

Expand Down Expand Up @@ -473,7 +507,7 @@ export class OpenID4VCIClient {
}

private assertIssuerData(): void {
if (!this._credentialOffer) {
if (!this._credentialOffer && !this._issuer) {
throw Error(`No issuance initiation or credential offer present`);
}
}
Expand Down
112 changes: 85 additions & 27 deletions packages/client/lib/__tests__/OpenID4VCIClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-common';
import { CodeChallengeMethod, EndpointMetadataResult, Jwt, WellKnownEndpoints } from '@sphereon/oid4vci-common'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { SignJWT } from 'jose'
import nock from 'nock';

import { OpenID4VCIClient } from '../OpenID4VCIClient';


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

describe('OpenID4VCIClient should', () => {
Expand Down Expand Up @@ -95,25 +97,25 @@ describe('OpenID4VCIClient should', () => {
format: 'ldp_vc',
credential_definition: {
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
types: ['VerifiableCredential', 'UniversityDegreeCredential'],
},
types: ['VerifiableCredential', 'UniversityDegreeCredential']
}
},
{
type: 'openid_credential',
format: 'mso_mdoc',
doctype: 'org.iso.18013.5.1.mDL',
},
doctype: 'org.iso.18013.5.1.mDL'
}
],
redirectUri: 'http://localhost:8881/cb',
}),
redirectUri: 'http://localhost:8881/cb'
})
).toEqual(
'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client',
);
});
'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client'
)
})
it('create an authorization request url with authorization_details object property', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
client._endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;
client._endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`

expect(
client.createAuthorizationRequestUrl({
Expand All @@ -124,19 +126,19 @@ describe('OpenID4VCIClient should', () => {
format: 'ldp_vc',
credential_definition: {
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
types: ['VerifiableCredential', 'UniversityDegreeCredential'],
},
types: ['VerifiableCredential', 'UniversityDegreeCredential']
}
},
redirectUri: 'http://localhost:8881/cb',
}),
redirectUri: 'http://localhost:8881/cb'
})
).toEqual(
'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client',
);
});
'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client'
)
})
it('create an authorization request url with authorization_details and scope', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
client._endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;
client._endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`

expect(
client.createAuthorizationRequestUrl({
Expand All @@ -148,14 +150,70 @@ describe('OpenID4VCIClient should', () => {
locations: ['https://test.com'],
credential_definition: {
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
types: ['VerifiableCredential', 'UniversityDegreeCredential'],
},
types: ['VerifiableCredential', 'UniversityDegreeCredential']
}
},
scope: 'openid',
redirectUri: 'http://localhost:8881/cb',
}),
redirectUri: 'http://localhost:8881/cb'
})
).toEqual(
'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%2C%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client',
);
});
});
'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%2C%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client'
)
})
})


// Research code below
const ESIGNET_ISSUER_URL = 'https://esignet.collab.mosip.net'
describe('OpenID4VCIClient with authorizaion_code flow should', () => {
let client: OpenID4VCIClient
beforeEach(async () => {

const origResponse = await fetch('https://esignet.collab.mosip.net/.well-known/openid-credential-issuer')
const responseText = await origResponse.text()
console.log(responseText)

client = await OpenID4VCIClient.fromIssuer({
issuer: ESIGNET_ISSUER_URL,
clientId: 'MUq1H5M4OBr9fxSC2fJrY4felRmxtDw4iRls2lBZQzI'
})
})

let metaData: EndpointMetadataResult
it('retrieve server metadata', async () => {
metaData = await client.retrieveServerMetadata()
expect(metaData.token_endpoint).toBeDefined()
expect(metaData.issuer).toBeDefined()
expect(metaData.credential_endpoint).toBeDefined()
expect(metaData.credentialIssuerMetadata).toBeDefined()
expect(metaData.credentialIssuerMetadata.credentials_supported).toBeDefined()
expect(metaData.credentialIssuerMetadata.credentials_supported).toHaveLength(1)
console.log(metaData.credentialIssuerMetadata)
})


async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
return await new SignJWT({ ...args.payload })
.setProtectedHeader({ ...args.header, kid: kid! })
.setIssuer(kid!)
.setIssuedAt()
.setExpirationTime('2h')
.sign(importedJwk)
}

it('get credential', async () => {

const credentialsSupported = metaData.credentialIssuerMetadata.credentials_supported
const credentials = await client.acquireCredentials({
credentialTypes: credentialsSupported[0],
format: 'ldp_vc',
proofCallbacks: {
signCallback: proofOfPossessionCallbackFunction
}
})

expect(credentials).toBeDefined()
expect(credentials.credential).toBeDefined()
})

})
1 change: 1 addition & 0 deletions packages/common/lib/types/Authorization.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export enum Encoding {

export enum ResponseType {
AUTH_CODE = 'code',
ID_TOKEN = 'id_token'
}

export enum CodeChallengeMethod {
Expand Down

0 comments on commit 14e1dee

Please sign in to comment.