@sphereon/oid4vci-issuer-server
Version:
OpenID 4 Verifiable Credential Issuance Server
1 lines • 84.8 kB
Source Map (JSON)
{"version":3,"sources":["../lib/index.ts","../lib/OID4VCIServer.ts","../lib/oid4vci-api-functions.ts","../lib/IssuerTokenEndpoint.ts","../lib/expressUtils.ts"],"sourcesContent":["export * from './OID4VCIServer'\nexport * from './oid4vci-api-functions'\nexport * from './expressUtils'\n\n// We re-export oidc-client types, as they were previously exported here\nexport type { ClientResponseType, ClientAuthMethod, ClientMetadata } from '@sphereon/oid4vci-common'\n","import {\n AuthorizationRequest,\n ClientMetadata,\n CreateCredentialOfferURIResult,\n CredentialConfigurationSupportedV1_0_15,\n CredentialOfferMode,\n OID4VCICredentialFormat,\n QRCodeOpts,\n} from '@sphereon/oid4vci-common'\nimport {\n CredentialSupportedBuilderV1_15,\n ITokenEndpointOpts,\n oidcAccessTokenVerifyCallback,\n VcIssuer,\n VcIssuerBuilder,\n} from '@sphereon/oid4vci-issuer'\nimport { ExpressSupport, HasEndpointOpts, ISingleEndpointOpts } from '@sphereon/ssi-express-support'\nimport express, { Express } from 'express'\n\nimport {\n accessTokenEndpoint,\n authorizationChallengeEndpoint,\n createCredentialOfferEndpoint,\n deleteCredentialOfferEndpoint,\n getBasePath,\n getCredentialEndpoint,\n getCredentialOfferEndpoint,\n getCredentialOfferReferenceEndpoint,\n getIssueStatusEndpoint,\n getMetadataEndpoints,\n nonceEndpoint,\n pushedAuthorizationEndpoint,\n} from './oid4vci-api-functions'\n\nfunction buildVCIFromEnvironment() {\n const credentialsSupported: Record<string, CredentialConfigurationSupportedV1_0_15> = new CredentialSupportedBuilderV1_15()\n .withCredentialSigningAlgValuesSupported(process.env.credential_signing_alg_values_supported as string)\n .withCryptographicBindingMethod(process.env.cryptographic_binding_methods_supported as string)\n .withFormat(process.env.credential_supported_format as unknown as OID4VCICredentialFormat)\n .withCredentialName(process.env.credential_supported_name_1 as string)\n .withCredentialDefinition({\n type: [process.env.credential_supported_1_definition_type_1 as string, process.env.credential_supported_1_definition_type_2 as string],\n // TODO: setup credentialSubject here from env\n // credentialSubject\n })\n .withCredentialSupportedDisplay({\n name: process.env.credential_display_name as string,\n locale: process.env.credential_display_locale as string,\n logo: {\n url: process.env.credential_display_logo_url as string,\n alt_text: process.env.credential_display_logo_alt_text as string,\n },\n background_color: process.env.credential_display_background_color as string,\n text_color: process.env.credential_display_text_color as string,\n })\n .build()\n const issuerBuilder = new VcIssuerBuilder()\n .withTXCode({\n length: process.env.user_pin_length as unknown as number,\n input_mode: process.env.user_pin_input_mode as 'numeric' | 'text',\n })\n .withAuthorizationServers(process.env.authorization_server as string)\n .withCredentialEndpoint(process.env.credential_endpoint as string)\n .withNonceEndpoint(process.env.nonce_endpoint as string)\n .withCredentialIssuer(process.env.credential_issuer as string)\n .withIssuerDisplay({\n name: process.env.issuer_name as string,\n locale: process.env.issuer_locale as string,\n })\n .withCredentialConfigurationsSupported(credentialsSupported)\n .withInMemoryCredentialOfferState()\n .withInMemoryCNonceState()\n\n if (process.env.authorization_server_client_id) {\n if (!process.env.authorization_server_redirect_uri) {\n throw Error('Authorization server redirect uri is required when client id is set')\n }\n issuerBuilder.withASClientMetadataParams({\n client_id: process.env.authorization_server_client_id,\n client_secret: process.env.authorization_server_client_secret,\n redirect_uris: [process.env.authorization_server_redirect_uri],\n })\n }\n\n return issuerBuilder.build()\n}\n\nexport type ICreateCredentialOfferURIResponse = Omit<CreateCredentialOfferURIResult, 'session'>\n\nexport interface IGetCredentialOfferEndpointOpts extends ISingleEndpointOpts {\n baseUrl: string\n}\n\nexport interface IDeleteCredentialOfferEndpointOpts extends ISingleEndpointOpts {\n baseUrl: string\n}\n\nexport interface ICreateCredentialOfferEndpointOpts extends ISingleEndpointOpts {\n getOfferPath?: string\n qrCodeOpts?: QRCodeOpts\n baseUrl?: string\n credentialOfferReferenceBasePath?: string\n defaultCredentialOfferMode?: CredentialOfferMode\n}\n\nexport interface IGetIssueStatusEndpointOpts extends ISingleEndpointOpts {\n baseUrl: string | URL\n}\n\nexport interface IGetIssuePayloadEndpointOpts extends ISingleEndpointOpts {\n baseUrl: string | URL\n}\n\nexport interface IAuthorizationChallengeEndpointOpts extends ISingleEndpointOpts {\n createAuthRequestUriEndpointPath?: string\n verifyAuthResponseEndpointPath?: string\n /**\n * Callback used for creating the authorization request uri used for the RP.\n * Added an optional state parameter so that when direct calls are used,\n * one could set the state value of the RP session to match the state value of the VCI session.\n */\n createAuthRequestUriCallback: (state?: string) => Promise<string>\n /**\n * Callback used for verifying the status of the authorization response.\n * This is checked by the issuer before issuing an authorization code.\n */\n verifyAuthResponseCallback: (correlationId: string) => Promise<boolean>\n}\n\nexport interface IOID4VCIEndpointOpts {\n trustProxy?: boolean | Array<string>\n tokenEndpointOpts?: ITokenEndpointOpts\n notificationOpts?: ISingleEndpointOpts\n createCredentialOfferOpts?: ICreateCredentialOfferEndpointOpts\n deleteCredentialOfferOpts?: IDeleteCredentialOfferEndpointOpts\n getCredentialOfferOpts?: IGetCredentialOfferEndpointOpts\n getStatusOpts?: IGetIssueStatusEndpointOpts\n getIssuePayloadOpts?: IGetIssuePayloadEndpointOpts\n parOpts?: ISingleEndpointOpts\n authorizationChallengeOpts?: IAuthorizationChallengeEndpointOpts\n nonceOpts?: INonceEndpointOpts\n}\n\nexport interface INonceEndpointOpts extends ISingleEndpointOpts {\n baseUrl: string | URL\n}\n\nexport enum WellKnownHostLocation {\n AT_CONTEXT_PATH = 'AT_CONTEXT_PATH',\n AT_ROOT_PATH = 'AT_ROOT_PATH',\n AT_BOTH = 'AT_BOTH',\n}\n\nexport interface IOID4VCIServerOpts extends HasEndpointOpts {\n asClientOpts?: ClientMetadata\n endpointOpts?: IOID4VCIEndpointOpts\n baseUrl?: string\n wellKnownHostLocation?: WellKnownHostLocation\n}\n\nexport class OID4VCIServer {\n private readonly _issuer: VcIssuer\n private authRequestsData: Map<string, AuthorizationRequest> = new Map()\n private readonly _app: Express\n private readonly _baseUrl: URL\n private readonly _expressSupport: ExpressSupport\n // private readonly _server?: http.Server\n private readonly _router: express.Router\n private readonly _asClientOpts?: ClientMetadata\n private readonly _wellknownHostLocation?: WellKnownHostLocation\n\n constructor(\n expressSupport: ExpressSupport,\n opts: IOID4VCIServerOpts & {\n issuer?: VcIssuer\n } /*If not supplied as argument, it will be fully configured from environment variables*/,\n ) {\n this._baseUrl = new URL(opts?.baseUrl ?? process.env.BASE_URL ?? opts?.issuer?.issuerMetadata?.credential_issuer ?? 'http://localhost')\n this._expressSupport = expressSupport\n this._app = expressSupport.express\n this._router = express.Router()\n this._issuer = opts?.issuer ? opts.issuer : buildVCIFromEnvironment()\n this._asClientOpts =\n opts.asClientOpts || this._issuer.asClientOpts ? ({ ...opts.asClientOpts, ...this._issuer.asClientOpts } as ClientMetadata) : undefined\n this._wellknownHostLocation =\n opts?.wellKnownHostLocation ?? (process.env.WELLKNOWN_HOST_LOCATION as WellKnownHostLocation) ?? WellKnownHostLocation.AT_BOTH\n pushedAuthorizationEndpoint(this.router, this.issuer, this.authRequestsData)\n\n // Create root router for alternative .well-known endpoints if needed\n const basePath = getBasePath(this.baseUrl)\n let rootRouter: express.Router | undefined\n if (\n basePath &&\n basePath !== '/' &&\n (this.wellknownHostLocation == WellKnownHostLocation.AT_ROOT_PATH || this.wellknownHostLocation == WellKnownHostLocation.AT_BOTH)\n ) {\n rootRouter = express.Router()\n this._app.use('/', rootRouter)\n }\n\n getMetadataEndpoints(this.router, this.issuer, {\n rootRouter,\n basePath,\n wellKnownHostLocation: this.wellknownHostLocation,\n })\n\n let issuerPayloadPath: string | undefined\n if (this.isGetIssuePayloadEndpointEnabled(opts?.endpointOpts?.getIssuePayloadOpts)) {\n issuerPayloadPath = getCredentialOfferReferenceEndpoint(this.router, this.issuer, {\n ...opts?.endpointOpts?.getIssuePayloadOpts,\n baseUrl: this.baseUrl,\n })\n }\n\n if (opts?.endpointOpts?.createCredentialOfferOpts?.enabled !== false || process.env.CREDENTIAL_OFFER_ENDPOINT_ENABLED === 'true') {\n createCredentialOfferEndpoint(this.router, this.issuer, opts?.endpointOpts?.createCredentialOfferOpts, issuerPayloadPath)\n deleteCredentialOfferEndpoint(this.router, this.issuer, opts?.endpointOpts?.deleteCredentialOfferOpts)\n }\n getCredentialOfferEndpoint(this.router, this.issuer, opts?.endpointOpts?.getCredentialOfferOpts)\n getCredentialEndpoint(this.router, this.issuer, {\n ...opts?.endpointOpts?.tokenEndpointOpts,\n baseUrl: this.baseUrl,\n accessTokenVerificationCallback:\n opts.endpointOpts?.tokenEndpointOpts?.accessTokenVerificationCallback ??\n (this._asClientOpts\n ? oidcAccessTokenVerifyCallback({\n clientMetadata: this._asClientOpts,\n credentialIssuer: this._issuer.issuerMetadata.credential_issuer,\n authorizationServer: this._issuer.issuerMetadata.authorization_servers![0],\n })\n : undefined),\n })\n this.assertAccessTokenHandling()\n if (!this.isTokenEndpointDisabled(opts?.endpointOpts?.tokenEndpointOpts, opts?.asClientOpts)) {\n accessTokenEndpoint(this.router, this.issuer, {\n ...opts?.endpointOpts?.tokenEndpointOpts,\n baseUrl: this.baseUrl,\n authRequestsData: this.authRequestsData,\n })\n }\n if (this.isStatusEndpointEnabled(opts?.endpointOpts?.getStatusOpts)) {\n getIssueStatusEndpoint(this.router, this.issuer, { ...opts?.endpointOpts?.getStatusOpts, baseUrl: this.baseUrl })\n }\n if (this.isAuthorizationChallengeEndpointEnabled(opts?.endpointOpts?.authorizationChallengeOpts)) {\n if (!opts?.endpointOpts?.authorizationChallengeOpts?.createAuthRequestUriCallback) {\n throw Error(`Unable to enable authorization challenge endpoint. No createAuthRequestUriCallback present in authorization challenge options`)\n } else if (!opts?.endpointOpts?.authorizationChallengeOpts?.verifyAuthResponseCallback) {\n throw Error(`Unable to enable authorization challenge endpoint. No verifyAuthResponseCallback present in authorization challenge options`)\n }\n authorizationChallengeEndpoint(this.router, this.issuer, {\n ...opts?.endpointOpts?.authorizationChallengeOpts,\n baseUrl: this.baseUrl,\n })\n }\n\n if (this.isNonceEndpointEnabled(opts?.endpointOpts?.nonceOpts)) {\n nonceEndpoint(this.router, this.issuer, {\n ...opts?.endpointOpts?.nonceOpts,\n baseUrl: this.baseUrl,\n })\n }\n this._app.use(basePath, this._router)\n }\n\n public get app(): Express {\n return this._app\n }\n\n /*public get server(): http.Server | undefined {\n return this._server\n }*/\n\n public get router(): express.Router {\n return this._router\n }\n\n get issuer(): VcIssuer {\n return this._issuer\n }\n\n public async stop() {\n if (!this._expressSupport) {\n throw Error('Cannot stop server is the REST API is only a router of an existing express app')\n }\n await this._expressSupport.stop()\n }\n\n private isTokenEndpointDisabled(tokenEndpointOpts?: ITokenEndpointOpts, asClientMetadata?: ClientMetadata) {\n return tokenEndpointOpts?.tokenEndpointDisabled === true || process.env.TOKEN_ENDPOINT_DISABLED === 'true' || asClientMetadata\n }\n\n private isStatusEndpointEnabled(statusEndpointOpts?: IGetIssueStatusEndpointOpts) {\n return statusEndpointOpts?.enabled !== false || process.env.STATUS_ENDPOINT_ENABLED !== 'false'\n }\n\n private isGetIssuePayloadEndpointEnabled(payloadEndpointOpts?: IGetIssuePayloadEndpointOpts) {\n return payloadEndpointOpts?.enabled !== false || process.env.STATUS_ENDPOINT_ENABLED !== 'false'\n }\n\n private isAuthorizationChallengeEndpointEnabled(authorizationChallengeEndpointOpts?: IAuthorizationChallengeEndpointOpts) {\n return authorizationChallengeEndpointOpts?.enabled === true || process.env.AUTHORIZATION_CHALLENGE_ENDPOINT_ENABLED === 'true'\n }\n\n private assertAccessTokenHandling(tokenEndpointOpts?: ITokenEndpointOpts) {\n const authServer = this.issuer.issuerMetadata.authorization_servers\n if (this.isTokenEndpointDisabled(tokenEndpointOpts, this.issuer.asClientOpts)) {\n if (!authServer || authServer.length === 0) {\n throw Error(\n `No Authorization Server (AS) is defined in the issuer metadata and the token endpoint is disabled. An AS or token endpoints needs to be present`,\n )\n }\n if (this.issuer.asClientOpts) {\n console.log(`Token endpoint disabled because AS client metadata is set for ${authServer[0]}`)\n } else {\n console.log(`Token endpoint disabled by configuration`)\n }\n } else {\n if (authServer && authServer.some((as) => as !== this.issuer.issuerMetadata.credential_issuer)) {\n throw Error(\n `An external Authorization Server (AS) was already enabled in the issuer metadata (${authServer}). Cannot both have an AS and enable the token endpoint at the same time `,\n )\n } else if (this._asClientOpts) {\n throw Error(`OIDC Client metadata is set, but the token endpoint is not disabled. This is not supported.`)\n }\n }\n }\n\n private isNonceEndpointEnabled(nonceEndpointOpts?: INonceEndpointOpts) {\n return nonceEndpointOpts?.enabled !== false || process.env.NONCE_ENDPOINT_ENABLED !== 'false'\n }\n\n get baseUrl(): URL {\n return this._baseUrl\n }\n\n get wellknownHostLocation(): WellKnownHostLocation | undefined {\n return this._wellknownHostLocation\n }\n}\n","import { uuidv4 } from '@sphereon/oid4vc-common'\nimport {\n ACCESS_TOKEN_ISSUER_REQUIRED_ERROR,\n AccessTokenRequest,\n adjustUrl,\n AuthorizationChallengeCodeResponse,\n AuthorizationChallengeError,\n AuthorizationChallengeErrorResponse,\n AuthorizationRequest,\n CommonAuthorizationChallengeRequest,\n CredentialIssuerMetadataOptsV1_0_15,\n CredentialOfferMode,\n CredentialOfferRESTRequestV1_0_15,\n CredentialRequestV1_0_15,\n determineGrantTypes,\n EVENTS,\n extractBearerToken,\n generateRandomString,\n getNumberOrUndefined,\n Grant,\n IssueStatusResponse,\n JWT_SIGNER_CALLBACK_REQUIRED_ERROR,\n NotificationRequest,\n NotificationStatusEventNames,\n TokenErrorResponse,\n trimBoth,\n trimEnd,\n trimStart,\n validateJWT,\n WellKnownEndpoints,\n} from '@sphereon/oid4vci-common'\nimport { IssuerCorrelation, ITokenEndpointOpts, LOG, VcIssuer } from '@sphereon/oid4vci-issuer'\nimport { env, ISingleEndpointOpts, sendErrorResponse } from '@sphereon/ssi-express-support'\nimport { InitiatorType, SubSystem, System } from '@sphereon/ssi-types'\nimport { NextFunction, Request, Response, Router } from 'express'\n\nimport { handleTokenRequest, verifyTokenRequest } from './IssuerTokenEndpoint'\nimport {\n IAuthorizationChallengeEndpointOpts,\n ICreateCredentialOfferEndpointOpts,\n ICreateCredentialOfferURIResponse,\n IGetCredentialOfferEndpointOpts,\n IGetIssueStatusEndpointOpts,\n INonceEndpointOpts,\n WellKnownHostLocation,\n} from './OID4VCIServer'\nimport { validateRequestBody } from './expressUtils'\n\nconst expiresIn = process.env.EXPIRES_IN ? parseInt(process.env.EXPIRES_IN) : 90\n\nexport function getIssueStatusEndpoint(router: Router, issuer: VcIssuer, opts: IGetIssueStatusEndpointOpts) {\n const path = determinePath(opts.baseUrl, opts?.path ?? '/webapp/credential-offer-status', { stripBasePath: true })\n LOG.log(`[OID4VCI] getIssueStatus endpoint enabled at ${path}`)\n router.post(path, async (request: Request, response: Response) => {\n try {\n const { id } = request.body\n const session = await issuer.getCredentialOfferSessionById(id)\n if (!session || !session.credentialOffer) {\n return sendErrorResponse(response, 404, {\n error: 'invalid_request',\n error_description: `Credential offer ${id} not found`,\n })\n }\n\n const authStatusBody: IssueStatusResponse = {\n createdAt: session.createdAt,\n lastUpdatedAt: session.lastUpdatedAt,\n expiresAt: session.expiresAt,\n status: session.status,\n statusLists: session.statusLists,\n ...(session.error && { error: session.error }),\n ...(session.clientId && { clientId: session.clientId }),\n }\n return response.json(authStatusBody)\n } catch (e) {\n return sendErrorResponse(\n response,\n 500,\n {\n error: 'invalid_request',\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n}\n\nexport function getCredentialOfferReferenceEndpoint(router: Router, issuer: VcIssuer, opts: IGetIssueStatusEndpointOpts): string {\n const path = determinePath(opts.baseUrl, opts?.path ?? '/credential-offers/:id', { stripBasePath: true })\n LOG.log(`[OID4VCI] getCredentialOfferReferenceEndpoint endpoint enabled at ${path}`)\n router.get(path, async (request: Request, response: Response) => {\n try {\n const { id } = request.params\n if (!id) {\n return sendErrorResponse(response, 404, {\n error: 'invalid_request',\n error_description: `query parameter 'id' is missing`,\n })\n }\n\n let session\n try {\n session = await issuer.getCredentialOfferSessionById(id as string)\n } catch (e) {\n /* will crash with 500 instead of 404 if we do not catch */\n }\n\n if (!session || !session.credentialOffer || session.status !== 'OFFER_CREATED') {\n if (session?.status) {\n LOG.warning(\n `[OID4VCI] credential offer reference URI request with ${id}, but request was already received earlier. Session status: ${session.status}`,\n )\n }\n return sendErrorResponse(response, 404, {\n error: 'invalid_request',\n error_description: `Credential offer ${id} not found`,\n })\n }\n\n return response.json(session.credentialOffer.credential_offer)\n } catch (e) {\n return sendErrorResponse(\n response,\n 500,\n {\n error: 'invalid_request',\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n return path\n}\n\nfunction isExternalAS(issuerMetadata: CredentialIssuerMetadataOptsV1_0_15) {\n return issuerMetadata.authorization_servers?.some((as) => !as.includes(issuerMetadata.credential_issuer))\n}\n\nexport function authorizationChallengeEndpoint(\n router: Router,\n issuer: VcIssuer,\n opts: IAuthorizationChallengeEndpointOpts & { baseUrl: string | URL },\n) {\n const endpoint = issuer.authorizationServerMetadata.authorization_challenge_endpoint ?? issuer.issuerMetadata.authorization_challenge_endpoint\n const baseUrl = getBaseUrl(opts.baseUrl)\n if (!endpoint) {\n LOG.info('authorization challenge endpoint disabled as no \"authorization_challenge_endpoint\" has been configured in issuer metadata')\n return\n }\n const path = determinePath(baseUrl, endpoint, { stripBasePath: true })\n LOG.log(`[OID4VCI] authorization challenge endpoint at ${path}`)\n router.post(path, async (request: Request, response: Response) => {\n const authorizationChallengeRequest = request.body as CommonAuthorizationChallengeRequest\n const { client_id, issuer_state, auth_session, presentation_during_issuance_session } = authorizationChallengeRequest\n\n try {\n if (!client_id && !auth_session) {\n const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {\n error: AuthorizationChallengeError.invalid_request,\n error_description: 'No client id or auth session present',\n } as AuthorizationChallengeErrorResponse\n throw authorizationChallengeErrorResponse\n }\n\n if (!auth_session && issuer_state) {\n const session = await issuer.credentialOfferSessions.get(issuer_state)\n if (!session) {\n const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {\n error: AuthorizationChallengeError.invalid_session,\n error_description: 'Session is invalid',\n }\n throw authorizationChallengeErrorResponse\n }\n\n const authRequestURI = await opts.createAuthRequestUriCallback(issuer_state) // TODO generate some error\n const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {\n error: AuthorizationChallengeError.insufficient_authorization,\n auth_session: issuer_state,\n presentation: authRequestURI,\n }\n throw authorizationChallengeErrorResponse\n }\n\n if (auth_session && presentation_during_issuance_session) {\n const session = await issuer.credentialOfferSessions.get(auth_session)\n if (!session) {\n const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {\n error: AuthorizationChallengeError.invalid_session,\n error_description: 'Session is invalid',\n }\n throw authorizationChallengeErrorResponse\n }\n\n const verifiedResponse = await opts.verifyAuthResponseCallback(presentation_during_issuance_session) // TODO generate some error\n if (verifiedResponse) {\n const authorizationCode = generateRandomString(16, 'base64url')\n session.authorizationCode = authorizationCode\n await issuer.credentialOfferSessions.set(auth_session, session)\n const authorizationChallengeCodeResponse: AuthorizationChallengeCodeResponse = {\n authorization_code: authorizationCode,\n }\n return response.json(authorizationChallengeCodeResponse)\n }\n }\n\n const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {\n error: AuthorizationChallengeError.invalid_request,\n }\n throw authorizationChallengeErrorResponse\n } catch (e) {\n return sendErrorResponse(response, 400, e as Error, e)\n }\n })\n}\n\nexport function accessTokenEndpoint(\n router: Router,\n issuer: VcIssuer,\n opts: ITokenEndpointOpts &\n ISingleEndpointOpts & {\n baseUrl: string | URL\n authRequestsData?: Map<string, AuthorizationRequest>\n },\n) {\n const externalAS = isExternalAS(issuer.issuerMetadata) || issuer.asClientOpts\n if (externalAS || (opts.accessTokenProvider && opts.accessTokenProvider !== 'internal')) {\n LOG.log(\n `[OID4VCI] External Authorization Server ${issuer.issuerMetadata.authorization_servers} is being used. Not enabling internal issuer token endpoint`,\n )\n return\n } else if (opts?.enabled === false) {\n LOG.log(`[OID4VCI] Internal issuer token endpoint is not enabled`)\n return\n }\n const accessTokenIssuer =\n opts?.accessTokenIssuer ??\n process.env.ACCESS_TOKEN_ISSUER ??\n issuer.issuerMetadata.authorization_servers?.[0] ??\n issuer.issuerMetadata.credential_issuer\n\n const preAuthorizedCodeExpirationDuration =\n opts?.preAuthorizedCodeExpirationDuration ?? getNumberOrUndefined(process.env.PRE_AUTHORIZED_CODE_EXPIRATION_DURATION) ?? 300\n const interval = opts?.interval ?? getNumberOrUndefined(process.env.INTERVAL) ?? 300\n const tokenExpiresIn = opts?.tokenExpiresIn ?? 300\n\n // todo: this means we cannot sign JWTs or issue access tokens when configured from env vars!\n if (opts?.accessTokenSignerCallback === undefined) {\n throw new Error(JWT_SIGNER_CALLBACK_REQUIRED_ERROR)\n } else if (!accessTokenIssuer) {\n throw new Error(ACCESS_TOKEN_ISSUER_REQUIRED_ERROR)\n }\n\n const baseUrl = getBaseUrl(opts.baseUrl)\n\n // issuer is also AS\n const path = determinePath(baseUrl, opts?.tokenPath ?? process.env.TOKEN_PATH ?? '/token', {\n skipBaseUrlCheck: false,\n stripBasePath: true,\n })\n // let's fix any baseUrl ending with a slash as path will always start with a slash, and we already removed it at the end of the base url\n\n const url = new URL(`${baseUrl}${path}`)\n\n LOG.log(`[OID4VCI] Token endpoint enabled at ${url.toString()}`)\n\n // this.issuer.issuerMetadata.token_endpoint = url.toString()\n router.post(\n determinePath(baseUrl, url.pathname, { stripBasePath: true }),\n verifyTokenRequest({\n issuer,\n preAuthorizedCodeExpirationDuration,\n authRequestsData: opts.authRequestsData,\n }),\n handleTokenRequest({\n issuer,\n accessTokenSignerCallback: opts.accessTokenSignerCallback,\n cNonceExpiresIn: issuer.cNonceExpiresIn,\n interval,\n tokenExpiresIn,\n accessTokenIssuer,\n }),\n )\n}\n\nexport function getCredentialEndpoint(\n router: Router,\n issuer: VcIssuer,\n opts: Pick<ITokenEndpointOpts, 'accessTokenVerificationCallback' | 'accessTokenSignerCallback' | 'tokenExpiresIn' | 'cNonceExpiresIn'> &\n ISingleEndpointOpts & { baseUrl: string | URL },\n) {\n const endpoint = issuer.issuerMetadata.credential_endpoint\n const baseUrl = getBaseUrl(opts.baseUrl)\n let path: string\n if (!endpoint) {\n path = `/credentials`\n issuer.issuerMetadata.credential_endpoint = `${baseUrl}${path}`\n } else {\n path = determinePath(baseUrl, endpoint, { stripBasePath: true, skipBaseUrlCheck: false })\n }\n path = determinePath(baseUrl, path, { stripBasePath: true })\n LOG.log(`[OID4VCI] getCredential endpoint enabled at ${path}`)\n router.post(path, async (request: Request, response: Response) => {\n try {\n const credentialRequest = request.body as CredentialRequestV1_0_15\n LOG.log(`credential request received`, credentialRequest)\n const issuerCorrelation: IssuerCorrelation = {}\n try {\n const jwt = extractBearerToken(request.header('Authorization'))\n const jwtVerifyResult = await validateJWT(jwt, {\n accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback,\n })\n const tokenClaims = jwtVerifyResult.jwt.payload\n if ('preAuthorizedCode' in tokenClaims && typeof tokenClaims.preAuthorizedCode === 'string') {\n issuerCorrelation.preAuthorizedCode = tokenClaims.preAuthorizedCode\n }\n if ('issuer_state' in tokenClaims && typeof tokenClaims.issuer_state === 'string') {\n issuerCorrelation.issuerState = tokenClaims.issuer_state\n }\n\n // Handle credential_identifier from authorization_details flow\n if ('authorization_details' in tokenClaims && Array.isArray(tokenClaims.authorization_details)) {\n issuerCorrelation.authorizationDetails = tokenClaims.authorization_details\n\n if (credentialRequest.credential_identifier) {\n const validIdentifiers = tokenClaims.authorization_details.flatMap((detail: any) => detail.credential_identifiers || [])\n\n if (!validIdentifiers.includes(credentialRequest.credential_identifier)) {\n return sendErrorResponse(response, 400, {\n error: 'invalid_credential_request',\n error_description: 'credential_identifier not found in authorization_details',\n })\n }\n }\n }\n } catch (e) {\n LOG.warning(e)\n return sendErrorResponse(response, 400, {\n error: 'invalid_token',\n })\n }\n\n const credential = await issuer.issueCredential({\n credentialRequest: credentialRequest,\n issuerCorrelation,\n tokenExpiresIn: opts.tokenExpiresIn,\n cNonceExpiresIn: opts.cNonceExpiresIn,\n })\n return response.json(credential)\n } catch (e) {\n return sendErrorResponse(\n response,\n 500,\n {\n error: 'invalid_request',\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n}\n\nexport function notificationEndpoint(\n router: Router,\n issuer: VcIssuer,\n opts: ISingleEndpointOpts & Pick<ITokenEndpointOpts, 'accessTokenVerificationCallback'> & { baseUrl: string | URL },\n) {\n const endpoint = issuer.issuerMetadata.notification_endpoint\n const baseUrl = getBaseUrl(opts.baseUrl)\n if (!endpoint) {\n LOG.warning('Notification endpoint disabled as no \"notification_endpoint\" has been configured in issuer metadata')\n return\n }\n const path = determinePath(baseUrl, endpoint, { stripBasePath: true })\n LOG.log(`[OID4VCI] notification endpoint enabled at ${path}`)\n router.post(path, async (request: Request, response: Response) => {\n try {\n const notificationRequest = request.body as NotificationRequest\n LOG.log(\n `notification ${notificationRequest.event}/${notificationRequest.event_description} received for ${notificationRequest.notification_id}`,\n )\n const jwt = extractBearerToken(request.header('Authorization'))\n EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_RECEIVED, {\n eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_RECEIVED,\n id: uuidv4(),\n data: notificationRequest,\n initiator: jwt,\n initiatorType: InitiatorType.EXTERNAL,\n system: System.OID4VCI,\n subsystem: SubSystem.API,\n })\n try {\n const jwtResult = await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback })\n const accessToken = jwtResult.jwt.payload as AccessTokenRequest\n const errorOrSession = await issuer.processNotification({\n preAuthorizedCode: accessToken['pre-authorized_code'],\n /*TODO: authorizationCode*/ notification: notificationRequest,\n })\n if (errorOrSession instanceof Error) {\n EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, {\n eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR,\n id: uuidv4(),\n data: notificationRequest,\n initiator: jwtResult.jwt,\n initiatorType: InitiatorType.EXTERNAL,\n system: System.OID4VCI,\n subsystem: SubSystem.API,\n })\n return sendErrorResponse(response, 400, errorOrSession.message)\n } else {\n EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, {\n eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED,\n id: uuidv4(),\n data: notificationRequest,\n initiator: jwtResult.jwt,\n initiatorType: InitiatorType.EXTERNAL,\n system: System.OID4VCI,\n subsystem: SubSystem.API,\n })\n }\n } catch (e) {\n LOG.warning(e)\n return sendErrorResponse(response, 400, {\n error: 'invalid_token',\n })\n }\n return response.status(204).send()\n } catch (e) {\n return sendErrorResponse(\n response,\n 400,\n {\n error: 'invalid_notification_request',\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n}\n\nexport function nonceEndpoint(router: Router, issuer: VcIssuer, opts: INonceEndpointOpts) {\n const endpoint = issuer.issuerMetadata.nonce_endpoint\n const baseUrl = getBaseUrl(opts.baseUrl)\n\n if (!endpoint) {\n LOG.warning('Nonce endpoint disabled as no \"nonce_endpoint\" has been configured in issuer metadata')\n return\n }\n\n const path = determinePath(baseUrl, endpoint, { stripBasePath: true })\n LOG.log(`[OID4VCI] nonce endpoint enabled at ${path}`)\n\n router.post(path, async (request: Request, response: Response) => {\n try {\n const cNonce = uuidv4()\n const cNonceExpiresIn = issuer.cNonceExpiresIn || 300\n\n const createdAt = +Date.now()\n const expiresAt = createdAt + Math.abs(cNonceExpiresIn) * 1000\n\n // Create nonce state - only include session identifiers if available\n const cNonceState: any = {\n cNonce,\n createdAt,\n expiresAt,\n }\n\n await issuer.cNonces.set(cNonce, cNonceState)\n\n return response.json({\n c_nonce: cNonce,\n c_nonce_expires_in: cNonceExpiresIn,\n })\n } catch (e) {\n return sendErrorResponse(\n response,\n 500,\n {\n error: 'server_error',\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n}\n\nexport function getCredentialOfferEndpoint(router: Router, issuer: VcIssuer, opts?: IGetCredentialOfferEndpointOpts) {\n const path = determinePath(opts?.baseUrl, opts?.path ?? '/webapp/credential-offers/:id', { stripBasePath: true })\n LOG.log(`[OID4VCI] getCredentialOffer endpoint enabled at ${path}`)\n router.get(path, async (request: Request, response: Response) => {\n try {\n const { id } = request.params\n const session = await issuer.getCredentialOfferSessionById(id)\n if (!session || !session.credentialOffer) {\n return sendErrorResponse(response, 404, {\n error: 'invalid_request',\n error_description: `Credential offer ${id} not found`,\n })\n }\n return response.json(session.credentialOffer.credential_offer)\n } catch (e) {\n return sendErrorResponse(\n response,\n 500,\n {\n error: 'invalid_request',\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n}\n\nexport function deleteCredentialOfferEndpoint(router: Router, issuer: VcIssuer, opts?: IGetCredentialOfferEndpointOpts) {\n const path = determinePath(opts?.baseUrl, opts?.path ?? '/webapp/credential-offers/:id', { stripBasePath: true })\n LOG.log(`[OID4VCI] deleteCredentialOffer endpoint enabled at ${path}`)\n router.delete(path, async (request: Request, response: Response) => {\n try {\n const { id } = request.params\n if (!id) {\n return sendErrorResponse(response, 400, {\n error: 'invalid_request',\n error_description: 'id must be present',\n })\n }\n await issuer.deleteCredentialOfferSessionById(id)\n return response.sendStatus(204)\n } catch (e) {\n return sendErrorResponse(\n response,\n 500,\n {\n error: 'invalid_request',\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n}\n\nfunction buildCredentialOfferReferenceUri(request: Request<CredentialOfferRESTRequestV1_0_15>, offerReferencePath?: string) {\n if (!offerReferencePath) {\n return Promise.reject(Error('issuePayloadPath must bet set for offerMode REFERENCE!'))\n }\n\n const protocol = request.headers['x-forwarded-proto']?.toString() ?? request.protocol\n let host = request.headers['x-forwarded-host']?.toString() ?? request.get('host')\n const forwardedPort = request.headers['x-forwarded-port']?.toString()\n\n if (forwardedPort && !(protocol === 'https' && forwardedPort === '443') && !(protocol === 'http' && forwardedPort === '80')) {\n host += `:${forwardedPort}`\n }\n\n const forwardedPrefix = request.headers['x-forwarded-prefix']?.toString() ?? ''\n\n return `${protocol}://${host}${forwardedPrefix}${request.baseUrl}${offerReferencePath}`\n}\n\nexport function createCredentialOfferEndpoint(\n router: Router,\n issuer: VcIssuer,\n opts?: ICreateCredentialOfferEndpointOpts & { baseUrl?: string },\n issuerPayloadPath?: string, // backwards compat, sigh\n) {\n const path = determinePath(opts?.baseUrl, opts?.path ?? '/webapp/credential-offers', { stripBasePath: true })\n const offerReferencePath =\n opts?.credentialOfferReferenceBasePath ?? issuerPayloadPath ?? determinePath(opts?.baseUrl, '/credential-offers', { stripBasePath: true })\n\n LOG.log(`[OID4VCI] createCredentialOffer endpoint enabled at ${path}`)\n router.post(path, async (request: Request<CredentialOfferRESTRequestV1_0_15>, response: Response<ICreateCredentialOfferURIResponse>) => {\n try {\n // const specVersion = determineSpecVersionFromOffer(request.body.original_credential_offer)\n // if (specVersion < OpenId4VCIVersion.VER_1_0_15) {\n // return sendErrorResponse(response, 400, {\n // error: TokenErrorResponse.invalid_client,\n // error_description: 'credential offer request should be of spec version 1.0.15 or above',\n // })\n // }\n\n const grantTypes = determineGrantTypes(request.body)\n if (grantTypes.length === 0) {\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_grant,\n error_description: 'No grant type supplied',\n })\n }\n const grants = request.body.grants as Grant\n const credentialConfigIds = request.body.credential_configuration_ids as string[]\n if (!credentialConfigIds || credentialConfigIds.length === 0) {\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_request,\n error_description: 'credential_configuration_ids missing credential_configuration_ids in credential offer payload',\n })\n }\n const qrCodeOpts = request.body.qrCodeOpts ?? opts?.qrCodeOpts\n const offerMode: CredentialOfferMode = request.body.offerMode ?? opts?.defaultCredentialOfferMode ?? 'VALUE' // default to existing mode when nothing specified\n\n const client_id: string | undefined = request.body.client_id ?? request.body.original_credential_offer?.client_id\n const result = await issuer.createCredentialOfferURI({\n ...request.body,\n offerMode,\n client_id,\n ...(request.body.correlationId && { correlationId: request.body.correlationId }),\n ...(offerMode === 'REFERENCE' && { credentialOfferUri: buildCredentialOfferReferenceUri(request, offerReferencePath) }),\n qrCodeOpts,\n grants,\n })\n const resultResponse: ICreateCredentialOfferURIResponse = result\n if ('session' in resultResponse) {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n delete resultResponse.session\n }\n return response.json(resultResponse)\n } catch (e) {\n return sendErrorResponse(\n response,\n 500,\n {\n error: TokenErrorResponse.invalid_request,\n error_description: (e as Error).message,\n },\n e,\n )\n }\n })\n}\n\nexport function pushedAuthorizationEndpoint(\n router: Router,\n issuer: VcIssuer,\n authRequestsData: Map<string, AuthorizationRequest>,\n opts?: ISingleEndpointOpts,\n) {\n const externalAS = isExternalAS(issuer.issuerMetadata) || issuer.asClientOpts\n if (externalAS) {\n LOG.log(\n `[OID4VCI] External Authorization Server ${issuer.issuerMetadata.authorization_servers} is being used. Not enabling internal PAR endpoint`,\n )\n return\n } else if (opts?.enabled === false) {\n LOG.log(`[OID4VCI] Internal PAR endpoint is not enabled`)\n return\n }\n\n const handleHttpStatus400 = async (req: Request, res: Response, next: NextFunction) => {\n if (!req.body) {\n return res.status(400).json({ error: 'invalid_request', error_description: 'Request body must be present' })\n }\n const required = ['client_id', 'code_challenge_method', 'code_challenge', 'redirect_uri']\n const conditional = ['authorization_details', 'scope']\n try {\n validateRequestBody({ required, conditional, body: req.body })\n } catch (e: unknown) {\n return sendErrorResponse(res, 400, {\n error: 'invalid_request',\n error_description: (e as Error).message,\n })\n }\n return next()\n }\n\n router.post('/par', handleHttpStatus400, (req: Request, res: Response) => {\n // FIXME Fake client for testing, it needs to come from a registered client\n const client = {\n scope: ['openid', 'test'],\n redirectUris: ['http://localhost:8080/*', 'https://www.test.com/*', 'https://test.nl', 'http://*/chart', 'http:*'],\n }\n\n // For security reasons the redirect_uri from the request needs to be matched against the ones present in the registered client\n const matched = client.redirectUris.filter((s: string) => new RegExp(s.replace('*', '.*')).test(req.body.redirect_uri))\n if (!matched.length) {\n return sendErrorResponse(res, 400, {\n error: 'invalid_request',\n error_description: 'redirect_uri is not valid for the given client',\n })\n }\n\n // The scopes from the request need to be matched against the ones present in the registered client\n if (!req.body.scope.split(',').every((scope: string) => client.scope.includes(scope))) {\n return sendErrorResponse(res, 400, {\n error: 'invalid_scope',\n error_description: 'scope is not valid for the given client',\n })\n }\n\n // Add the authorization_details validation here:\n if (req.body.authorization_details) {\n const authDetails = Array.isArray(req.body.authorization_details) ? req.body.authorization_details : JSON.parse(req.body.authorization_details)\n\n // Validate each authorization detail\n for (const detail of authDetails) {\n if (detail.type !== 'openid_credential') {\n return sendErrorResponse(res, 400, {\n error: 'invalid_authorization_details',\n error_description: 'Only openid_credential type is supported',\n })\n }\n\n // Validate credential_configuration_id exists in issuer metadata\n if (detail.credential_configuration_id && !issuer.issuerMetadata.credential_configurations_supported[detail.credential_configuration_id]) {\n return sendErrorResponse(res, 400, {\n error: 'invalid_credential_request',\n error_description: `Unsupported credential configuration: ${detail.credential_configuration_id}`,\n })\n }\n }\n }\n\n // TODO: Both UUID and requestURI need to be configurable for the server\n const uuid = uuidv4()\n const requestUri = `urn:ietf:params:oauth:request_uri:${uuid}`\n\n // Store authorization_details in the request for later retrieval\n let requestData = req.body\n if (req.body.authorization_details) {\n const authDetails = Array.isArray(req.body.authorization_details) ? req.body.authorization_details : JSON.parse(req.body.authorization_details)\n\n requestData = {\n ...req.body,\n authorization_details: authDetails, // Store parsed authorization details\n }\n }\n\n authRequestsData.set(requestUri, requestData)\n\n // Invalidates the request_uri removing it from the mapping after it is expired, needs to be refactored because\n // some of the properties will be needed in subsequent steps if the authorization succeeds\n // TODO in the /token endpoint the code_challenge must be matched against the hashed code_verifier\n setTimeout(() => {\n authRequestsData.delete(requestUri)\n }, expiresIn * 1000)\n\n return res.status(201).json({ request_uri: requestUri, expires_in: expiresIn })\n })\n}\n\nexport function getMetadataEndpoints(\n router: Router,\n issuer: VcIssuer,\n opts?: {\n rootRouter?: Router\n basePath?: string\n wellKnownHostLocation?: WellKnownHostLocation\n },\n) {\n const credentialIssuerHandler = (request: Request, response: Response) => {\n return response.json(issuer.issuerMetadata)\n }\n\n const authorizationServerHandler = (request: Request, response: Response) => {\n return response.json(issuer.authorizationServerMetadata)\n }\n\n const location = opts?.wellKnownHostLocation ?? WellKnownHostLocation.AT_BOTH\n\n // Register endpoints on context router if configured\n if (location === WellKnownHostLocation.AT_CONTEXT_PATH || location === WellKnownHostLocation.AT_BOTH) {\n router.get(WellKnownEndpoints.OPENID4VCI_ISSUER, credentialIssuerHandler)\n router.get(WellKnownEndpoints.OAUTH_AS, authorizationServerHandler)\n }\n\n // Register endpoints on root router if configured\n if (\n opts?.rootRouter &&\n opts?.basePath &&\n opts.basePath !== '/' &&\n (location === WellKnownHostLocation.AT_ROOT_PATH || location === WellKnownHostLocation.AT_BOTH)\n ) {\n opts.rootRouter.get(`/.well-known/openid-credential-issuer${opts.basePath}`, credentialIssuerHandler)\n opts.rootRouter.get(`/.well-known/oauth-authorization-server${opts.basePath}`, authorizationServerHandler)\n }\n}\n\nexport function determinePath(\n baseUrl: URL | string | undefined,\n endpoint: string,\n opts?: { skipBaseUrlCheck?: boolean; prependUrl?: string; stripBasePath?: boolean },\n) {\n const basePath = baseUrl ? getBasePath(baseUrl) : ''\n let path = endpoint\n if (opts?.prependUrl) {\n path = adjustUrl(path, { prepend: opts.prependUrl })\n }\n if (opts?.skipBaseUrlCheck !== true) {\n assertEndpointHasIssuerBaseUrl(baseUrl, endpoint)\n }\n if (endpoint.includes('://')) {\n path = new URL(endpoint).pathname\n }\n path = `/${trimBoth(path, '/')}`\n if (opts?.stripBasePath && path.startsWith(basePath)) {\n path = trimStart(path, basePath)\n path = `/${trimBoth(path, '/')}`\n }\n return path\n}\n\nfunction assertEndpointHasIssuerBaseUrl(baseUrl: URL | string | undefined, endpoint: string) {\n if (!validateEndpointHasIssuerBaseUrl(baseUrl, endpoint)) {\n throw Error(`endpoint '${endpoint}' does not have base url '${baseUrl ? getBaseUrl(baseUrl) : '<no baseurl supplied>'}'`)\n }\n}\n\nfunction validateEndpointHasIssuerBaseUrl(baseUrl: URL | string | undefined, endpoint: string): boolean {\n if (!endpoint) {\n return false\n } else if (!endpoint.includes('://')) {\n return true //absolute or relative path, not containing a hostname\n } else if (!baseUrl) {\n return true\n }\n return endpoint.startsWith(getBaseUrl(baseUrl))\n}\n\nexport function getBaseUrl(url?: URL | string | undefined) {\n let baseUrl = url\n if (!baseUrl) {\n const envUrl = env('BASE_URL', process?.env?.ENV_PREFIX)\n if (envUrl && envUrl.length > 0) {\n baseUrl = new URL(envUrl)\n }\n }\n if (!baseUrl) {\n throw Error(`No base URL provided`)\n }\n return trimEnd(baseUrl.toString(), '/')\n}\n\nexport function getBasePath(url?: URL | string) {\n const basePath = new URL(getBaseUrl(url)).pathname\n if (basePath === '' || basePath === '/') {\n return ''\n }\n return `/${trimBoth(basePath, '/')}`\n}\n","import { DPoPVerifyJwtCallback, JWK, uuidv4, verifyDPoP } from '@sphereon/oid4vc-common'\nimport { AuthorizationRequest, GrantTypes, PRE_AUTHORIZED_CODE_REQUIRED_ERROR, TokenError, TokenErrorResponse } from '@sphereon/oid4vci-common'\nimport { assertValidAccessTokenRequest, createAccessTokenResponse, ITokenEndpointOpts, VcIssuer } from '@sphereon/oid4vci-issuer'\nimport { sendErrorResponse } from '@sphereon/ssi-express-support'\nimport { NextFunction, Request, Response } from 'express'\n\n/**\n *\n * @param tokenExpiresIn\n * @param accessTokenSignerCallback\n * @param accessTokenIssuer\n * @param cNonceExpiresIn\n * @param issuer\n * @param interval\n */\nexport const handleTokenRequest = ({\n tokenExpiresIn, // expiration in seconds\n accessTokenEndpoint,\n accessTokenSignerCallback,\n accessTokenIssuer,\n cNonceExpiresIn, // expiration in seconds\n issuer,\n interval,\n dpop,\n}: Required<Pick<ITokenEndpointOpts, 'accessTokenIssuer' | 'cNonceExpiresIn' | 'interval' | 'accessTokenSignerCallback' | 'tokenExpiresIn'>> & {\n issuer: VcIssuer\n dpop?: {\n requireDPoP?: boolean\n dPoPVerifyJwtCallback: DPoPVerifyJwtCallback\n }\n // The full URL of the access token endpoint\n accessTokenEndpoint?: string\n}) => {\n return async (request: Request, response: Response) => {\n response.set({\n 'Cache-Control': 'no-store',\n Pragma: 'no-cache',\n })\n\n if (request.body.grant_type !== GrantTypes.PRE_AUTHORIZED_CODE) {\n // Yes this is redundant, only here to remind us that we need to implement the auth flow as well\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_request,\n error_description: PRE_AUTHORIZED_CODE_REQUIRED_ERROR,\n })\n }\n\n if (request.headers.authorization && request.headers.authorization.startsWith('DPoP ') && !request.headers.DPoP) {\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_request,\n error_description: 'DPoP header is required',\n })\n }\n\n let dPoPJwk: JWK | undefined\n if (dpop?.requireDPoP && !request.headers.dpop) {\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_request,\n error_description: 'DPoP is required for requesting access tokens.',\n })\n }\n\n if (request.headers.dpop) {\n if (!dpop) {\n console.error('Receiv