UNPKG

@sphereon/oid4vci-issuer-server

Version:

OpenID 4 Verifiable Credential Issuance Server

1 lines • 74.8 kB
{"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 { ClientResponseType, ClientAuthMethod, ClientMetadata } from '@sphereon/oid4vci-common'\n","import {\n AuthorizationRequest,\n ClientMetadata,\n CreateCredentialOfferURIResult,\n CredentialConfigurationSupportedV1_0_13,\n CredentialOfferMode,\n IssuerCredentialSubjectDisplay,\n OID4VCICredentialFormat,\n QRCodeOpts,\n} from '@sphereon/oid4vci-common'\nimport {\n CredentialSupportedBuilderV1_13,\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 pushedAuthorizationEndpoint,\n} from './oid4vci-api-functions'\n\nfunction buildVCIFromEnvironment() {\n const credentialsSupported: Record<string, CredentialConfigurationSupportedV1_0_13> = new CredentialSupportedBuilderV1_13()\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 .addCredentialSubjectPropertyDisplay(\n process.env.credential_subject_display_key1 as string,\n {\n name: process.env.credential_subject_display_key1_name as string,\n locale: process.env.credential_subject_display_key1_locale as string,\n } as IssuerCredentialSubjectDisplay, // fixme: This is wrong (remove the cast and see it has no matches)\n )\n .build()\n const issuerBuilder = new VcIssuerBuilder()\n .withTXCode({ length: process.env.user_pin_length as unknown as number, input_mode: process.env.user_pin_input_mode as 'numeric' | 'text' })\n .withAuthorizationServers(process.env.authorization_server as string)\n .withCredentialEndpoint(process.env.credential_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}\n\nexport interface IOID4VCIServerOpts extends HasEndpointOpts {\n asClientOpts?: ClientMetadata\n endpointOpts?: IOID4VCIEndpointOpts\n baseUrl?: string\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\n constructor(\n expressSupport: ExpressSupport,\n opts: IOID4VCIServerOpts & { issuer?: VcIssuer } /*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\n pushedAuthorizationEndpoint(this.router, this.issuer, this.authRequestsData)\n getMetadataEndpoints(this.router, this.issuer)\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, { ...opts?.endpointOpts?.tokenEndpointOpts, baseUrl: this.baseUrl })\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, { ...opts?.endpointOpts?.authorizationChallengeOpts, baseUrl: this.baseUrl })\n }\n this._app.use(getBasePath(this.baseUrl), 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 get baseUrl(): URL {\n return this._baseUrl\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_13,\n CredentialOfferMode,\n CredentialOfferRESTRequest,\n CredentialRequestV1_0_13,\n determineGrantTypes,\n determineSpecVersionFromOffer,\n EVENTS,\n extractBearerToken,\n generateRandomString,\n getNumberOrUndefined,\n Grant,\n IssueStatusResponse,\n JWT_SIGNER_CALLBACK_REQUIRED_ERROR,\n NotificationRequest,\n NotificationStatusEventNames,\n OpenId4VCIVersion,\n TokenErrorResponse,\n trimBoth,\n trimEnd,\n trimStart,\n validateJWT,\n WellKnownEndpoints,\n} from '@sphereon/oid4vci-common'\nimport { 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} 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_13) {\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(router: Router, issuer: VcIssuer, opts: ITokenEndpointOpts & ISingleEndpointOpts & { baseUrl: string | URL }) {\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 }),\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_13\n LOG.log(`credential request received`, credentialRequest)\n try {\n const jwt = extractBearerToken(request.header('Authorization'))\n await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback ?? issuer.jwtVerifyCallback })\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 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 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<CredentialOfferRESTRequest>, 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<CredentialOfferRESTRequest>, response: Response<ICreateCredentialOfferURIResponse>) => {\n try {\n const specVersion = determineSpecVersionFromOffer(request.body.original_credential_offer)\n if (specVersion < OpenId4VCIVersion.VER_1_0_13) {\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_client,\n error_description: 'credential offer request should be of spec version 1.0.13 or above',\n })\n }\n\n const grantTypes = determineGrantTypes(request.body)\n if (grantTypes.length === 0) {\n return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_grant, error_description: 'No grant type supplied' })\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 //TODO Implement authorization_details verification\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 // The redirect_uri is created and set in a map, to keep track of the actual request\n authRequestsData.set(requestUri, req.body)\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(router: Router, issuer: VcIssuer) {\n const credentialIssuerHandler = (request: Request, response: Response) => {\n return response.json(issuer.issuerMetadata)\n }\n router.get(WellKnownEndpoints.OPENID4VCI_ISSUER, credentialIssuerHandler)\n\n const authorizationServerHandler = (request: Request, response: Response) => {\n return response.json(issuer.authorizationServerMetadata)\n }\n router.get(WellKnownEndpoints.OAUTH_AS, authorizationServerHandler)\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 { 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('Received unsupported DPoP header. The issuer is not configured to work with DPoP. Provide DPoP options for it to work.')\n\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_request,\n error_description: 'Received unsupported DPoP header.',\n })\n }\n\n try {\n const fullUrl = accessTokenEndpoint ?? request.protocol + '://' + request.get('host') + request.originalUrl\n dPoPJwk = await verifyDPoP(\n { method: request.method, headers: request.headers, fullUrl },\n {\n jwtVerifyCallback: dpop.dPoPVerifyJwtCallback,\n expectAccessToken: false,\n maxIatAgeInSeconds: undefined,\n },\n )\n } catch (error) {\n return sendErrorResponse(response, 400, {\n error: TokenErrorResponse.invalid_dpop_proof,\n error_description: error instanceof Error ? error.message : 'Unknown error',\n })\n }\n }\n\n try {\n const responseBody = await createAccessTokenResponse(request.body, {\n credentialOfferSessions: issuer.credentialOfferSessions,\n accessTokenIssuer,\n cNonces: issuer.cNonces,\n cNonce: uuidv4(),\n accessTokenSignerCallback,\n cNonceExpiresIn,\n interval,\n tokenExpiresIn,\n dPoPJwk,\n })\n return response.status(200).json(responseBody)\n } catch (error) {\n return sendErrorResponse(\n response,\n 400,\n {\n error: TokenErrorResponse.invalid_request,\n },\n error,\n )\n }\n }\n}\n\nexport const verifyTokenRequest = ({\n preAuthorizedCodeExpirationDuration,\n issuer,\n}: Required<Pick<ITokenEndpointOpts, 'preAuthorizedCodeExpirationDuration'> & { issuer: VcIssuer }>) => {\n return async (request: Request, response: Response, next: NextFunction) => {\n try {\n await assertValidAccessTokenRequest(request.body, {\n expirationDuration: preAuthorizedCodeExpirationDuration,\n credentialOfferSessions: issuer.credentialOfferSessions,\n })\n } catch (error) {\n if (error instanceof TokenError) {\n return sendErrorResponse(response, error.statusCode, {\n error: error.responseError,\n error_description: error.getDescription(),\n })\n } else {\n return sendErrorResponse(response, 400, { error: TokenErrorResponse.invalid_request, error_description: (error as Error).message }, error)\n }\n }\n\n return next()\n }\n}\n","import { Request } from 'express'\n\nexport const validateRequestBody = ({\n required,\n conditional,\n body,\n}: {\n required?: string[]\n conditional?: string[]\n body: Pick<Request, 'body'>\n}): void => {\n const keys = Object.keys(body)\n let message\n if (required && !required.every((k) => keys.includes(k))) {\n message = `Request must contain ${required.toString()}`\n }\n if (conditional && !conditional.some((k) => keys.includes(k))) {\n message = message ? `and request must contain ether ${conditional.toString()}` : `Request must contain ether ${conditional.toString()}`\n }\n if (message) {\n throw new Error(message)\n }\n}\n"],"mappings":"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