Skip to content

Commit

Permalink
fix: Ensure we have a single client that handles both v13 and v11 and…
Browse files Browse the repository at this point in the history
… lower
  • Loading branch information
nklomp committed Jun 14, 2024
1 parent ccbcaa7 commit eadbba0
Show file tree
Hide file tree
Showing 22 changed files with 1,330 additions and 246 deletions.
2 changes: 1 addition & 1 deletion packages/client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Release with support for the pre-authorized code flow only!
- Documentation updates/fixes

- Fixes:
- The acquireCredential in the OpenID4VCIClient was not using the access token, resulting in auth issues.
- The acquireCredential in the OpenID4VCIClientV1_0_13 was not using the access token, resulting in auth issues.

## v0.3.1 - 2022-11-20

Expand Down
14 changes: 7 additions & 7 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ This initiates the client using a URI obtained from the Issuer using a link (URL
already fetching the Server Metadata

```typescript
import { OpenID4VCIClient } from '@sphereon/oid4vci-client';
import { OpenID4VCIClientV1_0_13 } from '@sphereon/oid4vci-client';

// The client is initiated from a URI. This URI is provided by the Issuer, typically as a URL or QR code.
const client = await OpenID4VCIClient.fromURI({
const client = await OpenID4VCIClientV1_0_13.fromURI({
uri: 'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true',
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21#key-1', // Our DID. You can defer this also to when the acquireCredential method is called
alg: Alg.ES256, // The signing Algorithm we will use. You can defer this also to when the acquireCredential method is called
Expand All @@ -71,10 +71,10 @@ console.log(client.getAccessTokenEndpoint()); // https://auth.research.identipro
Using https scheme

```typescript
import { OpenID4VCIClient } from '@sphereon/oid4vci-client';
import { OpenID4VCIClientV1_0_13 } from '@sphereon/oid4vci-client';

// The client is initiated from a URI. This URI is provided by the Issuer, typically as a URL or QR code.
const client = await OpenID4VCIClient.fromURI({
const client = await OpenID4VCIClientV1_0_13.fromURI({
uri: 'https://launchpad.vii.electron.mattrlabs.io?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21#key-1', // Our DID. You can defer this also to when the acquireCredential method is called
alg: Alg.ES256, // The signing Algorithm we will use. You can defer this also to when the acquireCredential method is called
Expand Down Expand Up @@ -206,15 +206,15 @@ The OpenID4VCI spec defines a server metadata object that contains information a
support. Next to this predefined endpoint there are also the well-known locations for OpenID Connect Discovery
configuration and
Oauth2 Authorization Server configuration. These contain for instance the token endpoints.
The MetadataClient checks the OpenID4VCI well-known location for the medata and existence of a token endpoint. If the
The MetadataClientV1_0_13 checks the OpenID4VCI well-known location for the medata and existence of a token endpoint. If the
OpenID4VCI well-known location is not found, the OIDC/OAuth2 well-known locations will be tried:

Example:

```typescript
import { MetadataClient } from '@sphereon/oid4vci-client';
import { MetadataClientV1_0_13 } from '@sphereon/oid4vci-client';

const metadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(initiationRequestWithUrl);
const metadata = await MetadataClientV1_0_13.retrieveAllMetadataFromCredentialOffer(initiationRequestWithUrl);

console.log(metadata);
/**
Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from '@sphereon/oid4vci-common';
import { ObjectUtils } from '@sphereon/ssi-types';

import { MetadataClient } from './MetadataClient';
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
import { LOG } from './types';

export class AccessTokenClient {
Expand Down Expand Up @@ -82,7 +82,7 @@ export class AccessTokenClient {
metadata: metadata
? metadata
: issuerOpts?.fetchMetadata
? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })
? await MetadataClientV1_0_13.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })
: undefined,
});

Expand Down
12 changes: 3 additions & 9 deletions packages/client/lib/AccessTokenClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,24 @@ import {
AuthorizationServerOpts,
AuthzFlowType,
convertJsonToURI,
CredentialOfferPayloadV1_0_11,
CredentialOfferV1_0_11,
CredentialOfferV1_0_13,
determineSpecVersionFromOffer,
EndpointMetadata,
formPost,
getIssuerFromCredentialOfferPayload,
GrantTypes,
IssuerOpts,
JsonURIMode,
OpenId4VCIVersion,
OpenIDResponse,
PRE_AUTH_CODE_LITERAL,
TokenErrorResponse,
toUniformCredentialOfferRequest,
toUniformCredentialOfferRequestV1_0_11,
UniformCredentialOfferPayload,
} from '@sphereon/oid4vci-common';
import { ObjectUtils } from '@sphereon/ssi-types';
import Debug from 'debug';

import { MetadataClient } from './MetadataClient';
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';

const debug = Debug('sphereon:oid4vci:token');

Expand Down Expand Up @@ -84,7 +80,7 @@ export class AccessTokenClientV1_0_11 {
metadata: metadata
? metadata
: issuerOpts?.fetchMetadata
? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })
? await MetadataClientV1_0_13.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })
: undefined,
});

Expand All @@ -94,9 +90,7 @@ export class AccessTokenClientV1_0_11 {
public async createAccessTokenRequest(opts: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
const credentialOfferRequest = opts.credentialOffer
? determineSpecVersionFromOffer(opts.credentialOffer as CredentialOfferPayloadV1_0_11).valueOf() <= OpenId4VCIVersion.VER_1_0_11.valueOf()
? await toUniformCredentialOfferRequestV1_0_11(opts.credentialOffer as CredentialOfferV1_0_11)
: await toUniformCredentialOfferRequest(opts.credentialOffer as CredentialOfferV1_0_13)
? await toUniformCredentialOfferRequest(opts.credentialOffer as CredentialOfferV1_0_11 | CredentialOfferV1_0_13)
: undefined;
const request: Partial<AccessTokenRequest> = {};

Expand Down
8 changes: 4 additions & 4 deletions packages/client/lib/CredentialOfferClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
determineSpecVersionFromURI,
getClientIdFromCredentialOfferPayload,
OpenId4VCIVersion,
toUniformCredentialOfferRequestV1_0_11,
toUniformCredentialOfferRequest,
} from '@sphereon/oid4vci-common';
import Debug from 'debug';

Expand All @@ -31,22 +31,22 @@ export class CredentialOfferClientV1_0_11 {
if (version < OpenId4VCIVersion.VER_1_0_11) {
credentialOfferPayload = convertURIToJsonObject(uri, {
arrayTypeProperties: ['credential_type'],
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['issuer', 'credential_type'],
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['issuer', 'credential_type='],
}) as CredentialOfferPayloadV1_0_09;
credentialOffer = {
credential_offer: credentialOfferPayload,
};
} else {
credentialOffer = convertURIToJsonObject(uri, {
arrayTypeProperties: ['credentials'],
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['credential_offer'],
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
}) as CredentialOfferV1_0_11;
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
}
}

const request = await toUniformCredentialOfferRequestV1_0_11(credentialOffer, {
const request = await toUniformCredentialOfferRequest(credentialOffer, {
...opts,
version,
});
Expand Down
63 changes: 49 additions & 14 deletions packages/client/lib/MetadataClient.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import {
AuthorizationServerMetadata,
AuthorizationServerType,
CredentialIssuerMetadataV1_0_11,
CredentialIssuerMetadataV1_0_13,
CredentialOfferPayload,
CredentialOfferPayloadV1_0_13,
CredentialOfferRequestWithBaseUrl,
determineSpecVersionFromOffer,
EndpointMetadataResultV1_0_11,
EndpointMetadataResultV1_0_13,
getIssuerFromCredentialOfferPayload,
IssuerMetadataV1_0_13,
IssuerMetadataV1_0_08,
OpenId4VCIVersion,
OpenIDResponse,
WellKnownEndpoints,
} from '@sphereon/oid4vci-common';
import Debug from 'debug';

import { MetadataClientV1_0_11 } from './MetadataClientV1_0_11';
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
import { retrieveWellknown } from './functions/OpenIDUtils';

const debug = Debug('sphereon:oid4vci:metadata');
Expand All @@ -24,18 +31,28 @@ export class MetadataClient {
*/
public static async retrieveAllMetadataFromCredentialOffer(
credentialOffer: CredentialOfferRequestWithBaseUrl,
): Promise<EndpointMetadataResultV1_0_13> {
return MetadataClient.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13);
): Promise<EndpointMetadataResultV1_0_13 | EndpointMetadataResultV1_0_11> {
if (determineSpecVersionFromOffer(credentialOffer.credential_offer) >= OpenId4VCIVersion.VER_1_0_13) {
return await MetadataClientV1_0_13.retrieveAllMetadataFromCredentialOffer(credentialOffer);
} else {
return await MetadataClientV1_0_11.retrieveAllMetadataFromCredentialOffer(credentialOffer);
}
}

/**
* Retrieve the metada using the initiation request obtained from a previous step
* @param request
*/
public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayloadV1_0_13): Promise<EndpointMetadataResultV1_0_13> {
public static async retrieveAllMetadataFromCredentialOfferRequest(
request: CredentialOfferPayload,
): Promise<EndpointMetadataResultV1_0_13 | EndpointMetadataResultV1_0_11> {
const issuer = getIssuerFromCredentialOfferPayload(request);
if (issuer) {
return MetadataClient.retrieveAllMetadata(issuer);
if (determineSpecVersionFromOffer(request) >= OpenId4VCIVersion.VER_1_0_13) {
return MetadataClientV1_0_13.retrieveAllMetadataFromCredentialOfferRequest(request as CredentialOfferPayloadV1_0_13);
} else {
return MetadataClientV1_0_11.retrieveAllMetadataFromCredentialOfferRequest(request);
}
}
throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present");
}
Expand All @@ -45,24 +62,33 @@ export class MetadataClient {
* @param issuer The issuer URL
* @param opts
*/
public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadataResultV1_0_13> {
public static async retrieveAllMetadata(
issuer: string,
opts?: { errorOnNotFound: boolean },
): Promise<EndpointMetadataResultV1_0_13 | EndpointMetadataResultV1_0_11> {
let token_endpoint: string | undefined;
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_servers: string[] = [issuer];
let authorization_servers: string[] | undefined = [issuer];
let authorization_server: string | undefined = undefined;
const oid4vciResponse = await MetadataClient.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations
let credentialIssuerMetadata = oid4vciResponse?.successBody;
if (credentialIssuerMetadata) {
debug(`Issuer ${issuer} OID4VCI well-known server metadata\r\n${JSON.stringify(credentialIssuerMetadata)}`);
credential_endpoint = credentialIssuerMetadata.credential_endpoint;
deferred_credential_endpoint = credentialIssuerMetadata.deferred_credential_endpoint;
deferred_credential_endpoint = credentialIssuerMetadata.deferred_credential_endpoint
? (credentialIssuerMetadata.deferred_credential_endpoint as string)
: undefined;
if (credentialIssuerMetadata.token_endpoint) {
token_endpoint = credentialIssuerMetadata.token_endpoint;
}
if (credentialIssuerMetadata.authorization_servers) {
authorization_servers = credentialIssuerMetadata.authorization_servers;
authorization_servers = credentialIssuerMetadata.authorization_servers as string[];
} else if (credentialIssuerMetadata.authorization_server) {
authorization_server = credentialIssuerMetadata.authorization_server as string;
authorization_servers = [authorization_server];
}
}
// No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OIDC IDP. Let's start with OIDC first
Expand Down Expand Up @@ -154,20 +180,24 @@ export class MetadataClient {

if (!credentialIssuerMetadata && authMetadata) {
// Apparently everything worked out and the issuer is exposing everything in oAuth2/OIDC well-knowns. Spec is vague about this situation, but we can support it
credentialIssuerMetadata = authMetadata as CredentialIssuerMetadataV1_0_13;
credentialIssuerMetadata = authorization_server
? (authMetadata as CredentialIssuerMetadataV1_0_11)
: (authMetadata as CredentialIssuerMetadataV1_0_13);
}
debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`);
return {
issuer,
token_endpoint,
credential_endpoint,
deferred_credential_endpoint,
authorization_server: authorization_servers[0],
...(authorization_server ? { authorization_server } : { authorization_servers: authorization_servers }),
authorization_endpoint,
authorizationServerType,
credentialIssuerMetadata: credentialIssuerMetadata,
credentialIssuerMetadata: authorization_server
? (credentialIssuerMetadata as IssuerMetadataV1_0_08 & Partial<AuthorizationServerMetadata>)
: (credentialIssuerMetadata as CredentialIssuerMetadataV1_0_13),
authorizationServerMetadata: authMetadata,
};
} as EndpointMetadataResultV1_0_13 | EndpointMetadataResultV1_0_11;
}

/**
Expand All @@ -180,7 +210,12 @@ export class MetadataClient {
opts?: {
errorOnNotFound?: boolean;
},
): Promise<OpenIDResponse<IssuerMetadataV1_0_13> | undefined> {
): Promise<
| OpenIDResponse<
CredentialIssuerMetadataV1_0_11 | CredentialIssuerMetadataV1_0_13 | (IssuerMetadataV1_0_08 & Partial<AuthorizationServerMetadata>)
>
| undefined
> {
return retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, {
errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound,
});
Expand Down
Loading

0 comments on commit eadbba0

Please sign in to comment.