Skip to content

Commit

Permalink
fix: MetadataClient for version 13 and added better type distinction.…
Browse files Browse the repository at this point in the history
… added credential_definition to credential metadata of v13
  • Loading branch information
sksadjad committed May 16, 2024
1 parent dcf7439 commit e39bf71
Show file tree
Hide file tree
Showing 32 changed files with 1,443 additions and 252 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess
import {
Alg,
CNonceState,
CredentialConfigurationSupported,
CredentialSupported,
CredentialIssuerMetadata,
IssuerCredentialSubjectDisplay,
IssueStatus,
Expand Down Expand Up @@ -86,7 +86,7 @@ describe('issuerCallback', () => {
const clientId = 'sphereon:wallet'

beforeAll(async () => {
const credentialsSupported: Record<string, CredentialConfigurationSupported> = new CredentialSupportedBuilderV1_13()
const credentialsSupported: Record<string, CredentialSupported> = new CredentialSupportedBuilderV1_13()
.withCryptographicSuitesSupported('ES256K')
.withCryptographicBindingMethod('did')
.withFormat('jwt_vc_json')
Expand Down Expand Up @@ -216,7 +216,7 @@ describe('issuerCallback', () => {
const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI }))
.withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential')
.withCredentialEndpointFromMetadata({
credential_configurations_supported: { VeriCred: { format: 'jwt_vc_json' } as CredentialConfigurationSupported },
credential_configurations_supported: { VeriCred: { format: 'jwt_vc_json' } as CredentialSupported },
} as unknown as CredentialIssuerMetadata)
.withFormat('jwt_vc_json')
.withCredentialType('credentialType')
Expand Down
29 changes: 18 additions & 11 deletions packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,26 +159,33 @@ export class AccessTokenClient {
}

private assertAlphanumericPin(pinMeta?: TxCodeAndPinRequired, pin?: string): void {
if (pinMeta && pinMeta.isPinRequired) {
if (pinMeta?.isPinRequired) {
let regex;
if (pinMeta.txCode && pinMeta.txCode.input_mode === 'numeric') {
regex = new RegExp(`^\\d{1,${pinMeta.txCode.length || 8}}$`);
} else if (pinMeta.txCode && pinMeta.txCode.input_mode === 'text') {
regex = new RegExp(`^[a-zA-Z0-9]{1,${pinMeta.txCode.length || 8}}$`);
} else {
// default regex that limits the length to 8
regex = /^[a-zA-Z0-9]{1,8}$/;

if (pinMeta.txCode) {
const { input_mode, length } = pinMeta.txCode;

if (input_mode === 'numeric') {
// Create a regex for numeric input. If no length specified, allow any length of numeric input.
regex = length ? new RegExp(`^\\d{1,${length}}$`) : /^\d+$/;
} else if (input_mode === 'text') {
// Create a regex for text input. If no length specified, allow any length of alphanumeric input.
regex = length ? new RegExp(`^[a-zA-Z0-9]{1,${length}}$`) : /^[a-zA-Z0-9]+$/;
}
}

// Default regex for alphanumeric with no specific length limit if no input_mode is specified.
regex = regex || /^[a-zA-Z0-9]+$/;

if (!pin || !regex.test(pin)) {
debug(
`Pin is not valid. Expected format: ${pinMeta?.txCode?.input_mode || 'alphanumeric'}, Length: up to ${pinMeta?.txCode?.length || 8} characters`,
`Pin is not valid. Expected format: ${pinMeta?.txCode?.input_mode || 'alphanumeric'}, Length: up to ${pinMeta?.txCode?.length || 'any number of'} characters`,
);
throw new Error('A valid pin must be present according to the specified transaction code requirements.');
}
} else if (pin) {
debug(`Pin set, whilst not required`);
throw new Error('Cannot set a pin, when the pin is not required.');
debug('Pin set, whilst not required');
throw new Error('Cannot set a pin when the pin is not required.');
}
}

Expand Down
26 changes: 13 additions & 13 deletions packages/client/lib/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ import {
AuthorizationRequestOpts,
CodeChallengeMethod,
convertJsonToURI,
CredentialConfigurationSupported,
CredentialOfferFormat,
CredentialOfferPayloadV1_0_13,
CredentialOfferRequestWithBaseUrl,
CredentialSupported,
determineSpecVersionFromOffer,
EndpointMetadataResult,
EndpointMetadataResultV1_0_13,
formPost,
JsonURIMode,
OpenId4VCIVersion,
PARMode,
PKCEOpts,
PushedAuthorizationResponse,
ResponseType,
} from '@sphereon/oid4vci-common';
PushedAuthorizationResponse,
ResponseType
} from '@sphereon/oid4vci-common'
import Debug from 'debug';

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

function filterSupportedCredentials(
credentialOffer: CredentialOfferPayloadV1_0_13,
credentialsSupported?: Record<string, CredentialConfigurationSupported>,
): CredentialConfigurationSupported[] {
credentialsSupported?: Record<string, CredentialSupported>,
): CredentialSupported[] {
if (!credentialOffer.credential_configuration_ids || !credentialsSupported) {
return [];
}
Expand All @@ -39,10 +39,10 @@ export const createAuthorizationRequestUrl = async ({
credentialConfigurationSupported,
}: {
pkce: PKCEOpts;
endpointMetadata: EndpointMetadataResult;
endpointMetadata: EndpointMetadataResultV1_0_13;
authorizationRequest: AuthorizationRequestOpts;
credentialOffer?: CredentialOfferRequestWithBaseUrl;
credentialConfigurationSupported?: Record<string, CredentialConfigurationSupported>;
credentialConfigurationSupported?: Record<string, CredentialSupported>;
}): Promise<string> => {
const { redirectUri, clientId } = authorizationRequest;
let { scope, authorizationDetails } = authorizationRequest;
Expand All @@ -58,7 +58,7 @@ export const createAuthorizationRequestUrl = async ({
if ('credentials' in credentialOffer.credential_offer) {
throw new Error('CredentialOffer format is wrong.');
}
const creds: (CredentialConfigurationSupported | CredentialOfferFormat | string)[] =
const creds: (CredentialSupported | CredentialOfferFormat | string)[] =
determineSpecVersionFromOffer(credentialOffer.credential_offer) === OpenId4VCIVersion.VER_1_0_13
? filterSupportedCredentials(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13, credentialConfigurationSupported)
: [];
Expand All @@ -67,7 +67,7 @@ export const createAuthorizationRequestUrl = async ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
authorizationDetails = creds
.flatMap((cred) => cred as CredentialConfigurationSupported)
.flatMap((cred) => cred as CredentialSupported)
.filter((cred) => !!cred)
.map((cred) => {
return {
Expand Down Expand Up @@ -144,7 +144,7 @@ export const createAuthorizationRequestUrl = async ({
};

const handleAuthorizationDetails = (
endpointMetadata: EndpointMetadataResult,
endpointMetadata: EndpointMetadataResultV1_0_13,
authorizationDetails?: AuthorizationDetails | AuthorizationDetails[],
): AuthorizationDetails | AuthorizationDetails[] | undefined => {
if (authorizationDetails) {
Expand All @@ -163,7 +163,7 @@ const handleAuthorizationDetails = (
return authorizationDetails;
};

const handleLocations = (endpointMetadata: EndpointMetadataResult, authorizationDetails: AuthorizationDetails) => {
const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_13, authorizationDetails: AuthorizationDetails) => {
if (typeof authorizationDetails === 'string') {
// backwards compat for older versions of the lib
return authorizationDetails;
Expand Down
14 changes: 7 additions & 7 deletions packages/client/lib/AuthorizationCodeClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import {
AuthorizationRequestOpts,
CodeChallengeMethod,
convertJsonToURI,
CredentialConfigurationSupported,
CredentialSupported,
CredentialOfferFormat,
CredentialOfferPayloadV1_0_11,
CredentialOfferRequestWithBaseUrl,
EndpointMetadataResult,
EndpointMetadataResultV1_0_11,
formPost,
JsonURIMode,
PARMode,
Expand All @@ -27,10 +27,10 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({
credentialsSupported,
}: {
pkce: PKCEOpts;
endpointMetadata: EndpointMetadataResult;
endpointMetadata: EndpointMetadataResultV1_0_11;
authorizationRequest: AuthorizationRequestOpts;
credentialOffer?: CredentialOfferRequestWithBaseUrl;
credentialsSupported?: CredentialConfigurationSupported[];
credentialsSupported?: CredentialSupported[];
}): Promise<string> => {
const { redirectUri, clientId } = authorizationRequest;
let { scope, authorizationDetails } = authorizationRequest;
Expand All @@ -50,7 +50,7 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({
// @ts-ignore
authorizationDetails = creds
.flatMap((cred) =>
typeof cred === 'string' && credentialsSupported ? Object.values(credentialsSupported) : (cred as CredentialConfigurationSupported),
typeof cred === 'string' && credentialsSupported ? Object.values(credentialsSupported) : (cred as CredentialSupported),
)
.filter((cred) => !!cred)
.map((cred) => {
Expand Down Expand Up @@ -128,7 +128,7 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({
};

const handleAuthorizationDetailsV1_0_11 = (
endpointMetadata: EndpointMetadataResult,
endpointMetadata: EndpointMetadataResultV1_0_11,
authorizationDetails?: AuthorizationDetails | AuthorizationDetails[],
): AuthorizationDetails | AuthorizationDetails[] | undefined => {
if (authorizationDetails) {
Expand All @@ -147,7 +147,7 @@ const handleAuthorizationDetailsV1_0_11 = (
return authorizationDetails;
};

const handleLocations = (endpointMetadata: EndpointMetadataResult, authorizationDetails: AuthorizationDetails) => {
const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_11, authorizationDetails: AuthorizationDetails) => {
if (typeof authorizationDetails === 'string') {
// backwards compat for older versions of the lib
return authorizationDetails;
Expand Down
74 changes: 26 additions & 48 deletions packages/client/lib/MetadataClient.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import {
AuthorizationServerMetadata,
AuthorizationServerType,
CredentialIssuerMetadata,
CredentialOfferPayload,
CredentialIssuerMetadataV1_0_13,
CredentialOfferPayloadV1_0_13,
CredentialOfferRequestWithBaseUrl,
EndpointMetadataResult,
EndpointMetadataResultV1_0_13,
getIssuerFromCredentialOfferPayload,
IssuerMetadataV1_0_13,
OpenIDResponse,
WellKnownEndpoints,
} from '@sphereon/oid4vci-common';
WellKnownEndpoints
} from '@sphereon/oid4vci-common'
import Debug from 'debug';

import { getJson } from './functions';
import { retrieveWellknown } from './functions/OpenIDUtils'

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

Expand All @@ -21,15 +22,15 @@ export class MetadataClient {
*
* @param credentialOffer
*/
public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise<EndpointMetadataResult> {
return MetadataClient.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer);
public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise<EndpointMetadataResultV1_0_13> {
return MetadataClient.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13);
}

/**
* Retrieve the metada using the initiation request obtained from a previous step
* @param request
*/
public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise<EndpointMetadataResult> {
public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayloadV1_0_13): Promise<EndpointMetadataResultV1_0_13> {
const issuer = getIssuerFromCredentialOfferPayload(request);
if (issuer) {
return MetadataClient.retrieveAllMetadata(issuer);
Expand All @@ -42,13 +43,13 @@ export class MetadataClient {
* @param issuer The issuer URL
* @param opts
*/
public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadataResult> {
public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadataResultV1_0_13> {
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_server: string = issuer;
let authorization_servers: string[] = [issuer];
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) {
Expand All @@ -58,16 +59,14 @@ export class MetadataClient {
if (credentialIssuerMetadata.token_endpoint) {
token_endpoint = credentialIssuerMetadata.token_endpoint;
}
if (credentialIssuerMetadata.authorization_server) {
authorization_server = credentialIssuerMetadata.authorization_server;
}
if (credentialIssuerMetadata.authorization_endpoint) {
authorization_endpoint = credentialIssuerMetadata.authorization_endpoint;
if (credentialIssuerMetadata.authorization_servers) {
authorization_servers = credentialIssuerMetadata.authorization_servers;
}
}
// No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OIDC IDP. Let's start with OIDC first
let response: OpenIDResponse<AuthorizationServerMetadata> = await MetadataClient.retrieveWellknown(
authorization_server,
// TODO: for now we're taking just the first one
let response: OpenIDResponse<AuthorizationServerMetadata> = await retrieveWellknown(
authorization_servers[0],
WellKnownEndpoints.OPENID_CONFIGURATION,
{
errorOnNotFound: false,
Expand All @@ -79,13 +78,14 @@ export class MetadataClient {
authorizationServerType = 'OIDC';
} else {
// Now let's do OAuth2
response = await MetadataClient.retrieveWellknown(authorization_server, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false });
// TODO: for now we're taking just the first one
response = await retrieveWellknown(authorization_servers[0], WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false });
authMetadata = response.successBody;
}
if (!authMetadata) {
// We will always throw an error, no matter whether the user provided the option not to, because this is bad.
if (issuer !== authorization_server) {
throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_server}, but that server did not provide metadata`);
if (!authorization_servers.includes(issuer)) {
throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_servers}, but that server did not provide metadata`);
}
} else {
if (!authorizationServerType) {
Expand All @@ -103,7 +103,7 @@ export class MetadataClient {
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (!authMetadata.token_endpoint) {
throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`);
throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`);
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
throw Error(
`Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`,
Expand Down Expand Up @@ -152,15 +152,15 @@ 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 CredentialIssuerMetadata;
credentialIssuerMetadata = 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_server: authorization_servers[0],
authorization_endpoint,
authorizationServerType,
credentialIssuerMetadata: credentialIssuerMetadata,
Expand All @@ -178,31 +178,9 @@ export class MetadataClient {
opts?: {
errorOnNotFound?: boolean;
},
): Promise<OpenIDResponse<CredentialIssuerMetadata> | undefined> {
return MetadataClient.retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, {
): Promise<OpenIDResponse<IssuerMetadataV1_0_13> | undefined> {
return retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, {
errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound,
});
}

/**
* Allows to retrieve information from a well-known location
*
* @param host The host
* @param endpointType The endpoint type, currently supports OID4VCI, OIDC and OAuth2 endpoint types
* @param opts Options, like for instance whether an error should be thrown in case the endpoint doesn't exist
*/
public static async retrieveWellknown<T>(
host: string,
endpointType: WellKnownEndpoints,
opts?: { errorOnNotFound?: boolean },
): Promise<OpenIDResponse<T>> {
const result: OpenIDResponse<T> = await getJson(`${host.endsWith('/') ? host.slice(0, -1) : host}${endpointType}`, {
exceptionOnHttpErrorStatus: opts?.errorOnNotFound,
});
if (result.origResponse.status >= 400) {
// We only get here when error on not found is false
debug(`host ${host} with endpoint type ${endpointType} status: ${result.origResponse.status}, ${result.origResponse.statusText}`);
}
return result;
}
}
Loading

0 comments on commit e39bf71

Please sign in to comment.