{"version":3,"file":"redirectEndpoint.mjs","names":[],"sources":["../../../src/openid4vc-issuer/router/redirectEndpoint.ts"],"sourcesContent":["import { joinUriParts, Kms, TypedArrayEncoder } from '@credo-ts/core'\nimport {\n  Oauth2ClientErrorResponseError,\n  Oauth2ErrorCodes,\n  Oauth2ServerErrorResponseError,\n  verifyIdTokenJwt,\n} from '@openid4vc/oauth2'\nimport { addSecondsToDate } from '@openid4vc/utils'\nimport type { NextFunction, Response, Router } from 'express'\nimport { getOid4vcCallbacks } from '../../shared'\nimport { getRequestContext, sendOauth2ErrorResponse, sendUnknownServerErrorResponse } from '../../shared/router'\nimport { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState'\nimport { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig'\nimport { OpenId4VcIssuerService } from '../OpenId4VcIssuerService'\nimport type { OpenId4VcIssuanceSessionRecord } from '../repository'\nimport type { OpenId4VcIssuanceRequest } from './requestContext'\n\nexport function configureRedirectEndpoint(router: Router, config: OpenId4VcIssuerModuleConfig) {\n  router.get(\n    config.redirectEndpoint,\n    async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => {\n      const requestContext = getRequestContext(request)\n      const { agentContext, issuer } = requestContext\n      const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService)\n      const issuerMetadata = await openId4VcIssuerService.getIssuerMetadata(agentContext, issuer)\n      const oauth2Client = openId4VcIssuerService.getOauth2Client(agentContext, issuer)\n      const authorizationServerIssuer = issuerMetadata.authorizationServers[0].issuer\n\n      let issuanceSession: OpenId4VcIssuanceSessionRecord | null = null\n      try {\n        const fullRequestUrl = joinUriParts(issuerMetadata.credentialIssuer.credential_issuer, [request.originalUrl])\n\n        const authorizationResponse = oauth2Client.parseAuthorizationResponseRedirectUrl({\n          url: fullRequestUrl,\n        })\n\n        if (!authorizationResponse.state) {\n          throw new Oauth2ServerErrorResponseError({\n            // Server error because it's an error of the external IDP\n            error: Oauth2ErrorCodes.ServerError,\n            error_description: `Missing required 'state' parameter`,\n          })\n        }\n\n        issuanceSession = await openId4VcIssuerService.findSingleIssuanceSessionByQuery(agentContext, {\n          issuerId: issuer.issuerId,\n          chainedIdentityState: authorizationResponse.state,\n        })\n\n        if (!issuanceSession || issuanceSession.state !== OpenId4VcIssuanceSessionState.AuthorizationInitiated) {\n          throw new Oauth2ServerErrorResponseError(\n            {\n              error: Oauth2ErrorCodes.InvalidRequest,\n              error_description: `Invalid 'state' parameter`,\n            },\n            {\n              internalMessage: !issuanceSession\n                ? `Issuance session not found for identity chaining 'state' parameter '${authorizationResponse.state}'`\n                : `Issuance session '${issuanceSession.id}' has state '${\n                    issuanceSession.state\n                  }' but expected ${OpenId4VcIssuanceSessionState.AuthorizationInitiated}`,\n            }\n          )\n        }\n\n        if (\n          !issuanceSession.chainedIdentity?.externalAuthorizationServerUrl ||\n          !issuanceSession.chainedIdentity?.externalAuthorizationServerMetadata ||\n          !issuanceSession.chainedIdentity.redirectUri\n        ) {\n          throw new Oauth2ServerErrorResponseError(\n            {\n              error: Oauth2ErrorCodes.InvalidRequest,\n              error_description: 'The session is invalid or has expired.',\n            },\n            {\n              internalMessage: `Issuance session '${issuanceSession.id}' does not have identity chaining configured, so it's not compatible with the redirect endpoint.`,\n            }\n          )\n        }\n\n        oauth2Client.verifyAuthorizationResponse({\n          authorizationResponse,\n          authorizationServerMetadata: issuanceSession.chainedIdentity.externalAuthorizationServerMetadata,\n        })\n\n        // Throw the error. This will be caught and processed below.\n        if (authorizationResponse.error) {\n          throw new Oauth2ServerErrorResponseError(authorizationResponse)\n        }\n\n        if (!authorizationResponse.code) {\n          throw new Oauth2ServerErrorResponseError({\n            error: Oauth2ErrorCodes.ServerError,\n            error_description: `Missing required 'error' or 'code' parameter`,\n          })\n        }\n\n        const authorizationServerUrl = issuanceSession.chainedIdentity.externalAuthorizationServerUrl\n        const authorizationServerConfig = issuer.chainedAuthorizationServerConfigs?.find(\n          (config) => config.issuer === authorizationServerUrl\n        )\n        if (!authorizationServerConfig) {\n          throw new Oauth2ServerErrorResponseError(\n            {\n              error: Oauth2ErrorCodes.ServerError,\n            },\n            {\n              internalMessage: `Issuer '${issuer.issuerId}' does not have a chained authorization server config for issuer '${authorizationServerUrl}'`,\n            }\n          )\n        }\n\n        const authorizationServerMetadata = await oauth2Client.fetchAuthorizationServerMetadata(\n          authorizationServerConfig.issuer\n        )\n        if (!authorizationServerMetadata) {\n          throw new Oauth2ServerErrorResponseError(\n            {\n              error: Oauth2ErrorCodes.ServerError,\n              error_description: `Unable to retrieve authorization server metadata from external identity provider.`,\n            },\n            {\n              internalMessage: `Unable to retrieve authorization server metadata from '${authorizationServerConfig.issuer}'`,\n            }\n          )\n        }\n\n        // Retrieve access token\n        // TODO: add support for DPoP\n        const { accessTokenResponse } = await oauth2Client\n          .retrieveAuthorizationCodeAccessToken({\n            authorizationCode: authorizationResponse.code,\n            authorizationServerMetadata,\n            pkceCodeVerifier: issuanceSession.chainedIdentity.pkceCodeVerifier,\n            redirectUri: joinUriParts(config.baseUrl, [issuer.issuerId, 'redirect']),\n          })\n          .catch((error) => {\n            if (error instanceof Oauth2ClientErrorResponseError) {\n              switch (error.errorResponse.error) {\n                case Oauth2ErrorCodes.InvalidGrant:\n                  throw new Oauth2ServerErrorResponseError(\n                    {\n                      error: Oauth2ErrorCodes.InvalidGrant,\n                    },\n                    {\n                      internalMessage: `Invalid authorization code received from '${authorizationServerMetadata.issuer}'.`,\n                      cause: error,\n                    }\n                  )\n                case Oauth2ErrorCodes.AccessDenied:\n                  throw new Oauth2ServerErrorResponseError(\n                    {\n                      error: Oauth2ErrorCodes.AccessDenied,\n                    },\n                    {\n                      internalMessage: `The request has been denied by the user at '${authorizationServerMetadata.issuer}'.`,\n                      cause: error,\n                    }\n                  )\n              }\n            }\n\n            throw new Oauth2ServerErrorResponseError(\n              {\n                error: Oauth2ErrorCodes.ServerError,\n                error_description: 'Error processing authorization code',\n              },\n              {\n                internalMessage: `Error occurred during retrieval of access token from ${authorizationServerMetadata.issuer}.`,\n                cause: error,\n              }\n            )\n          })\n\n        // Verify the ID Token if 'openid' scope was requested\n        if (accessTokenResponse.scope?.split(' ').includes('openid')) {\n          const idToken = accessTokenResponse.id_token\n          if (typeof idToken !== 'string') {\n            throw new Oauth2ServerErrorResponseError(\n              {\n                error: Oauth2ErrorCodes.ServerError,\n                error_description: `Missing 'id_token' in access token response`,\n              },\n              {\n                internalMessage: `id_token is missing from access token response from ${authorizationServerMetadata.issuer} even though 'openid' scope was requested.`,\n              }\n            )\n          }\n\n          await verifyIdTokenJwt({\n            idToken,\n            authorizationServer: authorizationServerMetadata,\n            clientId: authorizationServerConfig.clientAuthentication.clientId,\n            callbacks: getOid4vcCallbacks(agentContext),\n          })\n        }\n\n        // Grant authorization\n        const kms = agentContext.resolve(Kms.KeyManagementApi)\n        const authorizationCode = TypedArrayEncoder.toBase64Url(kms.randomBytes({ length: 32 }))\n        const authorizationCodeExpiresAt = addSecondsToDate(new Date(), config.authorizationCodeExpiresInSeconds)\n\n        const redirectUri = new URL(issuanceSession.chainedIdentity.redirectUri)\n\n        // First authorization server is the internal authorization server (always used with chained authorization)\n        redirectUri.searchParams.set('iss', authorizationServerIssuer)\n        redirectUri.searchParams.set('code', authorizationCode)\n\n        if (issuanceSession.chainedIdentity.state) {\n          redirectUri.searchParams.set('state', issuanceSession.chainedIdentity.state)\n        }\n\n        // Update authorization information\n        issuanceSession.authorization = {\n          ...issuanceSession.authorization,\n          code: authorizationCode,\n          codeExpiresAt: authorizationCodeExpiresAt,\n        }\n\n        // Store access token response\n        issuanceSession.chainedIdentity = {\n          ...issuanceSession.chainedIdentity,\n          externalAccessTokenResponse: accessTokenResponse,\n        }\n\n        // TODO: we need to start using locks so we can't get corrupted state\n        await openId4VcIssuerService.updateState(\n          agentContext,\n          issuanceSession,\n          OpenId4VcIssuanceSessionState.AuthorizationGranted\n        )\n\n        return response.redirect(redirectUri.toString())\n      } catch (error) {\n        if (error instanceof Oauth2ServerErrorResponseError) {\n          // Redirect to the redirect URI if available.\n          if (issuanceSession?.chainedIdentity?.redirectUri) {\n            const redirectUri = new URL(issuanceSession.chainedIdentity.redirectUri)\n            redirectUri.searchParams.set('error', error.errorResponse.error)\n            redirectUri.searchParams.set('iss', authorizationServerIssuer)\n            if (error.errorResponse.error_description) {\n              redirectUri.searchParams.set('error_description', error.errorResponse.error_description)\n            }\n            if (issuanceSession.chainedIdentity.state) {\n              redirectUri.searchParams.set('state', issuanceSession.chainedIdentity.state)\n            }\n\n            agentContext.config.logger.warn(\n              `[OID4VC] Sending oauth2 error response: ${JSON.stringify(error.message)}`,\n              {\n                error,\n              }\n            )\n\n            return response.redirect(redirectUri.toString())\n          }\n\n          return sendOauth2ErrorResponse(response, next, agentContext.config.logger, error)\n        }\n\n        return sendUnknownServerErrorResponse(response, next, agentContext.config.logger, error)\n      }\n    }\n  )\n}\n"],"mappings":"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"}