{"version":3,"file":"credentialEndpoint.mjs","names":[],"sources":["../../../src/openid4vc-issuer/router/credentialEndpoint.ts"],"sourcesContent":["import { joinUriParts, utils } from '@credo-ts/core'\nimport type { HttpMethod } from '@openid4vc/oauth2'\nimport { Oauth2ErrorCodes, Oauth2ResourceUnauthorizedError, Oauth2ServerErrorResponseError } from '@openid4vc/oauth2'\nimport {\n  type CredentialConfigurationsSupportedWithFormats,\n  getCredentialConfigurationsMatchingRequestFormat,\n  Openid4vciVersion,\n} from '@openid4vc/openid4vci'\nimport type { Response, Router } from 'express'\nimport { getCredentialConfigurationsSupportedForScopes } from '../../shared'\nimport {\n  getRequestContext,\n  sendJsonResponse,\n  sendOauth2ErrorResponse,\n  sendUnauthorizedError,\n  sendUnknownServerErrorResponse,\n} from '../../shared/router'\nimport { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState'\nimport type { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig'\nimport { OpenId4VcIssuerService } from '../OpenId4VcIssuerService'\nimport { OpenId4VcIssuanceSessionRecord, OpenId4VcIssuanceSessionRepository } from '../repository'\nimport type { OpenId4VcIssuanceRequest } from './requestContext'\n\nexport function configureCredentialEndpoint(router: Router, config: OpenId4VcIssuerModuleConfig) {\n  router.post(config.credentialEndpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => {\n    const { agentContext, issuer } = getRequestContext(request)\n    const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService)\n    const issuerMetadata = await openId4VcIssuerService.getIssuerMetadata(agentContext, issuer, true)\n    const vcIssuer = openId4VcIssuerService.getIssuer(agentContext)\n    const resourceServer = openId4VcIssuerService.getResourceServer(agentContext, issuer)\n\n    const fullRequestUrl = joinUriParts(issuerMetadata.credentialIssuer.credential_issuer, [\n      config.credentialEndpointPath,\n    ])\n    const resourceRequestResult = await resourceServer\n      .verifyResourceRequest({\n        authorizationServers: issuerMetadata.authorizationServers,\n        resourceServer: issuerMetadata.credentialIssuer.credential_issuer,\n        request: {\n          headers: new Headers(request.headers as Record<string, string>),\n          method: request.method as HttpMethod,\n          url: fullRequestUrl,\n        },\n      })\n      .catch((error) => {\n        sendUnauthorizedError(response, next, agentContext.config.logger, error)\n      })\n    if (!resourceRequestResult) return\n    const { tokenPayload, accessToken, scheme, authorizationServer } = resourceRequestResult\n\n    const credentialRequest = request.body\n    const issuanceSessionRepository = agentContext.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository)\n\n    const parsedCredentialRequest = vcIssuer.parseCredentialRequest({\n      credentialRequest,\n      issuerMetadata,\n    })\n\n    let issuanceSession: OpenId4VcIssuanceSessionRecord | null = null\n    const preAuthorizedCode =\n      typeof tokenPayload['pre-authorized_code'] === 'string' ? tokenPayload['pre-authorized_code'] : undefined\n    const issuerState = typeof tokenPayload.issuer_state === 'string' ? tokenPayload.issuer_state : undefined\n\n    const subject = tokenPayload.sub\n    if (!subject) {\n      return sendOauth2ErrorResponse(\n        response,\n        next,\n        agentContext.config.logger,\n        new Oauth2ServerErrorResponseError(\n          {\n            error: Oauth2ErrorCodes.ServerError,\n          },\n          {\n            internalMessage: `Received token without 'sub' claim. Subject is required for binding issuance session`,\n          }\n        )\n      )\n    }\n\n    // Already handle request without format/credential_configuration_id. Simplifies next code sections\n    if (!parsedCredentialRequest.format && !parsedCredentialRequest.credentialConfiguration) {\n      return sendOauth2ErrorResponse(\n        response,\n        next,\n        agentContext.config.logger,\n        new Oauth2ServerErrorResponseError({\n          error: parsedCredentialRequest.credentialIdentifier\n            ? Oauth2ErrorCodes.InvalidCredentialRequest\n            : Oauth2ErrorCodes.UnsupportedCredentialFormat,\n          error_description: parsedCredentialRequest.credentialIdentifier\n            ? `Credential request containing 'credential_identifier' not supported`\n            : parsedCredentialRequest.credentialConfigurationId\n              ? `Credential configuration '${parsedCredentialRequest.credentialConfigurationId}' not supported`\n              : `Credential format '${parsedCredentialRequest.credentialRequest.format}' not supported`,\n        })\n      )\n    }\n\n    if (preAuthorizedCode || issuerState) {\n      issuanceSession = await issuanceSessionRepository.findSingleByQuery(agentContext, {\n        issuerId: issuer.issuerId,\n        preAuthorizedCode,\n        issuerState,\n      })\n\n      if (!issuanceSession) {\n        agentContext.config.logger.warn(\n          `No issuance session found for incoming credential request for issuer ${\n            issuer.issuerId\n          } but access token data has ${\n            issuerState ? 'issuer_state' : 'pre-authorized_code'\n          }. Returning error response`,\n          {\n            tokenPayload,\n          }\n        )\n\n        return sendOauth2ErrorResponse(\n          response,\n          next,\n          agentContext.config.logger,\n          new Oauth2ServerErrorResponseError(\n            {\n              error: Oauth2ErrorCodes.CredentialRequestDenied,\n            },\n            {\n              internalMessage: `No issuance session found for incoming credential request for issuer ${issuer.issuerId} and access token data`,\n            }\n          )\n        )\n      }\n\n      // Use issuance session dpop config\n      if (issuanceSession.dpop?.required && !resourceRequestResult.dpop) {\n        return sendUnauthorizedError(\n          response,\n          next,\n          agentContext.config.logger,\n          new Oauth2ResourceUnauthorizedError('Missing required DPoP proof', {\n            scheme,\n            error: Oauth2ErrorCodes.InvalidDpopProof,\n          })\n        )\n      }\n\n      const expiresAt =\n        issuanceSession.expiresAt ??\n        utils.addSecondsToDate(issuanceSession.createdAt, config.statefulCredentialOfferExpirationInSeconds)\n\n      // Verify the issuance session subject\n      if (issuanceSession.authorization?.subject) {\n        if (issuanceSession.authorization.subject !== tokenPayload.sub) {\n          return sendOauth2ErrorResponse(\n            response,\n            next,\n            agentContext.config.logger,\n            new Oauth2ServerErrorResponseError(\n              {\n                error: Oauth2ErrorCodes.CredentialRequestDenied,\n              },\n              {\n                internalMessage: `Issuance session authorization subject does not match with the token payload subject for issuance session '${issuanceSession.id}'. Returning error response`,\n              }\n            )\n          )\n        }\n      }\n\n      // Stateful session expired\n      else if (Date.now() > expiresAt.getTime()) {\n        issuanceSession.errorMessage = 'Credential offer has expired'\n        await openId4VcIssuerService.updateState(agentContext, issuanceSession, OpenId4VcIssuanceSessionState.Error)\n        return sendOauth2ErrorResponse(\n          response,\n          next,\n          agentContext.config.logger,\n          new Oauth2ServerErrorResponseError({\n            // What is the best error here?\n            error: Oauth2ErrorCodes.CredentialRequestDenied,\n            error_description: 'Session expired',\n          })\n        )\n      } else {\n        issuanceSession.authorization = {\n          ...issuanceSession.authorization,\n          subject: tokenPayload.sub,\n        }\n        await issuanceSessionRepository.update(agentContext, issuanceSession)\n      }\n    }\n\n    if (!issuanceSession && config.allowDynamicIssuanceSessions) {\n      agentContext.config.logger.warn(\n        `No issuance session found for incoming credential request for issuer ${issuer.issuerId} and access token data has no issuer_state or pre-authorized_code. Creating on-demand issuance session`,\n        {\n          tokenPayload,\n        }\n      )\n\n      // Use global config when creating a dynamic session\n      if (config.dpopRequired && !resourceRequestResult.dpop) {\n        return sendUnauthorizedError(\n          response,\n          next,\n          agentContext.config.logger,\n          new Oauth2ResourceUnauthorizedError('Missing required DPoP proof', {\n            scheme,\n            error: Oauth2ErrorCodes.InvalidDpopProof,\n          })\n        )\n      }\n\n      const configurationsForScope = getCredentialConfigurationsSupportedForScopes(\n        issuerMetadata.credentialIssuer.credential_configurations_supported,\n        tokenPayload.scope?.split(' ') ?? []\n      )\n\n      // All credential configurations that match the request scope and credential request\n      // This is just so we don't create an issuance session that will fail immediately after\n      let configurationsForToken: CredentialConfigurationsSupportedWithFormats = {}\n\n      if (parsedCredentialRequest.credentialConfigurationId && parsedCredentialRequest.credentialConfiguration) {\n        if (configurationsForScope[parsedCredentialRequest.credentialConfigurationId]) {\n          configurationsForToken = {\n            [parsedCredentialRequest.credentialConfigurationId]: parsedCredentialRequest.credentialConfiguration,\n          }\n        }\n      } else if (parsedCredentialRequest.format) {\n        configurationsForToken = getCredentialConfigurationsMatchingRequestFormat({\n          issuerMetadata,\n          requestFormat: parsedCredentialRequest.format,\n        })\n      }\n\n      if (Object.keys(configurationsForToken).length === 0) {\n        return sendUnauthorizedError(\n          response,\n          next,\n          agentContext.config.logger,\n          new Oauth2ResourceUnauthorizedError(\n            'No credential configurations match credential request and access token scope',\n            {\n              scheme,\n              error: Oauth2ErrorCodes.InsufficientScope,\n            }\n          ),\n          // Forbidden for InsufficientScope\n          403\n        )\n      }\n\n      const createdAt = new Date()\n      const expiresAt = utils.addSecondsToDate(createdAt, config.statefulCredentialOfferExpirationInSeconds)\n\n      issuanceSession = new OpenId4VcIssuanceSessionRecord({\n        createdAt,\n        expiresAt,\n        credentialOfferPayload: {\n          credential_configuration_ids: Object.keys(configurationsForToken),\n          credential_issuer: issuerMetadata.credentialIssuer.credential_issuer,\n        },\n        credentialOfferId: utils.uuid(),\n        issuerId: issuer.issuerId,\n        state: OpenId4VcIssuanceSessionState.CredentialRequestReceived,\n        clientId: tokenPayload.client_id,\n        dpop: config.dpopRequired\n          ? {\n              required: true,\n            }\n          : undefined,\n        authorization: {\n          subject: tokenPayload.sub,\n        },\n        openId4VciVersion:\n          issuerMetadata.originalDraftVersion === Openid4vciVersion.V1\n            ? 'v1'\n            : issuerMetadata.originalDraftVersion === Openid4vciVersion.Draft15\n              ? 'v1.draft15'\n              : 'v1.draft11-14',\n      })\n\n      // Save and update\n      await issuanceSessionRepository.save(agentContext, issuanceSession)\n      openId4VcIssuerService.emitStateChangedEvent(agentContext, issuanceSession, null)\n    } else if (!issuanceSession) {\n      return sendOauth2ErrorResponse(\n        response,\n        next,\n        agentContext.config.logger,\n        new Oauth2ServerErrorResponseError(\n          {\n            error: Oauth2ErrorCodes.CredentialRequestDenied,\n          },\n          {\n            internalMessage: `Access token without 'issuer_state' or 'pre-authorized_code' issued by external authorization server provided, but 'allowDynamicIssuanceSessions' is disabled. Either bind the access token to a stateful credential offer, or enable 'allowDynamicIssuanceSessions'.`,\n          }\n        )\n      )\n    }\n\n    try {\n      const { credentialResponse } = await openId4VcIssuerService.createCredentialResponse(agentContext, {\n        issuanceSession,\n        credentialRequest,\n        authorization: {\n          authorizationServer,\n          accessToken: {\n            payload: tokenPayload,\n            value: accessToken,\n          },\n        },\n      })\n\n      return sendJsonResponse(\n        response,\n        next,\n        credentialResponse,\n        undefined,\n        credentialResponse.transaction_id ? 202 : 200\n      )\n    } catch (error) {\n      issuanceSession.errorMessage =\n        typeof error.message === 'string' && error.message !== ''\n          ? error.message\n          : 'Failed to create a credential response'\n      await openId4VcIssuerService.updateState(agentContext, issuanceSession, OpenId4VcIssuanceSessionState.Error)\n\n      if (error instanceof Oauth2ServerErrorResponseError) {\n        return sendOauth2ErrorResponse(response, next, agentContext.config.logger, error)\n      }\n      if (error instanceof Oauth2ResourceUnauthorizedError) {\n        return sendUnauthorizedError(response, next, agentContext.config.logger, error)\n      }\n\n      return sendUnknownServerErrorResponse(response, next, agentContext.config.logger, error)\n    }\n  })\n}\n"],"mappings":"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"}