UNPKG

@sphereon/oid4vci-client

Version:

OpenID for Verifiable Credential Issuance (OpenID4VCI) client

798 lines (726 loc) • 31.5 kB
import { CreateDPoPClientOpts, JWK } from '@sphereon/oid4vc-common'; import { AccessTokenRequestOpts, AccessTokenResponse, Alg, AuthorizationChallengeCodeResponse, AuthorizationChallengeErrorResponse, AuthorizationChallengeRequestOpts, AuthorizationRequestOpts, AuthorizationResponse, AuthorizationServerOpts, AuthzFlowType, CodeChallengeMethod, CredentialConfigurationSupported, CredentialConfigurationSupportedV1_0_13, CredentialOfferPayloadV1_0_08, CredentialOfferPayloadV1_0_11, CredentialOfferRequestWithBaseUrl, CredentialResponse, CredentialsSupportedLegacy, DefaultURISchemes, determineVersionsFromIssuerMetadata, DPoPResponseParams, EndpointMetadataResultV1_0_11, EndpointMetadataResultV1_0_13, ExperimentalSubjectIssuance, getClientIdFromCredentialOfferPayload, getIssuerFromCredentialOfferPayload, getSupportedCredentials, getTypesFromCredentialSupported, getTypesFromObject, KID_JWK_X5C_ERROR, NotificationRequest, NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, PKCEOpts, ProofOfPossessionCallbacks, toAuthorizationResponsePayload, } from '@sphereon/oid4vci-common'; import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; import { AccessTokenClient } from './AccessTokenClient'; import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11'; import { acquireAuthorizationChallengeAuthCode, createAuthorizationRequestUrl } from './AuthorizationCodeClient'; import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11'; import { CredentialOfferClient } from './CredentialOfferClient'; import { CredentialRequestOpts } from './CredentialRequestClient'; import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13'; import { MetadataClient } from './MetadataClient'; import { OpenID4VCIClientStateV1_0_11 } from './OpenID4VCIClientV1_0_11'; import { OpenID4VCIClientStateV1_0_13 } from './OpenID4VCIClientV1_0_13'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { generateMissingPKCEOpts, sendNotification } from './functions'; const debug = Debug('sphereon:oid4vci'); export type OpenID4VCIClientState = OpenID4VCIClientStateV1_0_11 | OpenID4VCIClientStateV1_0_13; export type EndpointMetadataResult = EndpointMetadataResultV1_0_11 | EndpointMetadataResultV1_0_13; export class OpenID4VCIClient { private readonly _state: OpenID4VCIClientState; private constructor({ credentialOffer, clientId, kid, alg, credentialIssuer, pkce, authorizationRequest, accessToken, jwk, endpointMetadata, accessTokenResponse, authorizationRequestOpts, authorizationCodeResponse, authorizationURL, }: { credentialOffer?: CredentialOfferRequestWithBaseUrl; kid?: string; alg?: Alg | string; clientId?: string; credentialIssuer?: string; pkce?: PKCEOpts; authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl jwk?: JWK; accessToken?: string; endpointMetadata?: EndpointMetadataResult; accessTokenResponse?: AccessTokenResponse; authorizationRequestOpts?: AuthorizationRequestOpts; authorizationCodeResponse?: AuthorizationResponse | AuthorizationChallengeCodeResponse; authorizationURL?: string; }) { const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined); if (!issuer) { throw Error('No credential issuer supplied or deduced from offer'); } this._state = { credentialOffer, credentialIssuer: issuer, kid, alg, // TODO: We need to refactor this and always explicitly call createAuthorizationRequestUrl, so we can have a credential selection first and use the kid as a default for the client id clientId: clientId ?? (credentialOffer && getClientIdFromCredentialOfferPayload(credentialOffer.credential_offer)) ?? kid?.split('#')[0], pkce: { disabled: false, codeChallengeMethod: CodeChallengeMethod.S256, ...pkce }, authorizationRequestOpts, authorizationCodeResponse, accessToken, jwk, endpointMetadata: endpointMetadata?.credentialIssuerMetadata?.authorization_server ? (endpointMetadata as EndpointMetadataResultV1_0_11) : (endpointMetadata as EndpointMetadataResultV1_0_13 | undefined), accessTokenResponse, authorizationURL, } as OpenID4VCIClientState; // Running syncAuthorizationRequestOpts later as it is using the state if (!this._state.authorizationRequestOpts) { this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(authorizationRequest); } debug(`Authorization req options: ${JSON.stringify(this._state.authorizationRequestOpts, null, 2)}`); } public static async fromCredentialIssuer({ kid, alg, retrieveServerMetadata, clientId, credentialIssuer, pkce, authorizationRequest, createAuthorizationRequestURL, endpointMetadata, }: { credentialIssuer: string; kid?: string; alg?: Alg | string; retrieveServerMetadata?: boolean; clientId?: string; createAuthorizationRequestURL?: boolean; authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl pkce?: PKCEOpts; endpointMetadata?: EndpointMetadataResult; }) { const client = new OpenID4VCIClient({ kid, alg, clientId: clientId ?? authorizationRequest?.clientId, credentialIssuer, pkce, authorizationRequest, endpointMetadata, }); if (retrieveServerMetadata === undefined || retrieveServerMetadata) { await client.retrieveServerMetadata(); } if (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL) { await client.createAuthorizationRequestUrl({ authorizationRequest, pkce }); } return client; } public static async fromState({ state }: { state: OpenID4VCIClientState | string }): Promise<OpenID4VCIClient> { const clientState = typeof state === 'string' ? JSON.parse(state) : state; return new OpenID4VCIClient(clientState); } public static async fromURI({ uri, kid, alg, retrieveServerMetadata, clientId, pkce, createAuthorizationRequestURL, authorizationRequest, resolveOfferUri, endpointMetadata, }: { uri: string; kid?: string; alg?: Alg | string; retrieveServerMetadata?: boolean; createAuthorizationRequestURL?: boolean; resolveOfferUri?: boolean; pkce?: PKCEOpts; clientId?: string; authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl endpointMetadata?: EndpointMetadataResult; }): Promise<OpenID4VCIClient> { const credentialOfferClient = await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }); const client = new OpenID4VCIClient({ credentialOffer: credentialOfferClient, kid, alg, clientId: clientId ?? authorizationRequest?.clientId ?? credentialOfferClient.clientId, pkce, authorizationRequest, endpointMetadata, }); if (retrieveServerMetadata === undefined || retrieveServerMetadata) { await client.retrieveServerMetadata(); } if ( credentialOfferClient.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) && (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL) ) { await client.createAuthorizationRequestUrl({ authorizationRequest, pkce }); debug(`Authorization Request URL: ${client._state.authorizationURL}`); } return client; } /** * Allows you to create an Authorization Request URL when using an Authorization Code flow. This URL needs to be accessed using the front channel (browser) * * The Identity provider would present a login screen typically; after you authenticated, it would redirect to the provided redirectUri; which can be same device or cross-device * @param opts */ public async createAuthorizationRequestUrl(opts?: { authorizationRequest?: AuthorizationRequestOpts; pkce?: PKCEOpts }): Promise<string> { if (!this._state.authorizationURL) { this.calculatePKCEOpts(opts?.pkce); this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(opts?.authorizationRequest); if (!this._state.authorizationRequestOpts) { throw Error(`No Authorization Request options present or provided in this call`); } // todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found // handling this because of the support for v1_0-08 if ( this._state.endpointMetadata?.credentialIssuerMetadata && 'authorization_endpoint' in this._state.endpointMetadata.credentialIssuerMetadata ) { this._state.endpointMetadata.authorization_endpoint = this._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string; } if (this.version() <= OpenId4VCIVersion.VER_1_0_11) { this._state.authorizationURL = await createAuthorizationRequestUrlV1_0_11({ pkce: this._state.pkce, endpointMetadata: this.endpointMetadata as EndpointMetadataResultV1_0_11, authorizationRequest: this._state.authorizationRequestOpts, credentialOffer: this.credentialOffer, credentialsSupported: Object.values(this.getCredentialsSupported(true)) as CredentialsSupportedLegacy[], }); } else { this._state.authorizationURL = await createAuthorizationRequestUrl({ pkce: this._state.pkce, endpointMetadata: this.endpointMetadata as EndpointMetadataResultV1_0_13, authorizationRequest: this._state.authorizationRequestOpts, credentialOffer: this.credentialOffer, credentialConfigurationSupported: this.getCredentialsSupported(false) as Record<string, CredentialConfigurationSupportedV1_0_13>, }); } } return this._state.authorizationURL; } public async retrieveServerMetadata(): Promise<EndpointMetadataResult> { this.assertIssuerData(); if (!this._state.endpointMetadata) { if (this.credentialOffer) { this._state.endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer); } else if (this._state.credentialIssuer) { this._state.endpointMetadata = await MetadataClient.retrieveAllMetadata(this._state.credentialIssuer); } else { throw Error(`Cannot retrieve issuer metadata without either a credential offer, or issuer value`); } } return this.endpointMetadata; } private calculatePKCEOpts(pkce?: PKCEOpts) { this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce }); } public async acquireAuthorizationChallengeCode(opts?: AuthorizationChallengeRequestOpts): Promise<AuthorizationChallengeCodeResponse> { const response = await acquireAuthorizationChallengeAuthCode({ metadata: this.endpointMetadata, credentialIssuer: this.getIssuer(), clientId: this._state.clientId ?? this._state.authorizationRequestOpts?.clientId, ...opts, }); if (response.errorBody) { debug(`Authorization code error:\r\n${JSON.stringify(response.errorBody)}`); const error = response.errorBody as AuthorizationChallengeErrorResponse; return Promise.reject(error); } else if (!response.successBody) { debug(`Authorization code error. No success body`); return Promise.reject( Error( `Retrieving an authorization code token from ${this._state.endpointMetadata?.authorization_challenge_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`, ), ); } return { ...response.successBody }; } public async acquireAccessToken( opts?: Omit<AccessTokenRequestOpts, 'credentialOffer' | 'credentialIssuer' | 'metadata' | 'additionalParams'> & { clientId?: string; authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse; // Pass in an auth response, either as URI/redirect, or object additionalRequestParams?: Record<string, any>; }, ): Promise<AccessTokenResponse & { params?: DPoPResponseParams }> { const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; const code = this.getAuthorizationCode(opts?.authorizationResponse, opts?.code); if (opts?.codeVerifier) { this._state.pkce.codeVerifier = opts.codeVerifier; } this.assertIssuerData(); const asOpts: AuthorizationServerOpts = { ...opts?.asOpts }; const kid = asOpts.clientOpts?.kid ?? this._state.kid ?? this._state.authorizationRequestOpts?.requestObjectOpts?.kid; const clientAssertionType = asOpts.clientOpts?.clientAssertionType ?? (kid && clientId && typeof asOpts.clientOpts?.signCallbacks?.signCallback === 'function' ? 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' : undefined); if (this.isEBSI() || (clientId && kid)) { if (!clientId) { throw Error(`Client id expected for EBSI`); } asOpts.clientOpts = { ...asOpts.clientOpts, clientId, ...(kid && { kid }), ...(clientAssertionType && { clientAssertionType }), signCallbacks: asOpts.clientOpts?.signCallbacks ?? this._state.authorizationRequestOpts?.requestObjectOpts?.signCallbacks, }; } if (clientId) { this._state.clientId = clientId; if (!asOpts.clientOpts) { asOpts.clientOpts = { clientId }; } asOpts.clientOpts.clientId = clientId; } if (!this._state.accessTokenResponse) { const accessTokenClient = this.version() <= OpenId4VCIVersion.VER_1_0_12 ? new AccessTokenClientV1_0_11() : new AccessTokenClient(); if (redirectUri && redirectUri !== this._state.authorizationRequestOpts?.redirectUri) { console.log( `Redirect URI mismatch between access-token (${redirectUri}) and authorization request (${this._state.authorizationRequestOpts?.redirectUri}). According to the specification that is not allowed.`, ); } if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) { redirectUri = this._state.authorizationRequestOpts.redirectUri; } const response = await accessTokenClient.acquireAccessToken({ credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, credentialIssuer: this.getIssuer(), pin, ...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }), code, redirectUri, asOpts, ...(opts?.createDPoPOpts && { createDPoPOpts: opts.createDPoPOpts }), ...(opts?.additionalRequestParams && { additionalParams: opts.additionalRequestParams }), }); if (response.errorBody) { debug(`Access token error:\r\n${JSON.stringify(response.errorBody)}`); throw Error( `Retrieving an access token from ${this._state.endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${ response.origResponse.status }`, ); } else if (!response.successBody) { debug(`Access token error. No success body`); throw Error( `Retrieving an access token from ${ this._state.endpointMetadata?.token_endpoint } for issuer ${this.getIssuer()} failed as there was no success response body`, ); } this._state.accessTokenResponse = response.successBody; this._state.dpopResponseParams = response.params; this._state.accessToken = response.successBody.access_token; } return { ...this.accessTokenResponse, ...(this.dpopResponseParams && { params: this.dpopResponseParams }) }; } public async acquireCredentials({ credentialTypes, context, proofCallbacks, format, kid, jwk, alg, jti, deferredCredentialAwait, deferredCredentialIntervalInMS, createDPoPOpts, }: { credentialTypes: string | string[]; context?: string[]; proofCallbacks: ProofOfPossessionCallbacks; format?: CredentialFormat | OID4VCICredentialFormat; kid?: string; jwk?: JWK; alg?: Alg | string; jti?: string; deferredCredentialAwait?: boolean; deferredCredentialIntervalInMS?: number; experimentalHolderIssuanceSupported?: boolean; createDPoPOpts?: CreateDPoPClientOpts; }): Promise<CredentialResponse & { params?: DPoPResponseParams; access_token: string }> { if ([jwk, kid].filter((v) => v !== undefined).length > 1) { throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`); } if (alg) this._state.alg = alg; if (jwk) this._state.jwk = jwk; if (kid) this._state.kid = kid; let requestBuilder: CredentialRequestClientBuilderV1_0_13 | CredentialRequestClientBuilderV1_0_11; if (this.version() < OpenId4VCIVersion.VER_1_0_13) { requestBuilder = this.credentialOffer ? CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, }) : CredentialRequestClientBuilderV1_0_11.fromCredentialIssuer({ credentialIssuer: this.getIssuer(), credentialTypes, metadata: this.endpointMetadata, version: this.version(), }); } else { requestBuilder = this.credentialOffer ? CredentialRequestClientBuilderV1_0_13.fromCredentialOffer({ credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, }) : CredentialRequestClientBuilderV1_0_13.fromCredentialIssuer({ credentialIssuer: this.getIssuer(), credentialTypes, metadata: this.endpointMetadata, version: this.version(), }); } // If we are in an auth code flow, without a c nonce, we return the issuerState back to the issuer in case it is present const issuerState = this.issuerSupportedFlowTypes().includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) && this._state.authorizationCodeResponse && !this.accessTokenResponse?.c_nonce && this._state.credentialOffer?.issuerState ? this._state.credentialOffer.issuerState : undefined; requestBuilder.withIssuerState(issuerState); requestBuilder.withTokenFromResponse(this.accessTokenResponse); requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS); let subjectIssuance: ExperimentalSubjectIssuance | undefined; if (this.endpointMetadata?.credentialIssuerMetadata) { const metadata = this.endpointMetadata.credentialIssuerMetadata; const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes]; if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) { let typeSupported = false; metadata.credentials_supported.forEach((supportedCredential) => { const subTypes = getTypesFromCredentialSupported(supportedCredential); if ( subTypes.every((t, i) => types[i] === t) || (types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0]))) ) { typeSupported = true; if (supportedCredential.credential_subject_issuance) { subjectIssuance = { credential_subject_issuance: supportedCredential.credential_subject_issuance }; } } }); if (!typeSupported) { console.log(`Not all credential types ${JSON.stringify(credentialTypes)} are present in metadata for ${this.getIssuer()}`); // throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); } } else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) { const credentialsSupported = metadata.credentials_supported; if (types.some((type) => !metadata.credentials_supported || !credentialsSupported[type])) { throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); } } // todo: Format check? We might end up with some disjoint type / format combinations supported by the server } if (subjectIssuance) { requestBuilder.withSubjectIssuance(subjectIssuance); } const credentialRequestClient = requestBuilder.build(); const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: this.accessTokenResponse, callbacks: proofCallbacks, version: this.version(), }) .withIssuer(this.getIssuer()) .withAlg(this.alg); if (this._state.jwk) { proofBuilder.withJWK(this._state.jwk); } if (this._state.kid) { proofBuilder.withKid(this._state.kid); } if (this.clientId) { proofBuilder.withClientId(this.clientId); } if (jti) { proofBuilder.withJti(jti); } const response = await credentialRequestClient.acquireCredentialsUsingProof({ proofInput: proofBuilder, credentialTypes, context, format, subjectIssuance, createDPoPOpts, }); this._state.dpopResponseParams = response.params; if (response.errorBody) { debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`); throw Error( `Retrieving a credential from ${this._state.endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${ response.origResponse.status }`, ); } else if (!response.successBody) { debug(`Credential request error. No success body`); throw Error( `Retrieving a credential from ${ this._state.endpointMetadata?.credential_endpoint } for issuer ${this.getIssuer()} failed as there was no success response body`, ); } return { ...response.successBody, ...(this.dpopResponseParams && { params: this.dpopResponseParams }), access_token: response.access_token }; } public async exportState(): Promise<string> { return JSON.stringify(this._state); } getCredentialsSupported( restrictToInitiationTypes?: boolean, format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[], ): Record<string, CredentialConfigurationSupportedV1_0_13> | Array<CredentialConfigurationSupported> { return getSupportedCredentials({ issuerMetadata: this.endpointMetadata.credentialIssuerMetadata, version: this.version(), format: format, types: restrictToInitiationTypes ? this.getCredentialOfferTypes() : undefined, }); } public async sendNotification( credentialRequestOpts: Partial<CredentialRequestOpts>, request: NotificationRequest, accessToken?: string, ): Promise<NotificationResponseResult> { return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token); } getCredentialOfferTypes(): string[][] | undefined { if (!this.credentialOffer) { return []; } else if (this.version() < OpenId4VCIVersion.VER_1_0_11) { const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08; const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type; const result: string[][] = []; result[0] = types; return result; } else if (this.version() < OpenId4VCIVersion.VER_1_0_13) { return (this.credentialOffer.credential_offer as CredentialOfferPayloadV1_0_11).credentials.map((c) => getTypesFromObject(c) ?? []); } // we don't have this for v13. v13 only has credential_configuration_ids which is not translatable to type return undefined; } issuerSupportedFlowTypes(): AuthzFlowType[] { return ( this.credentialOffer?.supportedFlows ?? ((this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server) ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] : []) ); } isFlowTypeSupported(flowType: AuthzFlowType): boolean { return this.issuerSupportedFlowTypes().includes(flowType); } get authorizationURL(): string | undefined { return this._state.authorizationURL; } public hasAuthorizationURL(): boolean { return !!this.authorizationURL; } get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined { return this._state.credentialOffer; } public version(): OpenId4VCIVersion { if (this.credentialOffer?.version && this.credentialOffer.version !== OpenId4VCIVersion.VER_UNKNOWN) { return this.credentialOffer.version; } const metadata = this._state.endpointMetadata; if (metadata?.credentialIssuerMetadata) { const versions = determineVersionsFromIssuerMetadata(metadata.credentialIssuerMetadata); if (versions.length > 0 && !versions.includes(OpenId4VCIVersion.VER_UNKNOWN)) { return versions[0]; } } return OpenId4VCIVersion.VER_1_0_13; } public get endpointMetadata(): EndpointMetadataResult { this.assertServerMetadata(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._state.endpointMetadata!; } get kid(): string { this.assertIssuerData(); if (!this._state.kid) { throw new Error('No value for kid is supplied'); } return this._state.kid; } get alg(): string { this.assertIssuerData(); if (!this._state.alg) { throw new Error('No value for alg is supplied'); } return this._state.alg; } set clientId(value: string | undefined) { this._state.clientId = value; } get clientId(): string | undefined { return this._state.clientId; } public hasAccessTokenResponse(): boolean { return !!this._state.accessTokenResponse; } get accessTokenResponse(): AccessTokenResponse { this.assertAccessToken(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._state.accessTokenResponse!; } get dpopResponseParams(): DPoPResponseParams | undefined { return this._state.dpopResponseParams; } public getIssuer(): string { this.assertIssuerData(); return this._state.credentialIssuer; } public getAccessTokenEndpoint(): string { this.assertIssuerData(); if (this.endpointMetadata) { return this.endpointMetadata.token_endpoint; } return this.version() <= OpenId4VCIVersion.VER_1_0_12 ? AccessTokenClientV1_0_11.determineTokenURL({ issuerOpts: { issuer: this.getIssuer() } }) : AccessTokenClient.determineTokenURL({ issuerOpts: { issuer: this.getIssuer() } }); } public getCredentialEndpoint(): string { this.assertIssuerData(); return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`; } public getAuthorizationChallengeEndpoint(): string | undefined { this.assertIssuerData(); return this.endpointMetadata?.authorization_challenge_endpoint; } public hasAuthorizationChallengeEndpoint(): boolean { return !!this.getAuthorizationChallengeEndpoint(); } public hasDeferredCredentialEndpoint(): boolean { return !!this.getAccessTokenEndpoint(); } public getDeferredCredentialEndpoint(): string { this.assertIssuerData(); return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`; } /** * Too bad we need a method like this, but EBSI is not exposing metadata */ public isEBSI() { if ( this.credentialOffer && (this.credentialOffer?.credential_offer as CredentialOfferPayloadV1_0_11)?.credentials?.find( (cred) => // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore typeof cred !== 'string' && 'trust_framework' in cred && 'name' in cred.trust_framework && cred.trust_framework.name.includes('ebsi'), ) ) { return true; } // this.assertIssuerData(); return ( this.clientId?.includes('ebsi') || this._state.kid?.includes('did:ebsi:') || this.getIssuer().includes('ebsi') || this.endpointMetadata.credentialIssuerMetadata?.authorization_endpoint?.includes('ebsi.eu') || this.endpointMetadata.credentialIssuerMetadata?.authorization_server?.includes('ebsi.eu') ); } private assertIssuerData(): void { if (!this._state.credentialIssuer) { throw Error(`No credential issuer value present`); } else if (!this._state.credentialOffer && this._state.endpointMetadata && this.issuerSupportedFlowTypes().length === 0) { throw Error(`No issuance initiation or credential offer present`); } } private assertServerMetadata(): void { if (!this._state.endpointMetadata) { throw Error('No server metadata'); } } private assertAccessToken(): void { if (!this._state.accessTokenResponse) { throw Error(`No access token present`); } } private syncAuthorizationRequestOpts(opts?: AuthorizationRequestOpts): AuthorizationRequestOpts { const requestObjectOpts = { ...this._state?.authorizationRequestOpts?.requestObjectOpts, ...opts?.requestObjectOpts }; let authorizationRequestOpts = { ...this._state?.authorizationRequestOpts, ...opts, ...(requestObjectOpts && { requestObjectOpts }), } as AuthorizationRequestOpts; if (!authorizationRequestOpts) { // We only set a redirectUri if no options are provided. // Note that this only works for mobile apps, that can handle a code query param on the default openid-credential-offer deeplink. // Provide your own options if that is not desired! authorizationRequestOpts = { redirectUri: `${DefaultURISchemes.CREDENTIAL_OFFER}://` }; } const clientId = authorizationRequestOpts.clientId ?? this._state.clientId; // sync clientId this._state.clientId = clientId; authorizationRequestOpts.clientId = clientId; return authorizationRequestOpts; } private getAuthorizationCode = ( authorizationResponse?: string | AuthorizationResponse | AuthorizationChallengeCodeResponse, code?: string, ): string | undefined => { if (authorizationResponse) { this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(authorizationResponse) }; } else if (code) { this._state.authorizationCodeResponse = { code }; } return ( (this._state.authorizationCodeResponse as AuthorizationResponse)?.code ?? (this._state.authorizationCodeResponse as AuthorizationChallengeCodeResponse)?.authorization_code ); }; }