diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index ef5613ab..dfefd438 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -4,7 +4,7 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess import { Alg, CNonceState, - CredentialConfigurationSupported, + CredentialSupported, CredentialIssuerMetadata, IssuerCredentialSubjectDisplay, IssueStatus, @@ -86,7 +86,7 @@ describe('issuerCallback', () => { const clientId = 'sphereon:wallet' beforeAll(async () => { - const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') @@ -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') diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index f1014238..cd0c5b40 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -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.'); } } diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 04d4fd96..4699146f 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -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, -): CredentialConfigurationSupported[] { + credentialsSupported?: Record, +): CredentialSupported[] { if (!credentialOffer.credential_configuration_ids || !credentialsSupported) { return []; } @@ -39,10 +39,10 @@ export const createAuthorizationRequestUrl = async ({ credentialConfigurationSupported, }: { pkce: PKCEOpts; - endpointMetadata: EndpointMetadataResult; + endpointMetadata: EndpointMetadataResultV1_0_13; authorizationRequest: AuthorizationRequestOpts; credentialOffer?: CredentialOfferRequestWithBaseUrl; - credentialConfigurationSupported?: Record; + credentialConfigurationSupported?: Record; }): Promise => { const { redirectUri, clientId } = authorizationRequest; let { scope, authorizationDetails } = authorizationRequest; @@ -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) : []; @@ -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 { @@ -144,7 +144,7 @@ export const createAuthorizationRequestUrl = async ({ }; const handleAuthorizationDetails = ( - endpointMetadata: EndpointMetadataResult, + endpointMetadata: EndpointMetadataResultV1_0_13, authorizationDetails?: AuthorizationDetails | AuthorizationDetails[], ): AuthorizationDetails | AuthorizationDetails[] | undefined => { if (authorizationDetails) { @@ -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; diff --git a/packages/client/lib/AuthorizationCodeClientV1_0_11.ts b/packages/client/lib/AuthorizationCodeClientV1_0_11.ts index 6140e0cf..e94e04d1 100644 --- a/packages/client/lib/AuthorizationCodeClientV1_0_11.ts +++ b/packages/client/lib/AuthorizationCodeClientV1_0_11.ts @@ -3,11 +3,11 @@ import { AuthorizationRequestOpts, CodeChallengeMethod, convertJsonToURI, - CredentialConfigurationSupported, + CredentialSupported, CredentialOfferFormat, CredentialOfferPayloadV1_0_11, CredentialOfferRequestWithBaseUrl, - EndpointMetadataResult, + EndpointMetadataResultV1_0_11, formPost, JsonURIMode, PARMode, @@ -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 => { const { redirectUri, clientId } = authorizationRequest; let { scope, authorizationDetails } = authorizationRequest; @@ -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) => { @@ -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) { @@ -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; diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index 1e58786e..602e996a 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -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'); @@ -21,15 +22,15 @@ export class MetadataClient { * * @param credentialOffer */ - public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise { - return MetadataClient.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer); + public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise { + 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 { + public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayloadV1_0_13): Promise { const issuer = getIssuerFromCredentialOfferPayload(request); if (issuer) { return MetadataClient.retrieveAllMetadata(issuer); @@ -42,13 +43,13 @@ export class MetadataClient { * @param issuer The issuer URL * @param opts */ - public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise { + public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise { 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) { @@ -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 = await MetadataClient.retrieveWellknown( - authorization_server, + // TODO: for now we're taking just the first one + let response: OpenIDResponse = await retrieveWellknown( + authorization_servers[0], WellKnownEndpoints.OPENID_CONFIGURATION, { errorOnNotFound: false, @@ -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) { @@ -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})`, @@ -152,7 +152,7 @@ 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 { @@ -160,7 +160,7 @@ export class MetadataClient { token_endpoint, credential_endpoint, deferred_credential_endpoint, - authorization_server, + authorization_server: authorization_servers[0], authorization_endpoint, authorizationServerType, credentialIssuerMetadata: credentialIssuerMetadata, @@ -178,31 +178,9 @@ export class MetadataClient { opts?: { errorOnNotFound?: boolean; }, - ): Promise | undefined> { - return MetadataClient.retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, { + ): Promise | 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( - host: string, - endpointType: WellKnownEndpoints, - opts?: { errorOnNotFound?: boolean }, - ): Promise> { - const result: OpenIDResponse = 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; - } } diff --git a/packages/client/lib/MetadataClientV1_0_11.ts b/packages/client/lib/MetadataClientV1_0_11.ts new file mode 100644 index 00000000..f50e3327 --- /dev/null +++ b/packages/client/lib/MetadataClientV1_0_11.ts @@ -0,0 +1,186 @@ +import { + AuthorizationServerMetadata, + AuthorizationServerType, + CredentialIssuerMetadataV1_0_11, + CredentialOfferPayload, + CredentialOfferRequestWithBaseUrl, + EndpointMetadataResultV1_0_11, + getIssuerFromCredentialOfferPayload, IssuerMetadataV1_0_08, + OpenIDResponse, + WellKnownEndpoints +} from '@sphereon/oid4vci-common' +import Debug from 'debug'; + +import { retrieveWellknown } from './functions/OpenIDUtils' + +const debug = Debug('sphereon:oid4vci:metadata'); + +export class MetadataClientV1_0_11 { + /** + * Retrieve metadata using the Initiation obtained from a previous step + * + * @param credentialOffer + */ + public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise { + return MetadataClientV1_0_11.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer); + } + + /** + * Retrieve the metada using the initiation request obtained from a previous step + * @param request + */ + public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise { + const issuer = getIssuerFromCredentialOfferPayload(request); + if (issuer) { + return MetadataClientV1_0_11.retrieveAllMetadata(issuer); + } + throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present"); + } + + /** + * Retrieve all metadata from an issuer + * @param issuer The issuer URL + * @param opts + */ + public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise { + 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; + const oid4vciResponse = await MetadataClientV1_0_11.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; + 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; + } + } + // No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OIDC IDP. Let's start with OIDC first + let response: OpenIDResponse = await retrieveWellknown( + authorization_server, + WellKnownEndpoints.OPENID_CONFIGURATION, + { + errorOnNotFound: false, + }, + ); + let authMetadata = response.successBody; + if (authMetadata) { + debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`); + authorizationServerType = 'OIDC'; + } else { + // Now let's do OAuth2 + response = await retrieveWellknown(authorization_server, 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`); + } + } else { + if (!authorizationServerType) { + authorizationServerType = 'OAuth 2.0'; + } + debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`); + if (!authMetadata.authorization_endpoint) { + console.warn( + `Issuer ${issuer} of type ${authorizationServerType} has no authorization_endpoint! Will use ${authorization_endpoint}. This only works for pre-authorized flows`, + ); + } else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) { + throw Error( + `Credential issuer has a different authorization_endpoint (${authorization_endpoint}) from the Authorization Server (${authMetadata.authorization_endpoint})`, + ); + } + authorization_endpoint = authMetadata.authorization_endpoint; + if (!authMetadata.token_endpoint) { + throw Error(`Authorization Sever ${authorization_server} 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})`, + ); + } + token_endpoint = authMetadata.token_endpoint; + if (authMetadata.credential_endpoint) { + if (credential_endpoint && authMetadata.credential_endpoint !== credential_endpoint) { + debug( + `Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.credential_endpoint}). Will use the issuer value`, + ); + } else { + credential_endpoint = authMetadata.credential_endpoint; + } + } + if (authMetadata.deferred_credential_endpoint) { + if (deferred_credential_endpoint && authMetadata.deferred_credential_endpoint !== deferred_credential_endpoint) { + debug( + `Credential issuer has a different deferred_credential_endpoint (${deferred_credential_endpoint}) from the Authorization Server (${authMetadata.deferred_credential_endpoint}). Will use the issuer value`, + ); + } else { + deferred_credential_endpoint = authMetadata.deferred_credential_endpoint; + } + } + } + + if (!authorization_endpoint) { + debug(`Issuer ${issuer} does not expose authorization_endpoint, so only pre-auth will be supported`); + } + if (!token_endpoint) { + debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`); + if (opts?.errorOnNotFound) { + throw Error(`Could not deduce the token_endpoint for ${issuer}`); + } else { + token_endpoint = `${issuer}${issuer.endsWith('/') ? 'token' : '/token'}`; + } + } + if (!credential_endpoint) { + debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`); + if (opts?.errorOnNotFound) { + throw Error(`Could not deduce the credential endpoint for ${issuer}`); + } else { + credential_endpoint = `${issuer}${issuer.endsWith('/') ? 'credential' : '/credential'}`; + } + } + + 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_11; + } + debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`); + return { + issuer, + token_endpoint, + credential_endpoint, + deferred_credential_endpoint, + authorization_server, + authorization_endpoint, + authorizationServerType, + credentialIssuerMetadata: credentialIssuerMetadata as unknown as (Partial & IssuerMetadataV1_0_08), + authorizationServerMetadata: authMetadata, + }; + } + + /** + * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata + * + * @param issuerHost The issuer hostname + */ + public static async retrieveOpenID4VCIServerMetadata( + issuerHost: string, + opts?: { + errorOnNotFound?: boolean; + }, + ): Promise | undefined> { + return retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, { + errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound, + }); + } +} diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index b6398267..e055eee9 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -5,11 +5,11 @@ import { AuthorizationResponse, AuthzFlowType, CodeChallengeMethod, - CredentialConfigurationSupported, CredentialOfferRequestWithBaseUrl, CredentialResponse, + CredentialSupported, DefaultURISchemes, - EndpointMetadataResult, + EndpointMetadataResultV1_0_13, getClientIdFromCredentialOfferPayload, getIssuerFromCredentialOfferPayload, getSupportedCredentials, @@ -19,9 +19,8 @@ import { OID4VCICredentialFormat, OpenId4VCIVersion, PKCEOpts, - ProofOfPossessionCallbacks, - toAuthorizationResponsePayload, -} from '@sphereon/oid4vci-common'; + ProofOfPossessionCallbacks, toAuthorizationResponsePayload +} from '@sphereon/oid4vci-common' import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; @@ -42,7 +41,7 @@ export interface OpenID4VCIClientState { kid?: string; jwk?: JWK; alg?: Alg | string; - endpointMetadata?: EndpointMetadataResult; + endpointMetadata?: EndpointMetadataResultV1_0_13; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; authorizationCodeResponse?: AuthorizationResponse; @@ -76,7 +75,7 @@ export class OpenID4VCIClient { pkce?: PKCEOpts; authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl jwk?: JWK; - endpointMetadata?: EndpointMetadataResult; + endpointMetadata?: EndpointMetadataResultV1_0_13; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; authorizationCodeResponse?: AuthorizationResponse; @@ -228,7 +227,7 @@ export class OpenID4VCIClient { return this._state.authorizationURL; } - public async retrieveServerMetadata(): Promise { + public async retrieveServerMetadata(): Promise { this.assertIssuerData(); if (!this._state.endpointMetadata) { if (this.credentialOffer) { @@ -441,7 +440,7 @@ export class OpenID4VCIClient { getCredentialsSupported( format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[], - ): Record { + ): Record { return getSupportedCredentials({ issuerMetadata: this.endpointMetadata.credentialIssuerMetadata, version: this.version(), @@ -465,7 +464,7 @@ export class OpenID4VCIClient { return this.credentialOffer?.version ?? OpenId4VCIVersion.VER_1_0_11; } - public get endpointMetadata(): EndpointMetadataResult { + public get endpointMetadata(): EndpointMetadataResultV1_0_13 { this.assertServerMetadata(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._state.endpointMetadata!; diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 293b5849..9c7f0206 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -5,13 +5,13 @@ import { AuthorizationResponse, AuthzFlowType, CodeChallengeMethod, - CredentialConfigurationSupported, CredentialOfferPayloadV1_0_08, CredentialOfferPayloadV1_0_11, CredentialOfferRequestWithBaseUrl, CredentialResponse, + CredentialSupported, DefaultURISchemes, - EndpointMetadataResult, + EndpointMetadataResultV1_0_11, getClientIdFromCredentialOfferPayload, getIssuerFromCredentialOfferPayload, getSupportedCredentials, @@ -31,7 +31,7 @@ import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11'; import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11'; import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11'; import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; -import { MetadataClient } from './MetadataClient'; +import { MetadataClientV1_0_11 } from './MetadataClientV1_0_11' import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { generateMissingPKCEOpts } from './functions/AuthorizationUtil'; @@ -44,7 +44,7 @@ export interface OpenID4VCIClientStateV1_0_11 { kid?: string; jwk?: JWK; alg?: Alg | string; - endpointMetadata?: EndpointMetadataResult; + endpointMetadata?: EndpointMetadataResultV1_0_11; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; authorizationCodeResponse?: AuthorizationResponse; @@ -78,7 +78,7 @@ export class OpenID4VCIClientV1_0_11 { pkce?: PKCEOpts; authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl jwk?: JWK; - endpointMetadata?: EndpointMetadataResult; + endpointMetadata?: EndpointMetadataResultV1_0_11; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; authorizationCodeResponse?: AuthorizationResponse; @@ -230,13 +230,13 @@ export class OpenID4VCIClientV1_0_11 { return this._state.authorizationURL; } - public async retrieveServerMetadata(): Promise { + public async retrieveServerMetadata(): Promise { this.assertIssuerData(); if (!this._state.endpointMetadata) { if (this.credentialOffer) { - this._state.endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer); + this._state.endpointMetadata = await MetadataClientV1_0_11.retrieveAllMetadataFromCredentialOffer(this.credentialOffer); } else if (this._state.credentialIssuer) { - this._state.endpointMetadata = await MetadataClient.retrieveAllMetadata(this._state.credentialIssuer); + this._state.endpointMetadata = await MetadataClientV1_0_11.retrieveAllMetadata(this._state.credentialIssuer); } else { throw Error(`Cannot retrieve issuer metadata without either a credential offer, or issuer value`); } @@ -449,7 +449,7 @@ export class OpenID4VCIClientV1_0_11 { getCredentialsSupportedV11( restrictToInitiationTypes: boolean, format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[], - ): Record { + ): Record { return getSupportedCredentials({ issuerMetadata: this.endpointMetadata.credentialIssuerMetadata, version: this.version(), @@ -460,7 +460,7 @@ export class OpenID4VCIClientV1_0_11 { getCredentialsSupported( format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[], - ): Record { + ): Record { return getSupportedCredentials({ issuerMetadata: this.endpointMetadata.credentialIssuerMetadata, version: this.version(), @@ -522,7 +522,7 @@ export class OpenID4VCIClientV1_0_11 { return this.credentialOffer?.version ?? OpenId4VCIVersion.VER_1_0_11; } - public get endpointMetadata(): EndpointMetadataResult { + public get endpointMetadata(): EndpointMetadataResultV1_0_11 { this.assertServerMetadata(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._state.endpointMetadata!; diff --git a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts index d5d5e8c6..648d7a60 100644 --- a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts @@ -283,4 +283,30 @@ describe('Credential Request Client with different issuers ', () => { expect(credentialRequest).toEqual(getMockData('diwala')?.credential.request); }); + + it('should create correct CredentialRequest for credenco', async () => { + const IRR_URI = + 'openid-credential-offer://mijnkvk.acc.credenco.com/?credential_offer_uri=https%3A%2F%2Fmijnkvk.acc.credenco.com%2Fopenid4vc%2FcredentialOffer%3Fid%3D32fc4ebf-9e31-4149-9877-e3c0b602d559'; + const credentialOffer = await ( + await CredentialRequestClientBuilder.fromURI({ + uri: IRR_URI, + metadata: getMockData('credenco')?.metadata as unknown as EndpointMetadata, + }) + ) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: 'jwt', + jwt: getMockData('diwala')?.credential.request.proof.jwt as string, + }, + credentialTypes: ['OpenBadgeCredential'], + format: 'ldp_vc', + version: OpenId4VCIVersion.VER_1_0_08, + }); + + // createCredentialRequest returns uniform format in draft 11 + const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08); + + expect(credentialRequest).toEqual(getMockData('diwala')?.credential.request); + }); }); diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index e0c2ed68..917e690d 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -6,6 +6,7 @@ import { Jwt, OpenId4VCIVersion, ProofOfPossession, + resolveCredentialOfferURI, WellKnownEndpoints, } from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -248,3 +249,133 @@ describe('OID4VCI-Client should', () => { UNIT_TEST_TIMEOUT, ); }); + +describe('OIDVCI-Client for v1_0_13 should', () => { + const INITIATE_QR_V1_0_13_CREDENCO = + 'openid-credential-offer://mijnkvk.acc.credenco.com/?credential_offer_uri=https%3A%2F%2Fmijnkvk.acc.credenco.com%2Fopenid4vc%2FcredentialOffer%3Fid%3D32fc4ebf-9e31-4149-9877-e3c0b602d559'; + + const mockedCredentialOffer = { + credential_issuer: 'https://mijnkvk.acc.credenco.com', + credential_configuration_ids: ['BevoegdheidUittreksel_jwt_vc_json'], + grants: { + authorization_code: { + issuer_state: '32fc4ebf-9e31-4149-9877-e3c0b602d559', + }, + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + pre_authorized_code: + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIzMmZjNGViZi05ZTMxLTQxNDktOTg3Ny1lM2MwYjYwMmQ1NTkiLCJpc3MiOiJodHRwczovL21pam5rdmsuYWNjLmNyZWRlbmNvLmNvbSIsImF1ZCI6IlRPS0VOIn0.754aiQ87O0vHYSpRvPqAS9cLOgf-pewdeXbpLziRwsxEp9mENfaXpY62muYpzOaWcYmTOydkzhFul-NDYXJZCA', + }, + }, + }; + + beforeEach(() => { + // Mock the HTTP GET request to the credential offer URI + nock('https://mijnkvk.acc.credenco.com') + .get('/openid4vc/credentialOffer?id=32fc4ebf-9e31-4149-9877-e3c0b602d559') + .reply(200, mockedCredentialOffer) + .persist(); // Use .persist() if you want the mock to remain active for multiple tests + }); + + afterEach(() => { + // Clean up all mocks + nock.cleanAll(); + }); + + /*function succeedWithAFullFlowWithClientSetup() { + nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA)); + nock(IDENTIPROOF_AS_URL).get('/.well-known/oauth-authorization-server').reply(200, JSON.stringify(IDENTIPROOF_AS_METADATA)); + nock(IDENTIPROOF_AS_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); + nock(IDENTIPROOF_AS_URL) + .post(/oauth2\/token.*!/) + .reply(200, JSON.stringify(mockedAccessTokenResponse)); + nock(ISSUER_URL) + .post(/credential/) + .reply(200, { + format: 'jwt-vc', + credential: mockedVC, + }); + }*/ + + it('should successfully resolve the credential offer URI', async () => { + const uri = 'https://mijnkvk.acc.credenco.com/openid4vc/credentialOffer?id=32fc4ebf-9e31-4149-9877-e3c0b602d559'; + + const credentialOffer = await resolveCredentialOfferURI(uri); + + expect(credentialOffer).toEqual(mockedCredentialOffer); + }); + + it( + 'succeed credenco with a full flow without the client v1_0_13', + async () => { + /* Convert the URI into an object */ + // openid-credential-offer://?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%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%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D + const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR_V1_0_13_CREDENCO); + /** + * {"credential_issuer":"https://mijnkvk.acc.credenco.com","credential_configuration_ids":["BevoegdheidUittreksel_jwt_vc_json"],"grants":{"authorization_code":{"issuer_state":"32fc4ebf-9e31-4149-9877-e3c0b602d559"},"urn:ietf:params:oauth:grant-type:pre-authorized_code":{"pre-authorized_code":"eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIzMmZjNGViZi05ZTMxLTQxNDktOTg3Ny1lM2MwYjYwMmQ1NTkiLCJpc3MiOiJodHRwczovL21pam5rdmsuYWNjLmNyZWRlbmNvLmNvbSIsImF1ZCI6IlRPS0VOIn0.754aiQ87O0vHYSpRvPqAS9cLOgf-pewdeXbpLziRwsxEp9mENfaXpY62muYpzOaWcYmTOydkzhFul-NDYXJZCA"}}} + */ + const preAuthorizedCode = + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIzMmZjNGViZi05ZTMxLTQxNDktOTg3Ny1lM2MwYjYwMmQ1NTkiLCJpc3MiOiJodHRwczovL21pam5rdmsuYWNjLmNyZWRlbmNvLmNvbSIsImF1ZCI6IlRPS0VOIn0.754aiQ87O0vHYSpRvPqAS9cLOgf-pewdeXbpLziRwsxEp9mENfaXpY62muYpzOaWcYmTOydkzhFul-NDYXJZCA'; + expect(credentialOffer.baseUrl).toEqual('openid-credential-offer://mijnkvk.acc.credenco.com/'); + expect((credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13).credential_configuration_ids).toEqual([ + 'BevoegdheidUittreksel_jwt_vc_json', + ]); + expect(credentialOffer.original_credential_offer.grants).toEqual({ + authorization_code: { + issuer_state: '32fc4ebf-9e31-4149-9877-e3c0b602d559', + }, + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + pre_authorized_code: preAuthorizedCode, + }, + }); + + /*nock(ISSUER_URL) + .post(/token.*!/) + .reply(200, JSON.stringify(mockedAccessTokenResponse));*/ + + /* The actual access token calls */ + const accessTokenClient: AccessTokenClient = new AccessTokenClient(); + const accessTokenResponse = await accessTokenClient.acquireAccessToken({ + credentialOffer: credentialOffer, + pin: preAuthorizedCode /*, metadata: {}*/, + }); + expect(accessTokenResponse.successBody).toEqual({}); + /*// Get the credential + nock(ISSUER_URL) + .post(/credential/) + .reply(200, { + format: 'jwt-vc', + credential: mockedVC, + }); + const credReqClient = CredentialRequestClientBuilder.fromCredentialOffer({ credentialOffer: credentialOffer }) + .withFormat('jwt_vc') + + .withTokenFromResponse(accessTokenResponse.successBody!) + .build(); + + //TS2322: Type '(args: ProofOfPossessionCallbackArgs) => Promise' + // is not assignable to type 'ProofOfPossessionCallback'. + // Types of parameters 'args' and 'args' are incompatible. + // Property 'kid' is missing in type '{ header: unknown; payload: unknown; }' but required in type 'ProofOfPossessionCallbackArgs'. + const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ + jwt, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_11, + }) + .withEndpointMetadata({ + issuer: 'https://issuer.research.identiproof.io', + credential_endpoint: 'https://issuer.research.identiproof.io/credential', + token_endpoint: 'https://issuer.research.identiproof.io/token', + }) + .withKid('did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1') + .build(); + const credResponse = await credReqClient.acquireCredentialsUsingProof({ + proofInput: proof, + credentialTypes: credentialOffer.original_credential_offer.credential_configuration_ids, + }); + expect(credResponse.successBody?.credential).toEqual(mockedVC);*/ + }, + UNIT_TEST_TIMEOUT, + ); +}); diff --git a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts index 8d9fa67c..92b3ec83 100644 --- a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts +++ b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts @@ -1,4 +1,7 @@ import { OpenId4VCIVersion } from '@sphereon/oid4vci-common'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import nock from 'nock'; import { CredentialOfferClient } from '../CredentialOfferClient'; @@ -30,4 +33,32 @@ describe('Issuance Initiation', () => { expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X'); }); + + it('Should return credenco Credential Offer', async () => { + nock('https://mijnkvk.acc.credenco.com') + .get('/openid4vc/credentialOffer?id=32fc4ebf-9e31-4149-9877-e3c0b602d559') + .reply(200, { + credential_issuer: 'https://mijnkvk.acc.credenco.com', + credential_configuration_ids: ['BevoegdheidUittreksel_jwt_vc_json'], + grants: { + authorization_code: { + issuer_state: '32fc4ebf-9e31-4149-9877-e3c0b602d559', + }, + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIzMmZjNGViZi05ZTMxLTQxNDktOTg3Ny1lM2MwYjYwMmQ1NTkiLCJpc3MiOiJodHRwczovL21pam5rdmsuYWNjLmNyZWRlbmNvLmNvbSIsImF1ZCI6IlRPS0VOIn0.754aiQ87O0vHYSpRvPqAS9cLOgf-pewdeXbpLziRwsxEp9mENfaXpY62muYpzOaWcYmTOydkzhFul-NDYXJZCA', + }, + }, + }); + const client = await CredentialOfferClient.fromURI( + 'openid-credential-offer://mijnkvk.acc.credenco.com/?credential_offer_uri=https%3A%2F%2Fmijnkvk.acc.credenco.com%2Fopenid4vc%2FcredentialOffer%3Fid%3D32fc4ebf-9e31-4149-9877-e3c0b602d559', + ); + expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_13); + expect(client.baseUrl).toEqual('openid-credential-offer://mijnkvk.acc.credenco.com/'); + expect(client.scheme).toEqual('openid-credential-offer'); + expect(client.credential_offer.credential_issuer).toEqual('https://mijnkvk.acc.credenco.com'); + expect(client.preAuthorizedCode).toEqual( + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIzMmZjNGViZi05ZTMxLTQxNDktOTg3Ny1lM2MwYjYwMmQ1NTkiLCJpc3MiOiJodHRwczovL21pam5rdmsuYWNjLmNyZWRlbmNvLmNvbSIsImF1ZCI6IlRPS0VOIn0.754aiQ87O0vHYSpRvPqAS9cLOgf-pewdeXbpLziRwsxEp9mENfaXpY62muYpzOaWcYmTOydkzhFul-NDYXJZCA', + ); + }); }); diff --git a/packages/client/lib/__tests__/MetadataClient.spec.ts b/packages/client/lib/__tests__/MetadataClient.spec.ts index 1a0ce12b..0cb8006c 100644 --- a/packages/client/lib/__tests__/MetadataClient.spec.ts +++ b/packages/client/lib/__tests__/MetadataClient.spec.ts @@ -18,6 +18,7 @@ import { WALT_ISSUER_URL, WALT_OID4VCI_METADATA, } from './MetadataMocks'; +import { getMockData } from './data/VciDataFixtures'; describe('MetadataClient with IdentiProof Issuer should', () => { beforeAll(() => { @@ -257,4 +258,64 @@ describe.skip('Metadataclient with SpruceId should', () => { }, }); }); + + it('succeed without OID4VCI and with OIDC metadata of credenco', async () => { + /*nock(WALT_ISSUER_URL).get(WellKnownEndpoints.OPENID4VCI_ISSUER).reply(200, JSON.stringify(WALT_OID4VCI_METADATA)); + + nock(WALT_ISSUER_URL) + .get(/.well-known\/.*!/) + .times(2) + .reply(404, JSON.stringify({ error: 'does not exist' })); +*/ + const metadata = await MetadataClient.retrieveAllMetadata('https://mijnkvk.acc.credenco.com/'); + expect(metadata.credential_endpoint).toEqual('https://ngi-oidc4vci-test.spruceid.xyz/credential'); + expect(metadata.token_endpoint).toEqual('https://ngi-oidc4vci-test.spruceid.xyz/token'); + expect(metadata.credentialIssuerMetadata).toEqual({ + issuer: 'https://ngi-oidc4vci-test.spruceid.xyz', + credential_endpoint: 'https://ngi-oidc4vci-test.spruceid.xyz/credential', + token_endpoint: 'https://ngi-oidc4vci-test.spruceid.xyz/token', + jwks_uri: 'https://ngi-oidc4vci-test.spruceid.xyz/jwks', + grant_types_supported: ['urn:ietf:params:oauth:grant-type:pre-authorized_code'], + credentials_supported: { + OpenBadgeCredential: { + formats: { + jwt_vc: { + types: ['VerifiableCredential', 'OpenBadgeCredential'], + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K'], + }, + ldp_vc: { + types: ['VerifiableCredential', 'OpenBadgeCredential'], + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + }, + }); + }); +}); + +describe('Metadataclient with Credenco should', () => { + beforeAll(() => { + nock.cleanAll(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it('succeed without OID4VCI and with OIDC metadata', async () => { + /*nock(WALT_ISSUER_URL).get(WellKnownEndpoints.OPENID4VCI_ISSUER).reply(200, JSON.stringify(WALT_OID4VCI_METADATA)); + + nock(WALT_ISSUER_URL) + .get(/.well-known\/.*!/) + .times(2) + .reply(404, JSON.stringify({ error: 'does not exist' })); +*/ + const metadata = await MetadataClient.retrieveAllMetadata('https://mijnkvk.acc.credenco.com/'); + expect(metadata.credential_endpoint).toEqual('https://mijnkvk.acc.credenco.com/credential'); + expect(metadata.token_endpoint).toEqual('https://mijnkvk.acc.credenco.com/token'); + expect(metadata.credentialIssuerMetadata).toEqual(getMockData('credenco')); + }); }); diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts index 59c88940..e8d88ef2 100644 --- a/packages/client/lib/__tests__/SdJwt.spec.ts +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -1,4 +1,9 @@ -import { AccessTokenRequest, CredentialRequestV1_0_11, CredentialSupportedSdJwtVc } from '@sphereon/oid4vci-common'; +import { + AccessTokenRequest, + CredentialConfigurationSupportedV1_0_13, + CredentialRequestV1_0_11, + CredentialSupportedSdJwtVc, +} from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import nock from 'nock'; @@ -19,7 +24,7 @@ const issuerMetadata = new IssuerMetadataBuilderV1_13() format: 'vc+sd-jwt', vct: 'SdJwtCredential', id: 'SdJwtCredentialId', - }) + } as CredentialConfigurationSupportedV1_0_13) .build(); const vcIssuer = new VcIssuerBuilder() @@ -122,7 +127,7 @@ describe('sd-jwt vc', () => { }); }); - await client.acquireAccessToken({pin: '123'}); + await client.acquireAccessToken({ pin: '123' }); nock(issuerMetadata.credential_endpoint as string) .post('/') diff --git a/packages/client/lib/__tests__/data/VciDataFixtures.ts b/packages/client/lib/__tests__/data/VciDataFixtures.ts index c2fe8d36..bab6bee2 100644 --- a/packages/client/lib/__tests__/data/VciDataFixtures.ts +++ b/packages/client/lib/__tests__/data/VciDataFixtures.ts @@ -1,4 +1,9 @@ -import { CredentialSupportedFormatV1_0_08, IssuerCredentialSubjectDisplay, IssuerMetadataV1_0_08 } from '@sphereon/oid4vci-common'; +import { + CredentialSupportedFormatV1_0_08, + IssuerCredentialSubjectDisplay, + IssuerMetadataV1_0_08, + IssuerMetadataV1_0_13, +} from '@sphereon/oid4vci-common'; import { ICredentialStatus, W3CVerifiableCredential } from '@sphereon/ssi-types'; export function getMockData(issuerName: string): IssuerMockData | null { @@ -17,7 +22,7 @@ export interface IssuerMockData { issuer?: string; token_endpoint: string; credential_endpoint: string; - openid4vci_metadata: IssuerMetadataV1_0_08; + openid4vci_metadata: IssuerMetadataV1_0_08 | IssuerMetadataV1_0_13; }; auth: { url: string; @@ -742,4 +747,636 @@ const mockData: VciMockDataStructure = { }, }, }, + credenco: { + metadata: { + issuer: 'https://mijnkvk.acc.credenco.com', + token_endpoint: 'https://mijnkvk.acc.credenco.com/token', + credential_endpoint: 'https://mijnkvk.acc.credenco.com/credential', + openid4vci_metadata: { + authorization_endpoint: 'https://mijnkvk.acc.credenco.com/authorize', + authorization_server: 'https://mijnkvk.acc.credenco.com', + batch_credential_endpoint: 'https://mijnkvk.acc.credenco.com/batch_credential', + credential_configurations_supported: { + BevoegdheidUittreksel_jwt_vc_json: { + credential_definition: { + type: ['VerifiableCredential', 'BevoegdheidUittreksel'], + value_type: { + functie: { + display: [ + { + name: 'Functie', + }, + { + locale: 'nl-NL', + name: 'Functie', + }, + { + locale: 'en-US', + name: 'Function', + }, + ], + }, + functionarisNaam: { + display: [ + { + name: 'Functionaris naam', + }, + { + locale: 'nl-NL', + name: 'Functionaris naam', + }, + { + locale: 'en-US', + name: 'Officer name', + }, + ], + mandatory: true, + }, + kvkNummer: { + display: [ + { + name: 'KVK nummer', + }, + { + locale: 'nl-NL', + name: 'KVK nummer', + }, + { + locale: 'en-US', + name: 'CoC number', + }, + ], + mandatory: true, + }, + naam: { + display: [ + { + name: 'Naam', + }, + { + locale: 'nl-NL', + name: 'Naam', + }, + { + locale: 'en-US', + name: 'Name', + }, + ], + mandatory: true, + }, + persoonRechtsvorm: { + display: [ + { + name: 'Rechtsvorm', + }, + { + locale: 'nl-NL', + name: 'Rechtsvorm', + }, + { + locale: 'en-US', + name: 'Legal form', + }, + ], + }, + soortBevoegdheid: { + display: [ + { + name: 'Soort bevoegdheid', + }, + { + locale: 'nl-NL', + name: 'Soort bevoegdheid', + }, + { + locale: 'en-US', + name: 'Type of authority', + }, + ], + }, + }, + }, + credential_signing_alg_values_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + cryptographic_binding_methods_supported: ['did:web', 'did:jwk', 'did:ebsi'], + display: [ + { + background_color: '#e6f2f5', + background_image: { + alt_text: 'KvK styled card Background', + url: 'https://mijnkvk.acc.credenco.com/kvk_card_background.png', + }, + name: 'BevoegdheidUittreksel', + text_color: '#00526e', + }, + { + locale: 'nl-NL', + name: 'Bevoegdheid uittreksel', + }, + { + locale: 'en-US', + name: 'Power of Attorney', + }, + ], + format: 'jwt_vc_json', + }, + KVKRegistration_jwt_vc_json: { + credential_definition: { + type: ['VerifiableCredential', 'VerifiableAttestation', 'KVKRegistration'], + value_type: { + einddatum: { + display: [ + { + name: 'Einddatum', + }, + { + locale: 'nl-NL', + name: 'Einddatum', + }, + { + locale: 'en-US', + name: 'End date', + }, + ], + }, + kvkNummer: { + display: [ + { + name: 'KVK nummer', + }, + { + locale: 'nl-NL', + name: 'KVK nummer', + }, + { + locale: 'en-US', + name: 'CoC number', + }, + ], + mandatory: true, + }, + naam: { + display: [ + { + name: 'Naam', + }, + { + locale: 'nl-NL', + name: 'Naam', + }, + { + locale: 'en-US', + name: 'Name', + }, + ], + mandatory: true, + }, + rechtsvorm: { + display: [ + { + name: 'Rechtsvorm', + }, + { + locale: 'nl-NL', + name: 'Rechtsvorm', + }, + { + locale: 'en-US', + name: 'Legal form', + }, + ], + }, + startdatum: { + display: [ + { + name: 'Startdatum', + }, + { + locale: 'nl-NL', + name: 'Startdatum', + }, + { + locale: 'en-US', + name: 'Start date', + }, + ], + mandatory: true, + }, + }, + }, + credential_signing_alg_values_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + cryptographic_binding_methods_supported: ['did:web', 'did:jwk', 'did:ebsi'], + display: [ + { + background_color: '#e6f2f5', + background_image: { + alt_text: 'KvK styled card Background', + url: 'https://mijnkvk.acc.credenco.com/kvk_card_background.png', + }, + name: 'KVK Registratie', + text_color: '#00526e', + }, + { + locale: 'nl-NL', + name: 'KVK Registratie', + }, + { + locale: 'en-US', + name: 'Chamber of Commerce Registration', + }, + ], + format: 'jwt_vc_json', + }, + RSIN_jwt_vc_json: { + credential_definition: { + type: ['VerifiableCredential', 'VerifiableAttestation', 'RSIN'], + value_type: { + naam: { + display: [ + { + name: 'Naam', + }, + { + locale: 'nl-NL', + name: 'Naam', + }, + { + locale: 'en-US', + name: 'Name', + }, + ], + }, + rsin: { + display: [ + { + name: 'RSIN', + }, + { + locale: 'nl-NL', + name: 'RSIN', + }, + { + locale: 'en-US', + name: 'RSIN', + }, + ], + }, + }, + }, + credential_signing_alg_values_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + cryptographic_binding_methods_supported: ['did:web', 'did:jwk', 'did:ebsi'], + display: [ + { + background_color: '#e6f2f5', + background_image: { + alt_text: 'KvK styled card Background', + url: 'https://mijnkvk.acc.credenco.com/kvk_card_background.png', + }, + name: 'RSIN', + text_color: '#00526e', + }, + { + description: 'Rechtspersonen en Samenwerkingsverbanden Identificatienummer', + locale: 'nl-NL', + name: 'RSIN', + }, + { + description: 'Identification number for legal entities and associations', + locale: 'en-US', + name: 'RSIN', + }, + ], + format: 'jwt_vc_json', + }, + }, + credential_endpoint: 'https://mijnkvk.acc.credenco.com/credential', + credential_issuer: 'https://mijnkvk.acc.credenco.com', + credentials_supported: [ + { + credential_definition: { + type: ['VerifiableCredential', 'VerifiableAttestation', 'KVKRegistration'], + value_type: { + einddatum: { + display: [ + { + name: 'Einddatum', + }, + { + locale: 'nl-NL', + name: 'Einddatum', + }, + { + locale: 'en-US', + name: 'End date', + }, + ], + }, + kvkNummer: { + display: [ + { + name: 'KVK nummer', + }, + { + locale: 'nl-NL', + name: 'KVK nummer', + }, + { + locale: 'en-US', + name: 'CoC number', + }, + ], + mandatory: true, + }, + naam: { + display: [ + { + name: 'Naam', + }, + { + locale: 'nl-NL', + name: 'Naam', + }, + { + locale: 'en-US', + name: 'Name', + }, + ], + mandatory: true, + }, + rechtsvorm: { + display: [ + { + name: 'Rechtsvorm', + }, + { + locale: 'nl-NL', + name: 'Rechtsvorm', + }, + { + locale: 'en-US', + name: 'Legal form', + }, + ], + }, + startdatum: { + display: [ + { + name: 'Startdatum', + }, + { + locale: 'nl-NL', + name: 'Startdatum', + }, + { + locale: 'en-US', + name: 'Start date', + }, + ], + mandatory: true, + }, + }, + }, + credential_signing_alg_values_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + cryptographic_binding_methods_supported: ['did:web', 'did:jwk', 'did:ebsi'], + display: [ + { + background_color: '#e6f2f5', + background_image: { + alt_text: 'KvK styled card Background', + url: 'https://mijnkvk.acc.credenco.com/kvk_card_background.png', + }, + name: 'KVK Registratie', + text_color: '#00526e', + }, + { + locale: 'nl-NL', + name: 'KVK Registratie', + }, + { + locale: 'en-US', + name: 'Chamber of Commerce Registration', + }, + ], + format: 'jwt_vc_json', + }, + { + credential_definition: { + type: ['VerifiableCredential', 'VerifiableAttestation', 'RSIN'], + value_type: { + naam: { + display: [ + { + name: 'Naam', + }, + { + locale: 'nl-NL', + name: 'Naam', + }, + { + locale: 'en-US', + name: 'Name', + }, + ], + }, + rsin: { + display: [ + { + name: 'RSIN', + }, + { + locale: 'nl-NL', + name: 'RSIN', + }, + { + locale: 'en-US', + name: 'RSIN', + }, + ], + }, + }, + }, + credential_signing_alg_values_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + cryptographic_binding_methods_supported: ['did:web', 'did:jwk', 'did:ebsi'], + display: [ + { + background_color: '#e6f2f5', + background_image: { + alt_text: 'KvK styled card Background', + url: 'https://mijnkvk.acc.credenco.com/kvk_card_background.png', + }, + name: 'RSIN', + text_color: '#00526e', + }, + { + description: 'Rechtspersonen en Samenwerkingsverbanden Identificatienummer', + locale: 'nl-NL', + name: 'RSIN', + }, + { + description: 'Identification number for legal entities and associations', + locale: 'en-US', + name: 'RSIN', + }, + ], + format: 'jwt_vc_json', + }, + { + credential_definition: { + type: ['VerifiableCredential', 'BevoegdheidUittreksel'], + value_type: { + functie: { + display: [ + { + name: 'Functie', + }, + { + locale: 'nl-NL', + name: 'Functie', + }, + { + locale: 'en-US', + name: 'Function', + }, + ], + }, + functionarisNaam: { + display: [ + { + name: 'Functionaris naam', + }, + { + locale: 'nl-NL', + name: 'Functionaris naam', + }, + { + locale: 'en-US', + name: 'Officer name', + }, + ], + mandatory: true, + }, + kvkNummer: { + display: [ + { + name: 'KVK nummer', + }, + { + locale: 'nl-NL', + name: 'KVK nummer', + }, + { + locale: 'en-US', + name: 'CoC number', + }, + ], + mandatory: true, + }, + naam: { + display: [ + { + name: 'Naam', + }, + { + locale: 'nl-NL', + name: 'Naam', + }, + { + locale: 'en-US', + name: 'Name', + }, + ], + mandatory: true, + }, + persoonRechtsvorm: { + display: [ + { + name: 'Rechtsvorm', + }, + { + locale: 'nl-NL', + name: 'Rechtsvorm', + }, + { + locale: 'en-US', + name: 'Legal form', + }, + ], + }, + soortBevoegdheid: { + display: [ + { + name: 'Soort bevoegdheid', + }, + { + locale: 'nl-NL', + name: 'Soort bevoegdheid', + }, + { + locale: 'en-US', + name: 'Type of authority', + }, + ], + }, + }, + }, + credential_signing_alg_values_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + cryptographic_binding_methods_supported: ['did:web', 'did:jwk', 'did:ebsi'], + display: [ + { + background_color: '#e6f2f5', + background_image: { + alt_text: 'KvK styled card Background', + url: 'https://mijnkvk.acc.credenco.com/kvk_card_background.png', + }, + name: 'BevoegdheidUittreksel', + text_color: '#00526e', + }, + { + locale: 'nl-NL', + name: 'Bevoegdheid uittreksel', + }, + { + locale: 'en-US', + name: 'Power of Attorney', + }, + ], + format: 'jwt_vc_json', + }, + ], + deferred_credential_endpoint: 'https://mijnkvk.acc.credenco.com/credential_deferred', + display: [ + { + logo: { + alt_text: 'KvK logo', + url: 'https://mijnkvk.acc.credenco.com/kvk_logo.png', + }, + name: 'Kamer van Koophandel', + }, + { + locale: 'nl-NL', + name: 'Kamer van Koophandel', + }, + { + locale: 'en-US', + name: 'Chamber of Commerce', + }, + ], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + id_token_signing_alg_values_supported: ['ES256'], + issuer: 'https://mijnkvk.acc.credenco.com', + jwks_uri: 'https://mijnkvk.acc.credenco.com/jwks', + pushed_authorization_request_endpoint: 'https://mijnkvk.acc.credenco.com/par', + response_modes_supported: ['query', 'fragment'], + response_types_supported: ['code', 'vp_token', 'id_token'], + scopes_supported: ['openid'], + subject_types_supported: ['public'], + token_endpoint: 'https://mijnkvk.acc.credenco.com/token', + } as IssuerMetadataV1_0_13, + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + auth: {}, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + credential: {}, + }, }; diff --git a/packages/client/lib/functions/OpenIDUtils.ts b/packages/client/lib/functions/OpenIDUtils.ts new file mode 100644 index 00000000..8d3b3ab1 --- /dev/null +++ b/packages/client/lib/functions/OpenIDUtils.ts @@ -0,0 +1,25 @@ +import { getJson, OpenIDResponse, WellKnownEndpoints } from '@sphereon/oid4vci-common' +import Debug from 'debug'; + +const debug = Debug('sphereon:openid4vci:openid-utils'); +/** + * 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 + */ +export const retrieveWellknown = async ( + host: string, + endpointType: WellKnownEndpoints, + opts?: { errorOnNotFound?: boolean }, +): Promise> => { + const result: OpenIDResponse = 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; +} \ No newline at end of file diff --git a/packages/client/lib/index.ts b/packages/client/lib/index.ts index 38949357..629911ea 100644 --- a/packages/client/lib/index.ts +++ b/packages/client/lib/index.ts @@ -10,6 +10,7 @@ export * from './CredentialRequestClientBuilder'; export * from './CredentialRequestClientBuilderV1_0_11'; export * from './functions'; export * from './MetadataClient'; +export * from './MetadataClientV1_0_11'; export * from './OpenID4VCIClient'; export * from './OpenID4VCIClientV1_0_11'; export * from './ProofOfPossessionBuilder'; diff --git a/packages/common/lib/functions/IssuerMetadataUtils.ts b/packages/common/lib/functions/IssuerMetadataUtils.ts index 693b6451..3bbe7f8d 100644 --- a/packages/common/lib/functions/IssuerMetadataUtils.ts +++ b/packages/common/lib/functions/IssuerMetadataUtils.ts @@ -1,6 +1,6 @@ import { AuthorizationServerMetadata, - CredentialConfigurationSupported, + CredentialSupported, CredentialIssuerMetadata, CredentialSupportedTypeV1_0_08, CredentialSupportedV1_0_08, @@ -16,7 +16,7 @@ export function getSupportedCredentials(options?: { version: OpenId4VCIVersion; types?: string[][]; format?: OID4VCICredentialFormat | string | (OID4VCICredentialFormat | string)[]; -}): Record { +}): Record { if (options?.types && Array.isArray(options.types)) { return options.types .map((typeSet) => { @@ -27,7 +27,7 @@ export function getSupportedCredentials(options?: { Object.assign(acc, result); return acc; }, - {} as Record, + {} as Record, ); } @@ -39,7 +39,7 @@ export function getSupportedCredential(opts?: { version: OpenId4VCIVersion; types?: string | string[]; format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[]; -}): Record { +}): Record { const { issuerMetadata, types, format } = opts ?? {}; if (!issuerMetadata || !issuerMetadata.credential_configurations_supported) { @@ -50,7 +50,7 @@ export function getSupportedCredential(opts?: { const formats = Array.isArray(format) ? format : format ? [format] : []; const normalizedTypes = Array.isArray(types) ? types : types ? [types] : []; - const filteredConfigs: Record = {}; + const filteredConfigs: Record = {}; Object.entries(configurations).forEach(([key, value]) => { const isTypeMatch = normalizedTypes.length === 0 || normalizedTypes.includes(key); const isFormatMatch = formats.length === 0 || formats.includes(value.format); @@ -64,7 +64,7 @@ export function getSupportedCredential(opts?: { } export function getTypesFromCredentialSupported( - credentialSupported: CredentialConfigurationSupported, + credentialSupported: CredentialSupported, opts?: { filterVerifiableCredential: boolean }, ) { let types: string[] = []; @@ -88,8 +88,8 @@ export function getTypesFromCredentialSupported( return types; } -export function credentialsSupportedV8ToV13(supportedV8: CredentialSupportedTypeV1_0_08): Record { - const credentialConfigsSupported: Record = {}; +export function credentialsSupportedV8ToV13(supportedV8: CredentialSupportedTypeV1_0_08): Record { + const credentialConfigsSupported: Record = {}; Object.entries(supportedV8).flatMap((entry) => { const type = entry[0]; const supportedV8 = entry[1]; @@ -98,21 +98,21 @@ export function credentialsSupportedV8ToV13(supportedV8: CredentialSupportedType return credentialConfigsSupported; } -export function credentialSupportedV8ToV13(key: string, supportedV8: CredentialSupportedV1_0_08): Record { - const credentialConfigsSupported: Record = {}; +export function credentialSupportedV8ToV13(key: string, supportedV8: CredentialSupportedV1_0_08): Record { + const credentialConfigsSupported: Record = {}; Object.entries(supportedV8.formats).map((entry) => { const format = entry[0]; const credentialSupportBrief = entry[1]; if (typeof format !== 'string') { throw Error(`Unknown format received ${JSON.stringify(format)}`); } - const credentialConfigSupported: Partial = { + const credentialConfigSupported: Partial = { format: format as OID4VCICredentialFormat, display: supportedV8.display, ...credentialSupportBrief, credentialSubject: supportedV8.claims, }; - credentialConfigsSupported[key] = credentialConfigSupported as CredentialConfigurationSupported; + credentialConfigsSupported[key] = credentialConfigSupported as CredentialSupported; }); return credentialConfigsSupported; } diff --git a/packages/common/lib/types/Generic.types.ts b/packages/common/lib/types/Generic.types.ts index a2a72fff..026c0ebf 100644 --- a/packages/common/lib/types/Generic.types.ts +++ b/packages/common/lib/types/Generic.types.ts @@ -52,7 +52,7 @@ export interface CredentialSupplierConfig { export interface CredentialIssuerMetadataOpts { credential_endpoint?: string; // REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. batch_credential_endpoint?: string; // OPTIONAL. URL of the Credential Issuer's Batch Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. If omitted, the Credential Issuer does not support the Batch Credential Endpoint. - credentials_supported: CredentialConfigurationSupported[]; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. + credentials_supported: CredentialSupported[]; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. credential_issuer: string; // REQUIRED. The Credential Issuer's identifier. authorization_server?: string; // OPTIONAL. Identifier of the OAuth 2.0 Authorization Server (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. token_endpoint?: string; @@ -93,7 +93,7 @@ export interface ResponseEncryption { export interface CredentialIssuerMetadata extends CredentialIssuerMetadataOpts, Partial { authorization_servers?: string[]; // OPTIONAL. Array of strings that identify the OAuth 2.0 Authorization Servers (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. credential_endpoint: string; // REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. - credential_configurations_supported: Record; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. + credential_configurations_supported: Record; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. credential_issuer: string; // REQUIRED. The Credential Issuer's identifier. credential_response_encryption_alg_values_supported?: string; // OPTIONAL. Array containing a list of the JWE [RFC7516] encryption algorithms (alg values) [RFC7518] supported by the Credential and/or Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. credential_response_encryption_enc_values_supported?: string; //OPTIONAL. Array containing a list of the JWE [RFC7516] encryption algorithms (enc values) [RFC7518] supported by the Credential and/or Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. @@ -148,7 +148,7 @@ export interface CredentialSupportedSdJwtVc extends CommonCredentialSupported { order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. } -export type CredentialConfigurationSupported = CommonCredentialSupported & +export type CredentialSupported = CommonCredentialSupported & (CredentialSupportedJwtVcJson | CredentialSupportedJwtVcJsonLdAndLdpVc | CredentialSupportedSdJwtVc); export interface CommonCredentialOfferFormat { diff --git a/packages/common/lib/types/ServerMetadata.ts b/packages/common/lib/types/ServerMetadata.ts index 190b0031..aaeceaab 100644 --- a/packages/common/lib/types/ServerMetadata.ts +++ b/packages/common/lib/types/ServerMetadata.ts @@ -1,6 +1,3 @@ -import { CredentialIssuerMetadata } from './Generic.types'; -import { IssuerMetadataV1_0_08 } from './v1_0_08.types'; - export interface AuthorizationServerMetadata { issuer: string; authorization_endpoint?: string; @@ -71,10 +68,3 @@ export interface EndpointMetadata { authorization_server?: string; authorization_endpoint?: string; // Can be undefined in pre-auth flow } -export interface EndpointMetadataResult extends EndpointMetadata { - // The EndpointMetadata are snake-case so they can easily be used in payloads/JSON. - // The values below should not end up in requests/responses directly, so they are using our normal CamelCase convention - authorizationServerType: AuthorizationServerType; - authorizationServerMetadata?: AuthorizationServerMetadata; - credentialIssuerMetadata?: Partial & (CredentialIssuerMetadata | IssuerMetadataV1_0_08); -} diff --git a/packages/common/lib/types/v1_0_11.types.ts b/packages/common/lib/types/v1_0_11.types.ts index c77f9242..b3ef8726 100644 --- a/packages/common/lib/types/v1_0_11.types.ts +++ b/packages/common/lib/types/v1_0_11.types.ts @@ -11,7 +11,8 @@ import { Grant, } from './Generic.types'; import { QRCodeOpts } from './QRCode.types'; -import { AuthorizationServerMetadata, EndpointMetadata } from './ServerMetadata'; +import { AuthorizationServerMetadata, AuthorizationServerType, EndpointMetadata } from './ServerMetadata'; +import { IssuerMetadataV1_0_08 } from './v1_0_08.types'; export interface AccessTokenRequestOptsV1_0_11 { credentialOffer?: UniformCredentialOffer; @@ -82,8 +83,12 @@ export type CredentialRequestV1_0_11 = CommonCredentialRequest & (CredentialRequestJwtVcJson | CredentialRequestJwtVcJsonLdAndLdpVc | CredentialRequestSdJwtVc); export interface CredentialIssuerMetadataV1_0_11 extends CredentialIssuerMetadataOpts, Partial { + authorization_servers?: string[]; // OPTIONAL. Array of strings that identify the OAuth 2.0 Authorization Servers (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. credential_endpoint: string; // REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. - authorization_server?: string; + credential_response_encryption_alg_values_supported?: string; // OPTIONAL. Array containing a list of the JWE [RFC7516] encryption algorithms (alg values) [RFC7518] supported by the Credential and/or Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. + credential_response_encryption_enc_values_supported?: string; //OPTIONAL. Array containing a list of the JWE [RFC7516] encryption algorithms (enc values) [RFC7518] supported by the Credential and/or Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. + require_credential_response_encryption?: boolean; //OPTIONAL. Boolean value specifying whether the Credential Issuer requires additional encryption on top of TLS for the Credential Response and expects encryption parameters to be present in the Credential Request and/or Batch Credential Request, with true indicating support. When the value is true, credential_response_encryption_alg_values_supported parameter MUST also be provided. If omitted, the default value is false. + credential_identifiers_supported?: boolean; // OPTIONAL. Boolean value specifying whether the Credential Issuer supports returning credential_identifiers parameter in the authorization_details Token Response parameter, with true indicating support. If omitted, the default value is false. } export interface AuthorizationRequestV1_0_11 extends AuthorizationDetailsJwtVcJson, AuthorizationDetailsJwtVcJson { @@ -94,3 +99,11 @@ export interface AuthorizationRequestV1_0_11 extends AuthorizationDetailsJwtVcJs export function isAuthorizationRequestV1_0_11(request: CommonAuthorizationRequest): boolean { return request && 'issuer_state' in request; } + +export interface EndpointMetadataResultV1_0_11 extends EndpointMetadata { + // The EndpointMetadata are snake-case so they can easily be used in payloads/JSON. + // The values below should not end up in requests/responses directly, so they are using our normal CamelCase convention + authorizationServerType: AuthorizationServerType; + authorizationServerMetadata?: AuthorizationServerMetadata; + credentialIssuerMetadata?: Partial & IssuerMetadataV1_0_08; +} diff --git a/packages/common/lib/types/v1_0_13.types.ts b/packages/common/lib/types/v1_0_13.types.ts index 7cf45389..f57ef63e 100644 --- a/packages/common/lib/types/v1_0_13.types.ts +++ b/packages/common/lib/types/v1_0_13.types.ts @@ -1,33 +1,58 @@ import { CommonCredentialRequest, - CredentialConfigurationSupported, CredentialDataSupplierInput, CredentialRequestJwtVcJson, CredentialRequestJwtVcJsonLdAndLdpVc, CredentialRequestSdJwtVc, + CredentialsSupportedDisplay, CredentialSupplierConfig, Grant, + IssuerCredentialSubject, + KeyProofType, MetadataDisplay, - NameAndLocale, + OID4VCICredentialFormat, + ProofType, ResponseEncryption, } from './Generic.types'; import { QRCodeOpts } from './QRCode.types'; +import { AuthorizationServerMetadata, AuthorizationServerType, EndpointMetadata } from './ServerMetadata'; export interface IssuerMetadataV1_0_13 { issuer?: string; + credential_configurations_supported: Record; // REQUIRED. A JSON object containing a list of key value pairs, where the key is a string serving as an abstract identifier of the Credential. This identifier is RECOMMENDED to be collision resistant - it can be globally unique, but does not have to be when naming conflicts are unlikely to arise in a given use case. The value is a JSON object. The JSON object MUST conform to the structure of the Section 11.2.1. + credential_issuer: string; // A Credential Issuer is identified by a case sensitive URL using the https scheme that contains scheme, host and, optionally, port number and path components, but no query or fragment components. credential_endpoint: string; // REQUIRED. URL of the OP's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. - credential_configurations_supported: CredentialConfigurationSupported; // REQUIRED. A JSON object containing a list of key value pairs, where the key is a string serving as an abstract identifier of the Credential. This identifier is RECOMMENDED to be collision resistant - it can be globally unique, but does not have to be when naming conflicts are unlikely to arise in a given use case. The value is a JSON object. The JSON object MUST conform to the structure of the Section 11.2.1. - credential_issuer?: { - // OPTIONAL. A JSON object containing display properties for the Credential issuer. - display: NameAndLocale | NameAndLocale[]; // OPTIONAL. An array of objects, where each object contains display properties of a Credential issuer for a certain language. Below is a non-exhaustive list of valid parameters that MAY be included: - }; authorization_servers?: string[]; + batch_credential_endpoint?: string; + deferred_credential_endpoint?: string; + notification_endpoint?: string; + credential_response_encryption?: ResponseEncryption; token_endpoint?: string; display?: MetadataDisplay[]; [x: string]: unknown; } +export type CredentialDefinitionV1_0_13 = { + type: string[]; + credentialSubject?: IssuerCredentialSubject; +}; + +export type CredentialConfigurationSupportedV1_0_13 = { + credential_definition: CredentialDefinitionV1_0_13; + /** + * TODO: These two (vct and id) are solely added because of backward compatibility with sd-jwt. as soons as we have a clear understanding of the new sd-jwt issuer protocol we can remove this + */ + vct?: string; + id?: string; + format: OID4VCICredentialFormat; //REQUIRED. A JSON string identifying the format of this credential, e.g. jwt_vc_json or ldp_vc. + scope?: string; // OPTIONAL. A JSON string identifying the scope value that this Credential Issuer supports for this particular Credential. The value can be the same across multiple credential_configurations_supported objects. The Authorization Server MUST be able to uniquely identify the Credential Issuer based on the scope value. The Wallet can use this value in the Authorization Request as defined in Section 5.1.2. Scope values in this Credential Issuer metadata MAY duplicate those in the scopes_supported parameter of the Authorization Server. + cryptographic_binding_methods_supported?: string[]; + credential_signing_alg_values_supported?: string[]; + proof_types_supported?: Record; + display?: CredentialsSupportedDisplay[]; // OPTIONAL. An array of objects, where each object contains the display properties of the supported credential for a certain language +}; + export type CredentialRequestV1_0_13 = CommonCredentialRequest & (CredentialRequestJwtVcJson | CredentialRequestJwtVcJsonLdAndLdpVc | CredentialRequestSdJwtVc); @@ -83,7 +108,7 @@ export interface CredentialIssuerMetadataOptsV1_0_13 { notification_endpoint?: string; // OPTIONAL. URL of the Credential Issuer's Notification Endpoint, as defined in Section 10. This URL MUST use the https scheme and MAY contain port, path, and query parameter components. If omitted, the Credential Issuer does not support the Notification Endpoint. credential_response_encryption?: ResponseEncryption; // OPTIONAL. Object containing information about whether the Credential Issuer supports encryption of the Credential and Batch Credential Response on top of TLS. credential_identifiers_supported?: boolean; // OPTIONAL. Boolean value specifying whether the Credential Issuer supports returning credential_identifiers parameter in the authorization_details Token Response parameter, with true indicating support. If omitted, the default value is false. - credential_configurations_supported?: Record; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. + credential_configurations_supported: Record; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. credential_issuer: string; // REQUIRED. The Credential Issuer's identifier. authorization_servers?: string[]; // OPTIONAL. Array of strings that identify the OAuth 2.0 Authorization Servers (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. signed_metadata?: string; // OPTIONAL. String that is a signed JWT. This JWT contains Credential Issuer metadata parameters as claims. @@ -93,3 +118,23 @@ export interface CredentialIssuerMetadataOptsV1_0_13 { token_endpoint?: string; credential_supplier_config?: CredentialSupplierConfig; } + +export interface EndpointMetadataResultV1_0_13 extends EndpointMetadata { + // The EndpointMetadata are snake-case so they can easily be used in payloads/JSON. + // The values below should not end up in requests/responses directly, so they are using our normal CamelCase convention + authorizationServerType: AuthorizationServerType; + authorizationServerMetadata?: AuthorizationServerMetadata; + credentialIssuerMetadata?: Partial & IssuerMetadataV1_0_13; +} + +// For now we extend the opts above. Only difference is that the credential endpoint is optional in the Opts, as it can come from other sources. The value is however required in the eventual Issuer Metadata +export interface CredentialIssuerMetadataV1_0_13 extends CredentialIssuerMetadataOptsV1_0_13, Partial { + authorization_servers?: string[]; // OPTIONAL. Array of strings that identify the OAuth 2.0 Authorization Servers (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. + credential_endpoint: string; // REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. + credential_configurations_supported: Record; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. + credential_issuer: string; // REQUIRED. The Credential Issuer's identifier. + credential_response_encryption_alg_values_supported?: string; // OPTIONAL. Array containing a list of the JWE [RFC7516] encryption algorithms (alg values) [RFC7518] supported by the Credential and/or Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. + credential_response_encryption_enc_values_supported?: string; //OPTIONAL. Array containing a list of the JWE [RFC7516] encryption algorithms (enc values) [RFC7518] supported by the Credential and/or Batch Credential Endpoint to encode the Credential or Batch Credential Response in a JWT [RFC7519]. + require_credential_response_encryption?: boolean; //OPTIONAL. Boolean value specifying whether the Credential Issuer requires additional encryption on top of TLS for the Credential Response and expects encryption parameters to be present in the Credential Request and/or Batch Credential Request, with true indicating support. When the value is true, credential_response_encryption_alg_values_supported parameter MUST also be provided. If omitted, the default value is false. + credential_identifiers_supported?: boolean; // OPTIONAL. Boolean value specifying whether the Credential Issuer supports returning credential_identifiers parameter in the authorization_details Token Response parameter, with true indicating support. If omitted, the default value is false. +} diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index 6586c010..d4127e8f 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -3,7 +3,7 @@ import process from 'process' import { AuthorizationRequest, - CredentialConfigurationSupported, + CredentialConfigurationSupportedV1_0_13, IssuerCredentialSubjectDisplay, OID4VCICredentialFormat, QRCodeOpts, @@ -25,12 +25,16 @@ import { } from './oid4vci-api-functions' function buildVCIFromEnvironment() { - const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported(process.env.cryptographic_suites_supported as string) + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported(process.env.credential_signing_alg_values_supported as string) .withCryptographicBindingMethod(process.env.cryptographic_binding_methods_supported as string) .withFormat(process.env.credential_supported_format as unknown as OID4VCICredentialFormat) - .withId(process.env.credential_supported_id as string) - .withTypes([process.env.credential_supported_types_1 as string, process.env.credential_supported_types_2 as string]) + .withCredentialName(process.env.credential_supported_name_1 as string) + .withCredentialDefinition({ + type: [process.env.credential_supported_1_definition_type_1 as string, process.env.credential_supported_1_definition_type_2 as string], + // TODO: setup credentialSubject here from env + // credentialSubject + }) .withCredentialSupportedDisplay({ name: process.env.credential_display_name as string, locale: process.env.credential_display_locale as string, diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index 8eccf424..7b7b0e8b 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -5,7 +5,8 @@ import { OpenID4VCIClient } from '@sphereon/oid4vci-client' import { AccessTokenResponse, Alg, - CredentialConfigurationSupported, + CredentialSupported, + CredentialConfigurationSupportedV1_0_13, CredentialOfferSession, IssuerCredentialSubjectDisplay, Jwt, @@ -69,12 +70,14 @@ describe('VcIssuer', () => { return new jose.SignJWT({ ...jwt.payload }).setProtectedHeader({ ...jwt.header, alg: Alg.ES256 }).sign(privateKey) } - const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported('ES256K') + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported('ES256K') .withCryptographicBindingMethod('did') - .withTypes('VerifiableCredential') .withFormat('jwt_vc_json') - .withId('UniversityDegree_JWT') + .withCredentialName('UniversityDegree_JWT') + .withCredentialDefinition({ + type: ['VerifiableCredential', 'UniversityDegree_JWT'], + }) .withCredentialSupportedDisplay({ name: 'University Credential', locale: 'en-US', @@ -196,7 +199,7 @@ describe('VcIssuer', () => { }, }, }, - credentials: { UniversityDegree_JWT: { format: 'ldp_vc', id: 'UniversityDegree_JWT' } as CredentialConfigurationSupported }, + credentials: { UniversityDegree_JWT: { format: 'ldp_vc', id: 'UniversityDegree_JWT' } as CredentialSupported }, scheme: 'http', }) .then((response) => response.uri) @@ -327,7 +330,17 @@ describe('VcIssuer', () => { .setExpirationTime('2h') .sign(subjectKeypair.privateKey) } - + client = await OpenID4VCIClient.fromURI({ + uri: `http://localhost:3456/test?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22previously-created-state%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22testcode%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegree_JWT%22%5D%2C%22credential_issuer%22%3A%22http%3A%2F%2Flocalhost%3A3456%2Ftest%22%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegree_JWT%22%5D%7D`, + kid: subjectDIDKey.didDocument.authentication[0], + alg: 'ES256', + createAuthorizationRequestURL: false, + }) + console.log('getting access token') + accessToken = await client.acquireAccessToken({ + pin: 'testcode', + }) + console.log(`access token: ${accessToken}`) const credentialResponse = await client.acquireCredentials({ credentialTypes: ['VerifiableCredential'], format: 'jwt_vc_json', diff --git a/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts b/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts index ea955f80..ea755b8f 100644 --- a/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts +++ b/packages/issuer-rest/lib/__tests__/IssuerTokenServer.spec.ts @@ -97,21 +97,22 @@ describe('OID4VCIServer', () => { display: [{ name: 'example issuer', locale: 'en-US' }], credential_configurations_supported: { UniversityDegree_JWT: { - format: 'jwt_vc_json', - types: ['VerifiableCredential', 'UniversityDegreeCredential'], - credentialSubject: { - given_name: { - display: [ - { - name: 'given name', - locale: 'en-US', - }, - ], + credential_definition: { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + credentialSubject: { + given_name: { + display: [ + { + name: 'given name', + locale: 'en-US', + }, + ], + }, }, }, - cryptographic_suites_supported: ['ES256K'], + format: 'jwt_vc_json', + credential_signing_alg_values_supported: ['ES256K'], cryptographic_binding_methods_supported: ['did'], - id: 'UniversityDegree_JWT', display: [ { name: 'University Credential', diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 9f179fd4..70425bbc 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -4,7 +4,8 @@ import { CNonceState, CreateCredentialOfferURIResult, CREDENTIAL_MISSING_ERROR, - CredentialConfigurationSupported, + CredentialSupported, + CredentialConfigurationSupportedV1_0_13, CredentialDataSupplierInput, CredentialOfferPayloadV1_0_13, CredentialOfferSession, @@ -88,7 +89,7 @@ export class VcIssuer { public async createCredentialOfferURI(opts: { grants?: Grant - credentials?: Record + credentials?: Record credentialDefinition?: JsonLdIssuerCredentialDefinition credentialOfferUri?: string credentialDataSupplierInput?: CredentialDataSupplierInput // Optional storage that can help the credential Data Supplier. For instance to store credential input data during offer creation, if no additional data can be supplied later on @@ -533,7 +534,7 @@ export class VcIssuer { return false } for (const credentialSupported of Object.values( - this._issuerMetadata['credential_configurations_supported'] as Record, + this._issuerMetadata['credential_configurations_supported'] as Record, )) { if (!Array.isArray(requestFormat) && credentialSupported.format === requestFormat) { return true diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index c10cd372..3ca64607 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -2,7 +2,8 @@ import { OpenID4VCIClient } from '@sphereon/oid4vci-client' import { Alg, ALG_ERROR, - CredentialConfigurationSupported, + CredentialSupported, + CredentialConfigurationSupportedV1_0_13, CredentialOfferSession, IssuerCredentialSubjectDisplay, IssueStatus, @@ -42,12 +43,14 @@ describe('VcIssuer', () => { beforeEach(async () => { jest.clearAllMocks() - const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported('ES256K') + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') - .withTypes('VerifiableCredential') - .withId('UniversityDegree_JWT') + .withCredentialName('UniversityDegree_JWT') + .withCredentialDefinition({ + type: ['VerifiableCredential', 'UniversityDegree_JWT'], + }) .withCredentialSupportedDisplay({ name: 'University Credential', locale: 'en-US', @@ -265,7 +268,7 @@ describe('VcIssuer', () => { credentials: { Credential: { format: 'ldp_vc', - } as CredentialConfigurationSupported, + } as CredentialSupported, }, credentialOfferUri: 'https://somehost.com/offer-id', }) diff --git a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts index 24f79993..0c314864 100644 --- a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts @@ -1,15 +1,15 @@ -import { CredentialConfigurationSupported, IssuerCredentialSubjectDisplay, IssueStatus, TokenErrorResponse } from '@sphereon/oid4vci-common' +import { CredentialConfigurationSupportedV1_0_13, IssuerCredentialSubjectDisplay, IssueStatus, TokenErrorResponse } from '@sphereon/oid4vci-common' import { v4 } from 'uuid' import { CredentialSupportedBuilderV1_13, VcIssuerBuilder } from '../index' describe('VcIssuer builder should', () => { it('generate a VcIssuer', () => { - const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported('ES256K') + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') - .withId('UniversityDegree_JWT') + .withCredentialName('UniversityDegree_JWT') .withCredentialSupportedDisplay({ name: 'University Credential', locale: 'en-US', @@ -20,7 +20,9 @@ describe('VcIssuer builder should', () => { background_color: '#12107c', text_color: '#FFFFFF', }) - .withTypes('VerifiableCredential') + .withCredentialDefinition({ + type: ['UniversityDegree_JWT'], + }) .addCredentialSubjectPropertyDisplay('given_name', { name: 'given name', locale: 'en-US', @@ -45,12 +47,14 @@ describe('VcIssuer builder should', () => { }) it('fail to generate a VcIssuer', () => { - const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported('ES256K') + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') - .withTypes('VerifiableCredential') - .withId('UniversityDegree_JWT') + .withCredentialName('UniversityDegree_JWT') + .withCredentialDefinition({ + type: ['VerifiableCredential', 'UniversityDegree_JWT'], + }) .withCredentialSupportedDisplay({ name: 'University Credential', locale: 'en-US', @@ -82,19 +86,21 @@ describe('VcIssuer builder should', () => { it('fail to generate a CredentialSupportedV1_11', () => { expect(() => new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported('ES256K') + .withCredentialSigningAlgValuesSupported('ES256K') .withCryptographicBindingMethod('did') - .withId('UniversityDegree_JWT') + .withCredentialName('UniversityDegree_JWT') .build(), ).toThrowError(TokenErrorResponse.invalid_request) }) it('should successfully attach an instance of the ICredentialOfferStateManager to the VcIssuer instance', async () => { - const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported('ES256K') + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') - .withTypes('VerifiableCredential') - .withId('UniversityDegree_JWT') + .withCredentialName('UniversityDegree_JWT') + .withCredentialDefinition({ + type: ['VerifiableCredential', 'UniversityDegree_JWT'], + }) .withCredentialSupportedDisplay({ name: 'University Credential', locale: 'en-US', diff --git a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_13.ts b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_13.ts index c1d0a7c0..cbf732d7 100644 --- a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_13.ts +++ b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_13.ts @@ -1,20 +1,23 @@ import { - CredentialConfigurationSupported, + CredentialConfigurationSupportedV1_0_13, + CredentialDefinitionV1_0_13, CredentialsSupportedDisplay, - isFormat, - isNotFormat, IssuerCredentialSubject, IssuerCredentialSubjectDisplay, + KeyProofType, OID4VCICredentialFormat, + ProofType, TokenErrorResponse, } from '@sphereon/oid4vci-common' export class CredentialSupportedBuilderV1_13 { format?: OID4VCICredentialFormat - id?: string - types?: string[] + scope?: string + credentialName?: string + credentialDefinition?: CredentialDefinitionV1_0_13 cryptographicBindingMethodsSupported?: ('jwk' | 'cose_key' | 'did' | string)[] - cryptographicSuitesSupported?: ('jwt_vc' | 'ldp_vc' | string)[] + credentialSigningAlgValuesSupported?: string[] + proofTypesSupported?: Record display?: CredentialsSupportedDisplay[] credentialSubject?: IssuerCredentialSubject @@ -23,30 +26,23 @@ export class CredentialSupportedBuilderV1_13 { return this } - withId(id: string): CredentialSupportedBuilderV1_13 { - this.id = id + withCredentialName(credentialName: string): CredentialSupportedBuilderV1_13 { + this.credentialName = credentialName return this } - addTypes(type: string | string[]): CredentialSupportedBuilderV1_13 { - if (!Array.isArray(type)) { - this.types = this.types ? [...this.types, type] : [type] - } else { - this.cryptographicBindingMethodsSupported = this.cryptographicBindingMethodsSupported - ? [...this.cryptographicBindingMethodsSupported, ...type] - : type + withCredentialDefinition(credentialDefinition: CredentialDefinitionV1_0_13): CredentialSupportedBuilderV1_13 { + if (!credentialDefinition.type) { + throw new Error('credentialDefinition should contain a type array') } + this.credentialDefinition = credentialDefinition return this } - withTypes(type: string | string[]): CredentialSupportedBuilderV1_13 { - if (this.format === 'vc+sd-jwt' && Array.isArray(type) && type.length > 1) { - throw new Error('Only one type is allowed for vc+sd-jwt') - } - this.types = Array.isArray(type) ? type : [type] + withScope(scope: string): CredentialSupportedBuilderV1_13 { + this.scope = scope return this } - addCryptographicBindingMethod(method: string | string[]): CredentialSupportedBuilderV1_13 { if (!Array.isArray(method)) { this.cryptographicBindingMethodsSupported = this.cryptographicBindingMethodsSupported @@ -65,17 +61,34 @@ export class CredentialSupportedBuilderV1_13 { return this } - addCryptographicSuitesSupported(suit: string | string[]): CredentialSupportedBuilderV1_13 { - if (!Array.isArray(suit)) { - this.cryptographicSuitesSupported = this.cryptographicSuitesSupported ? [...this.cryptographicSuitesSupported, suit] : [suit] + addCredentialSigningAlgValuesSupported(algValues: string | string[]): CredentialSupportedBuilderV1_13 { + if (!Array.isArray(algValues)) { + this.credentialSigningAlgValuesSupported = this.credentialSigningAlgValuesSupported + ? [...this.credentialSigningAlgValuesSupported, algValues] + : [algValues] } else { - this.cryptographicSuitesSupported = this.cryptographicSuitesSupported ? [...this.cryptographicSuitesSupported, ...suit] : suit + this.credentialSigningAlgValuesSupported = this.credentialSigningAlgValuesSupported + ? [...this.credentialSigningAlgValuesSupported, ...algValues] + : algValues } return this } - withCryptographicSuitesSupported(suit: string | string[]): CredentialSupportedBuilderV1_13 { - this.cryptographicSuitesSupported = Array.isArray(suit) ? suit : [suit] + withCredentialSigningAlgValuesSupported(algValues: string | string[]): CredentialSupportedBuilderV1_13 { + this.credentialSigningAlgValuesSupported = Array.isArray(algValues) ? algValues : [algValues] + return this + } + + addProofTypesSupported(keyProofType: KeyProofType, proofType: ProofType): CredentialSupportedBuilderV1_13 { + if (!this.proofTypesSupported) { + this.proofTypesSupported = {} as Record + } + this.proofTypesSupported[keyProofType] = proofType + return this + } + + withProofTypesSupported(proofTypesSupported: Record): CredentialSupportedBuilderV1_13 { + this.proofTypesSupported = proofTypesSupported return this } @@ -93,7 +106,7 @@ export class CredentialSupportedBuilderV1_13 { return this } - withCredentialSubjectDisplay(credentialSubject: IssuerCredentialSubject) { + withCredentialSubject(credentialSubject: IssuerCredentialSubject) { this.credentialSubject = credentialSubject return this } @@ -109,17 +122,27 @@ export class CredentialSupportedBuilderV1_13 { return this } - public build(): Record { + public build(): Record { if (!this.format) { throw new Error(TokenErrorResponse.invalid_request) } - const credentialSupported: Partial = { + + const credentialSupported: CredentialConfigurationSupportedV1_0_13 = { format: this.format, + } as CredentialConfigurationSupportedV1_0_13 + + if (!this.credentialDefinition) { + throw new Error('credentialDefinition is required') } - if (!this.types) { - throw new Error('types are required') + credentialSupported.credential_definition = this.credentialDefinition + if (this.scope) { + credentialSupported.scope = this.scope } - + if (!this.credentialName) { + throw new Error('A unique credential name is required') + } + //TODO: right now commented out all the special handlings for sd-jwt + /* // SdJwtVc has a different format if (isFormat(credentialSupported, 'vc+sd-jwt')) { if (this.types.length > 1) { @@ -134,23 +157,21 @@ export class CredentialSupportedBuilderV1_13 { if (this.credentialSubject) { credentialSupported.credentialSubject = this.credentialSubject } - } + }*/ - if (this.cryptographicSuitesSupported) { - credentialSupported.credential_signing_alg_values_supported = this.cryptographicSuitesSupported + if (this.credentialSigningAlgValuesSupported) { + credentialSupported.credential_signing_alg_values_supported = this.credentialSigningAlgValuesSupported } if (this.cryptographicBindingMethodsSupported) { credentialSupported.cryptographic_binding_methods_supported = this.cryptographicBindingMethodsSupported } - if (this.id) { - credentialSupported.id = this.id - } if (this.display) { credentialSupported.display = this.display } - const supportedConfiguration: Record = {} - //TODO: is this the correct approach here? since the id is not mandatory, I'm relying on a none-unique value - supportedConfiguration[this.id ?? this.format] = credentialSupported as CredentialConfigurationSupported + + const supportedConfiguration: Record = {} + supportedConfiguration[this.credentialName] = credentialSupported as CredentialConfigurationSupportedV1_0_13 + return supportedConfiguration } } diff --git a/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts b/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts index 0a87e642..7be368c4 100644 --- a/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts +++ b/packages/issuer/lib/builder/IssuerMetadataBuilderV1_13.ts @@ -1,4 +1,4 @@ -import { CredentialConfigurationSupported, CredentialIssuerMetadata, MetadataDisplay } from '@sphereon/oid4vci-common' +import { CredentialConfigurationSupportedV1_0_13, IssuerMetadataV1_0_13, MetadataDisplay } from '@sphereon/oid4vci-common' import { CredentialSupportedBuilderV1_13 } from './CredentialSupportedBuilderV1_13' import { DisplayBuilder } from './DisplayBuilder' @@ -7,7 +7,7 @@ export class IssuerMetadataBuilderV1_13 { credentialEndpoint: string | undefined credentialIssuer: string | undefined supportedBuilders: CredentialSupportedBuilderV1_13[] = [] - credentialConfigurationsSupported: Record = {} + credentialConfigurationsSupported: Record = {} displayBuilders: DisplayBuilder[] = [] display: MetadataDisplay[] = [] batchCredentialEndpoint?: string @@ -58,7 +58,7 @@ export class IssuerMetadataBuilderV1_13 { return this } - public addCredentialConfigurationsSupported(id: string, supportedCredential: CredentialConfigurationSupported) { + public addCredentialConfigurationsSupported(id: string, supportedCredential: CredentialConfigurationSupportedV1_0_13) { this.credentialConfigurationsSupported[id] = supportedCredential return this } @@ -82,14 +82,16 @@ export class IssuerMetadataBuilderV1_13 { return builder } - public build(): CredentialIssuerMetadata { + public build(): IssuerMetadataV1_0_13 { if (!this.credentialIssuer) { throw Error('No credential issuer supplied') } else if (!this.credentialEndpoint) { throw Error('No credential endpoint supplied') } - const credential_configurations_supported: Record = this.credentialConfigurationsSupported - const configurationsEntryList: Record[] = this.supportedBuilders.map((builder) => builder.build()) + const credential_configurations_supported: Record = this.credentialConfigurationsSupported + const configurationsEntryList: Record[] = this.supportedBuilders.map((builder) => + builder.build(), + ) configurationsEntryList.forEach((configRecord) => { Object.keys(configRecord).forEach((key) => { credential_configurations_supported[key] = configRecord[key] @@ -111,6 +113,6 @@ export class IssuerMetadataBuilderV1_13 { ...(this.authorizationServers && { authorization_servers: this.authorizationServers }), ...(this.tokenEndpoint && { token_endpoint: this.tokenEndpoint }), ...(display.length > 0 && { display }), - } as CredentialIssuerMetadata + } as IssuerMetadataV1_0_13 } } diff --git a/packages/issuer/lib/builder/VcIssuerBuilder.ts b/packages/issuer/lib/builder/VcIssuerBuilder.ts index 83b8abac..2c70e09e 100644 --- a/packages/issuer/lib/builder/VcIssuerBuilder.ts +++ b/packages/issuer/lib/builder/VcIssuerBuilder.ts @@ -1,8 +1,8 @@ import { CNonceState, - CredentialConfigurationSupported, - CredentialIssuerMetadata, + CredentialConfigurationSupportedV1_0_13, CredentialOfferSession, + IssuerMetadataV1_0_13, IStateManager, JWTVerifyCallback, MetadataDisplay, @@ -32,7 +32,7 @@ export class VcIssuerBuilder { jwtVerifyCallback?: JWTVerifyCallback credentialDataSupplier?: CredentialDataSupplier - public withIssuerMetadata(issuerMetadata: CredentialIssuerMetadata) { + public withIssuerMetadata(issuerMetadata: IssuerMetadataV1_0_13) { this.issuerMetadata = issuerMetadata return this } @@ -83,12 +83,12 @@ export class VcIssuerBuilder { return this } - public withCredentialConfigurationsSupported(credentialConfigurationsSupported: Record) { + public withCredentialConfigurationsSupported(credentialConfigurationsSupported: Record) { this.issuerMetadata.credential_configurations_supported = credentialConfigurationsSupported return this } - public addCredentialConfigurationsSupported(id: string, supportedCredential: CredentialConfigurationSupported) { + public addCredentialConfigurationsSupported(id: string, supportedCredential: CredentialConfigurationSupportedV1_0_13) { if (!this.issuerMetadata.credential_configurations_supported) { this.issuerMetadata.credential_configurations_supported = {} } @@ -160,14 +160,14 @@ export class VcIssuerBuilder { } const builder = this.issuerMetadataBuilder?.build() - const metadata: Partial = { ...this.issuerMetadata, ...builder } + const metadata: Partial = { ...this.issuerMetadata, ...builder } // Let's make sure these get merged correctly: metadata.credential_configurations_supported = this.issuerMetadata.credential_configurations_supported metadata.display = [...(this.issuerMetadata.display ?? []), ...(builder?.display ?? [])] if (!metadata.credential_endpoint || !metadata.credential_issuer || !this.issuerMetadata.credential_configurations_supported) { throw new Error(TokenErrorResponse.invalid_request) } - return new VcIssuer(metadata as CredentialIssuerMetadata, { + return new VcIssuer(metadata as IssuerMetadataV1_0_13, { //TODO: discuss this with Niels. I did not find this in the spec. but I think we should somehow communicate this ...(this.txCode && { txCode: this.txCode }), defaultCredentialOfferBaseUri: this.defaultCredentialOfferBaseUri, diff --git a/packages/issuer/lib/functions/CredentialOfferUtils.ts b/packages/issuer/lib/functions/CredentialOfferUtils.ts index 4ed7bc08..81bf98ce 100644 --- a/packages/issuer/lib/functions/CredentialOfferUtils.ts +++ b/packages/issuer/lib/functions/CredentialOfferUtils.ts @@ -1,10 +1,10 @@ import { - CredentialIssuerMetadata, CredentialIssuerMetadataOptsV1_0_13, CredentialOfferPayloadV1_0_13, CredentialOfferSession, CredentialOfferV1_0_13, Grant, + IssuerMetadataV1_0_13, PIN_VALIDATION_ERROR, TxCode, UniformCredentialOffer, @@ -119,7 +119,7 @@ export function createCredentialOfferURIFromObject( } export function createCredentialOfferURI( - issuerMetadata?: CredentialIssuerMetadata, + issuerMetadata?: IssuerMetadataV1_0_13, // todo: probably it's wise to create another builder for CredentialOfferPayload that will generate different kinds of CredentialOfferPayload opts?: { state?: string diff --git a/packages/issuer/lib/tokens/index.ts b/packages/issuer/lib/tokens/index.ts index a051a3bc..e692c43b 100644 --- a/packages/issuer/lib/tokens/index.ts +++ b/packages/issuer/lib/tokens/index.ts @@ -83,11 +83,11 @@ export const assertValidAccessTokenRequest = async ( } // Pre-auth flow - if (!request[PRE_AUTH_CODE_LITERAL]) { + if (!request['pre-authorized_code']) { throw new TokenError(400, TokenErrorResponse.invalid_request, PRE_AUTHORIZED_CODE_REQUIRED_ERROR) } - const credentialOfferSession = await credentialOfferSessions.getAsserted(request[PRE_AUTH_CODE_LITERAL]) + const credentialOfferSession = await credentialOfferSessions.getAsserted(request['pre-authorized_code']) credentialOfferSession.status = IssueStatus.ACCESS_TOKEN_REQUESTED credentialOfferSession.lastUpdatedAt = +new Date() await credentialOfferSessions.set(request[PRE_AUTH_CODE_LITERAL], credentialOfferSession) @@ -98,7 +98,12 @@ export const assertValidAccessTokenRequest = async ( invalid_request: the Authorization Server expects a PIN in the pre-authorized flow but the client does not provide a PIN */ - if (credentialOfferSession.credentialOffer.credential_offer?.grants?.[GrantTypes.PRE_AUTHORIZED_CODE]?.user_pin_required && !request.user_pin) { + if ( + credentialOfferSession.credentialOffer.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.[ + 'pre-authorized_code' + ] && + !request.user_pin + ) { throw new TokenError(400, TokenErrorResponse.invalid_request, USER_PIN_REQUIRED_ERROR) } /*