{"version":3,"file":"accessTokenEndpoint.mjs","names":[],"sources":["../../../src/openid4vc-issuer/router/accessTokenEndpoint.ts"],"sourcesContent":["import { CredoError, joinUriParts, type Query, utils } from '@credo-ts/core'\nimport type { HttpMethod, Jwk, VerifyAccessTokenRequestReturn } from '@openid4vc/oauth2'\nimport {\n  authorizationCodeGrantIdentifier,\n  Oauth2ErrorCodes,\n  Oauth2ServerErrorResponseError,\n  preAuthorizedCodeGrantIdentifier,\n  refreshTokenGrantIdentifier,\n} from '@openid4vc/oauth2'\nimport type { NextFunction, Response, Router } from 'express'\nimport {\n  getRequestContext,\n  sendJsonResponse,\n  sendOauth2ErrorResponse,\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 configureAccessTokenEndpoint(router: Router, config: OpenId4VcIssuerModuleConfig) {\n  router.post(config.accessTokenEndpointPath, handleTokenRequest(config))\n}\n\nexport function handleTokenRequest(config: OpenId4VcIssuerModuleConfig) {\n  return async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => {\n    response.set({ 'Cache-Control': 'no-store', Pragma: 'no-cache' })\n    const requestContext = getRequestContext(request)\n    const { agentContext, issuer } = requestContext\n\n    try {\n      const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService)\n      const issuanceSessionRepository = agentContext.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository)\n      const issuerMetadata = await openId4VcIssuerService.getIssuerMetadata(agentContext, issuer)\n      const accessTokenSigningKey = issuer.resolvedAccessTokenPublicJwk\n      let oauth2AuthorizationServer = openId4VcIssuerService.getOauth2AuthorizationServer(agentContext)\n\n      const fullRequestUrl = joinUriParts(issuerMetadata.credentialIssuer.credential_issuer, [\n        config.accessTokenEndpointPath,\n      ])\n      const requestLike = {\n        headers: new Headers(request.headers as Record<string, string>),\n        method: request.method as HttpMethod,\n        url: fullRequestUrl,\n      } as const\n\n      const { accessTokenRequest, grant, dpop, clientAttestation, pkceCodeVerifier } =\n        oauth2AuthorizationServer.parseAccessTokenRequest({\n          accessTokenRequest: request.body,\n          request: requestLike,\n        })\n\n      let allowedStates: OpenId4VcIssuanceSessionState[]\n      let query: Query<OpenId4VcIssuanceSessionRecord>\n      let parsedRefreshToken: ReturnType<OpenId4VcIssuerService['parseRefreshToken']> | undefined\n\n      switch (grant.grantType) {\n        case preAuthorizedCodeGrantIdentifier:\n          allowedStates = [OpenId4VcIssuanceSessionState.OfferCreated, OpenId4VcIssuanceSessionState.OfferUriRetrieved]\n          query = { preAuthorizedCode: grant.preAuthorizedCode }\n          break\n        case authorizationCodeGrantIdentifier:\n          allowedStates = [OpenId4VcIssuanceSessionState.AuthorizationGranted]\n          query = { authorizationCode: grant.code }\n          break\n        case refreshTokenGrantIdentifier:\n          allowedStates = [\n            OpenId4VcIssuanceSessionState.CredentialRequestReceived,\n            OpenId4VcIssuanceSessionState.CredentialsPartiallyIssued,\n          ]\n          parsedRefreshToken = openId4VcIssuerService.parseRefreshToken(agentContext, grant.refreshToken)\n          query = {\n            preAuthorizedCode: parsedRefreshToken.preAuthorizedCode,\n            issuerState: parsedRefreshToken.issuerState,\n          }\n          break\n        default:\n          throw new Oauth2ServerErrorResponseError({\n            error: Oauth2ErrorCodes.UnsupportedGrantType,\n            error_description: 'Unsupported grant type',\n          })\n      }\n\n      const issuanceSession = await issuanceSessionRepository.findSingleByQuery(agentContext, query)\n      if (!issuanceSession || !allowedStates.includes(issuanceSession.state)) {\n        throw new Oauth2ServerErrorResponseError({\n          error: Oauth2ErrorCodes.InvalidGrant,\n          error_description: 'Invalid authorization code',\n        })\n      }\n\n      const expiresAt =\n        issuanceSession.expiresAt ??\n        utils.addSecondsToDate(issuanceSession.createdAt, config.statefulCredentialOfferExpirationInSeconds)\n\n      if (Date.now() > expiresAt.getTime()) {\n        issuanceSession.errorMessage = 'Credential offer has expired'\n        await openId4VcIssuerService.updateState(agentContext, issuanceSession, OpenId4VcIssuanceSessionState.Error)\n        throw new Oauth2ServerErrorResponseError({\n          // What is the best error here?\n          error: Oauth2ErrorCodes.InvalidGrant,\n          error_description: 'Session expired',\n        })\n      }\n\n      oauth2AuthorizationServer = openId4VcIssuerService.getOauth2AuthorizationServer(agentContext, {\n        issuanceSessionId: issuanceSession.id,\n      })\n      let verificationResult: VerifyAccessTokenRequestReturn\n\n      if (grant.grantType === preAuthorizedCodeGrantIdentifier) {\n        if (!issuanceSession.preAuthorizedCode) {\n          throw new Oauth2ServerErrorResponseError(\n            {\n              error: Oauth2ErrorCodes.InvalidGrant,\n              error_description: 'Invalid authorization code',\n            },\n            {\n              internalMessage:\n                'Found issuance session without preAuthorizedCode. This should not happen as the issuance session is fetched based on the pre authorized code',\n            }\n          )\n        }\n\n        verificationResult = await oauth2AuthorizationServer.verifyPreAuthorizedCodeAccessTokenRequest({\n          accessTokenRequest,\n          expectedPreAuthorizedCode: issuanceSession.preAuthorizedCode,\n          grant,\n          request: requestLike,\n          authorizationServerMetadata: issuerMetadata.authorizationServers[0],\n          clientAttestation: {\n            ...clientAttestation,\n            // First session config, fall back to global config\n            required: issuanceSession.walletAttestation?.required ?? config.walletAttestationsRequired,\n\n            // NOTE: we might want to enforce this? Not sure\n            // ensureConfirmationKeyMatchesDpopKey: true\n          },\n          dpop: {\n            ...dpop,\n            // First session config, fall back to global config\n            required: issuanceSession.dpop?.required ?? config.dpopRequired,\n          },\n          expectedTxCode: issuanceSession.userPin,\n          preAuthorizedCodeExpiresAt:\n            issuanceSession.expiresAt ??\n            utils.addSecondsToDate(issuanceSession.createdAt, config.statefulCredentialOfferExpirationInSeconds),\n        })\n      } else if (grant.grantType === authorizationCodeGrantIdentifier) {\n        if (!issuanceSession.authorization?.code || !issuanceSession.authorization?.codeExpiresAt) {\n          throw new Oauth2ServerErrorResponseError(\n            {\n              error: Oauth2ErrorCodes.InvalidGrant,\n              error_description: 'Invalid authorization code',\n            },\n            {\n              internalMessage:\n                'Found issuance session without authorization.code or authorization.codeExpiresAt. This should not happen as the issuance session is fetched based on the authorization code',\n            }\n          )\n        }\n        verificationResult = await oauth2AuthorizationServer.verifyAuthorizationCodeAccessTokenRequest({\n          accessTokenRequest,\n          expectedCode: issuanceSession.authorization.code,\n          codeExpiresAt: issuanceSession.authorization.codeExpiresAt,\n          grant,\n          authorizationServerMetadata: issuerMetadata.authorizationServers[0],\n          request: requestLike,\n          clientAttestation: {\n            ...clientAttestation,\n\n            // Ensure it matches the previously provided client id\n            // FIXME: we don't verify that the attestation is issued by the same party\n            expectedClientId: issuanceSession.clientId,\n\n            // NOTE: we don't look at the global config here. As we already checked and\n            // set required to true previously if client attestations were provided or required.\n            required: issuanceSession.walletAttestation?.required,\n\n            // NOTE: we might want to enforce this? Not sure\n            // ensureConfirmationKeyMatchesDpopKey: true\n          },\n          dpop: {\n            ...dpop,\n            // NOTE: we don't look at the global config here. As we already checked and\n            // set required to true previously if client attestations were provided or required.\n            required: issuanceSession.dpop?.required,\n\n            // Ensure it matches previously provided jwk thumbprint\n            expectedJwkThumbprint: issuanceSession.dpop?.dpopJkt,\n          },\n          pkce: issuanceSession.pkce\n            ? {\n                codeChallenge: issuanceSession.pkce.codeChallenge,\n                codeChallengeMethod: issuanceSession.pkce.codeChallengeMethod,\n                codeVerifier: pkceCodeVerifier,\n              }\n            : undefined,\n        })\n      } else if (grant.grantType === refreshTokenGrantIdentifier) {\n        if (!parsedRefreshToken) {\n          throw new CredoError('Refresh token verification is required for refresh token grant type')\n        }\n\n        verificationResult = await oauth2AuthorizationServer.verifyRefreshTokenAccessTokenRequest({\n          accessTokenRequest,\n          // Refresh token validity is already checked before\n          expectedRefreshToken: grant.refreshToken,\n          grant,\n          request: requestLike,\n          authorizationServerMetadata: issuerMetadata.authorizationServers[0],\n          clientAttestation: {\n            ...clientAttestation,\n            // First session config, fall back to global config\n            required: issuanceSession.walletAttestation?.required ?? config.walletAttestationsRequired,\n\n            // NOTE: we might want to enforce this? Not sure\n            // ensureConfirmationKeyMatchesDpopKey: true\n          },\n          dpop: {\n            ...dpop,\n            // First session config, fall back to global config\n            required: issuanceSession.dpop?.required ?? config.dpopRequired,\n          },\n          refreshTokenExpiresAt: parsedRefreshToken?.expiresAt,\n        })\n\n        await openId4VcIssuerService.verifyRefreshToken(agentContext, issuer, parsedRefreshToken, {\n          dpop: verificationResult.dpop,\n        })\n      } else {\n        throw new Oauth2ServerErrorResponseError({\n          error: Oauth2ErrorCodes.UnsupportedGrantType,\n          error_description: 'Unsupported grant type',\n        })\n      }\n\n      // Do not update the session state if the grant type is refresh token. This\n      // avoids the session state going \"backwards\".\n      if (grant.grantType !== refreshTokenGrantIdentifier) {\n        await openId4VcIssuerService.updateState(\n          agentContext,\n          issuanceSession,\n          OpenId4VcIssuanceSessionState.AccessTokenRequested\n        )\n      }\n\n      const { cNonce, cNonceExpiresInSeconds } = await openId4VcIssuerService.createNonce(agentContext, issuer)\n\n      // for authorization code flow we take the authorization scopes. For pre-auth we don't use scopes (we just\n      // use the offered credential configuration ids so a scope is not required)\n      const scopes =\n        grant.grantType === authorizationCodeGrantIdentifier ? issuanceSession.authorization?.scopes : undefined\n      const subject = `credo:${utils.uuid()}`\n\n      const tokenDpop = verificationResult.dpop\n        ? {\n            jwk: verificationResult.dpop?.jwk,\n          }\n        : undefined\n\n      // Generate a refresh token if they're enabled in the config and the grant type is not refresh token\n      let refreshToken: string | undefined\n      if (issuanceSession.generateRefreshTokens && grant.grantType !== refreshTokenGrantIdentifier) {\n        refreshToken = await openId4VcIssuerService.createRefreshToken(agentContext, issuer, {\n          preAuthorizedCode: grant.grantType === preAuthorizedCodeGrantIdentifier ? grant.preAuthorizedCode : undefined,\n          issuerState: issuanceSession.authorization?.issuerState,\n          dpop: tokenDpop,\n        })\n      }\n\n      const signerJwk = accessTokenSigningKey\n      const accessTokenResponse = await oauth2AuthorizationServer.createAccessTokenResponse({\n        audience: issuerMetadata.credentialIssuer.credential_issuer,\n        authorizationServer: issuerMetadata.credentialIssuer.credential_issuer,\n        expiresInSeconds: config.accessTokenExpiresInSeconds,\n        signer: {\n          method: 'jwk',\n          alg: signerJwk.supportedSignatureAlgorithms[0],\n          publicJwk: signerJwk.toJson() as Jwk,\n        },\n        dpop: tokenDpop,\n        scope: scopes?.join(' '),\n        clientId: issuanceSession.clientId,\n\n        additionalAccessTokenPayload: {\n          'pre-authorized_code':\n            grant.grantType === preAuthorizedCodeGrantIdentifier\n              ? grant.preAuthorizedCode\n              : parsedRefreshToken?.preAuthorizedCode,\n          issuer_state: issuanceSession.authorization?.issuerState,\n        },\n        // We generate a random subject for each access token and bind the issuance session to this.\n        subject,\n\n        refreshToken,\n\n        // NOTE: these have been removed in newer drafts. Keeping them in for now\n        cNonce,\n        cNonceExpiresIn: cNonceExpiresInSeconds,\n      })\n\n      issuanceSession.authorization = {\n        ...issuanceSession.authorization,\n        subject,\n      }\n\n      await openId4VcIssuerService.updateState(\n        agentContext,\n        issuanceSession,\n        // Retain the current session state when refreshing the access token.\n        grant.grantType === refreshTokenGrantIdentifier\n          ? issuanceSession.state\n          : OpenId4VcIssuanceSessionState.AccessTokenCreated\n      )\n\n      return sendJsonResponse(response, next, accessTokenResponse)\n    } catch (error) {\n      if (error instanceof Oauth2ServerErrorResponseError) {\n        return sendOauth2ErrorResponse(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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"}