diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index 8744114f21..3e49da344f 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -1,7 +1,7 @@ import type { OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' import type { OpenId4VcIssuanceRequest } from './router' import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' -import type { NextFunction, Response } from 'express' +import type { NextFunction, Response, Router } from 'express' import { setGlobalConfig } from '@animo-id/oauth2' import { AgentConfig } from '@credo-ts/core' @@ -30,9 +30,11 @@ import { export class OpenId4VcIssuerModule implements Module { public readonly api = OpenId4VcIssuerApi public readonly config: OpenId4VcIssuerModuleConfig + public readonly contextRouter: Router - public constructor(options: OpenId4VcIssuerModuleConfigOptions) { + public constructor(options: OpenId4VcIssuerModuleConfigOptions, router?: Router) { this.config = new OpenId4VcIssuerModuleConfig(options) + this.contextRouter = router ?? importExpress().Router() } /** @@ -82,14 +84,13 @@ export class OpenId4VcIssuerModule implements Module { // We use separate context router and endpoint router. Context router handles the linking of the request // to a specific agent context. Endpoint router only knows about a single context const endpointRouter = Router() - const contextRouter = this.config.router // parse application/x-www-form-urlencoded - contextRouter.use(urlencoded({ extended: false })) + this.contextRouter.use(urlencoded({ extended: false })) // parse application/json - contextRouter.use(json()) + this.contextRouter.use(json()) - contextRouter.param('issuerId', async (req: OpenId4VcIssuanceRequest, _res, next, issuerId: string) => { + this.contextRouter.param('issuerId', async (req: OpenId4VcIssuanceRequest, _res, next, issuerId: string) => { if (!issuerId) { rootAgentContext.config.logger.debug('No issuerId provided for incoming oid4vci request, returning 404') _res.status(404).send('Not found') @@ -123,7 +124,7 @@ export class OpenId4VcIssuerModule implements Module { next() }) - contextRouter.use('/:issuerId', endpointRouter) + this.contextRouter.use('/:issuerId', endpointRouter) // Configure endpoints configureIssuerMetadataEndpoint(endpointRouter) @@ -136,7 +137,7 @@ export class OpenId4VcIssuerModule implements Module { configureCredentialEndpoint(endpointRouter, this.config) // First one will be called for all requests (when next is called) - contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => { + this.contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => { const { agentContext } = getRequestContext(req) await agentContext.endSession() @@ -144,22 +145,24 @@ export class OpenId4VcIssuerModule implements Module { }) // This one will be called for all errors that are thrown - contextRouter.use(async (_error: unknown, req: OpenId4VcIssuanceRequest, res: Response, next: NextFunction) => { - const { agentContext } = getRequestContext(req) - - if (!res.headersSent) { - agentContext.config.logger.warn( - 'Error was thrown but openid4vci endpoint did not send a response. Sending generic server_error.' - ) + this.contextRouter.use( + async (_error: unknown, req: OpenId4VcIssuanceRequest, res: Response, next: NextFunction) => { + const { agentContext } = getRequestContext(req) + + if (!res.headersSent) { + agentContext.config.logger.warn( + 'Error was thrown but openid4vci endpoint did not send a response. Sending generic server_error.' + ) + + res.status(500).json({ + error: 'server_error', + error_description: 'An unexpected error occurred on the server.', + }) + } - res.status(500).json({ - error: 'server_error', - error_description: 'An unexpected error occurred on the server.', - }) + await agentContext.endSession() + next() } - - await agentContext.endSession() - next() - }) + ) } } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index 2056824d14..96dbd0c767 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -2,9 +2,6 @@ import type { OpenId4VciCredentialRequestToCredentialMapper, OpenId4VciGetVerificationSessionForIssuanceSessionAuthorization, } from './OpenId4VcIssuerServiceOptions' -import type { Router } from 'express' - -import { importExpress } from '../shared/router' const DEFAULT_C_NONCE_EXPIRES_IN = 1 * 60 // 1 minute const DEFAULT_AUTHORIZATION_CODE_EXPIRES_IN = 1 * 60 // 1 minute @@ -18,15 +15,6 @@ export interface OpenId4VcIssuerModuleConfigOptions { */ baseUrl: string - /** - * Express router on which the openid4vci endpoints will be registered. If - * no router is provided, a new one will be created. - * - * NOTE: you must manually register the router on your express app and - * expose this on a public url that is reachable when `baseUrl` is called. - */ - router?: Router - /** * The time after which a cNonce will expire. * @@ -130,12 +118,12 @@ export interface OpenId4VcIssuerModuleConfigOptions { export class OpenId4VcIssuerModuleConfig { private options: OpenId4VcIssuerModuleConfigOptions - public readonly router: Router + // public readonly router: Router public constructor(options: OpenId4VcIssuerModuleConfigOptions) { this.options = options - this.router = options.router ?? importExpress().Router() + // this.router = options.router ?? } public get baseUrl() { diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts index 2cd330f315..f588a477e7 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts @@ -25,9 +25,8 @@ describe('OpenId4VcIssuerModule', () => { credentialRequestToCredentialMapper: () => { throw new Error('Not implemented') }, - router: Router(), } as const - const openId4VcClientModule = new OpenId4VcIssuerModule(options) + const openId4VcClientModule = new OpenId4VcIssuerModule(options, Router()) openId4VcClientModule.register(dependencyManager) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) @@ -43,6 +42,6 @@ describe('OpenId4VcIssuerModule', () => { await openId4VcClientModule.initialize(agentContext) - expect(openId4VcClientModule.config.router).toBeDefined() + expect(openId4VcClientModule.contextRouter).toBeDefined() }) }) diff --git a/packages/openid4vc/src/openid4vc-issuer/handler/credentialEndpointHandler.ts b/packages/openid4vc/src/openid4vc-issuer/handler/credentialEndpointHandler.ts new file mode 100644 index 0000000000..81d63d4a48 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/handler/credentialEndpointHandler.ts @@ -0,0 +1,231 @@ +/* + * Copyright 2025 Velocity Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import type { OpenId4VciCredentialRequest, OpenId4VcIssuerModuleConfig } from '../../..' +import type { OpenId4VCIssuanceRequestContext } from '../router/requestContext' +import type * as http from 'node:http' + +import { + type HttpMethod, + Oauth2ErrorCodes, + Oauth2ResourceUnauthorizedError, + Oauth2ServerErrorResponseError, + SupportedAuthenticationScheme, +} from '@animo-id/oauth2' +import { getCredentialConfigurationsMatchingRequestFormat } from '@animo-id/oid4vci' +import { joinUriParts } from '@credo-ts/core' + +import { getCredentialConfigurationsSupportedForScopes } from '../../shared' +import { addSecondsToDate } from '../../shared/utils' +import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' +import { OpenId4VcIssuanceSessionRecord, OpenId4VcIssuanceSessionRepository } from '../repository' + +export function configureCredentialEndpointHandler(config: OpenId4VcIssuerModuleConfig) { + return async function credentialEndpointHandler( + credentialRequest: OpenId4VciCredentialRequest, + httpRequest: http.IncomingMessage, // or use @animo-id/oauth2/RequestLike type + { agentContext, issuer }: OpenId4VCIssuanceRequestContext + ) { + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + const issuerMetadata = await openId4VcIssuerService.getIssuerMetadata(agentContext, issuer, true) + const vcIssuer = openId4VcIssuerService.getIssuer(agentContext) + const resourceServer = openId4VcIssuerService.getResourceServer(agentContext, issuer) + + const fullRequestUrl = joinUriParts(issuerMetadata.credentialIssuer.credential_issuer, [ + config.credentialEndpointPath, + ]) + const resourceRequestResult = await resourceServer.verifyResourceRequest({ + authorizationServers: issuerMetadata.authorizationServers, + resourceServer: issuerMetadata.credentialIssuer.credential_issuer, + allowedAuthenticationSchemes: config.dpopRequired ? [SupportedAuthenticationScheme.DPoP] : undefined, + request: { + headers: new Headers(httpRequest.headers as Record), + method: httpRequest.method as HttpMethod, + url: fullRequestUrl, + }, + }) + if (!resourceRequestResult) return null + const { tokenPayload, accessToken, scheme, authorizationServer } = resourceRequestResult + + const issuanceSessionRepository = agentContext.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) + + const parsedCredentialRequest = vcIssuer.parseCredentialRequest({ + credentialRequest, + }) + + let issuanceSession: OpenId4VcIssuanceSessionRecord | null = null + const preAuthorizedCode = + typeof tokenPayload['pre-authorized_code'] === 'string' ? tokenPayload['pre-authorized_code'] : undefined + const issuerState = typeof tokenPayload.issuer_state === 'string' ? tokenPayload.issuer_state : undefined + + const subject = tokenPayload.sub + if (!subject) { + throw new Oauth2ServerErrorResponseError( + { + error: Oauth2ErrorCodes.ServerError, + }, + { + internalMessage: `Received token without 'sub' claim. Subject is required for binding issuance session`, + } + ) + } + + // Already handle request without format. Simplifies next code sections + if (!parsedCredentialRequest.format) { + throw new Oauth2ServerErrorResponseError({ + error: parsedCredentialRequest.credentialIdentifier + ? Oauth2ErrorCodes.InvalidCredentialRequest + : Oauth2ErrorCodes.UnsupportedCredentialFormat, + error_description: parsedCredentialRequest.credentialIdentifier + ? `Credential request containing 'credential_identifier' not supported` + : `Credential format '${parsedCredentialRequest.credentialRequest.format}' not supported`, + }) + } + + if (preAuthorizedCode || issuerState) { + issuanceSession = await issuanceSessionRepository.findSingleByQuery(agentContext, { + issuerId: issuer.issuerId, + preAuthorizedCode, + issuerState, + }) + + if (!issuanceSession) { + agentContext.config.logger.warn( + `No issuance session found for incoming credential request for issuer ${ + issuer.issuerId + } but access token data has ${ + issuerState ? 'issuer_state' : 'pre-authorized_code' + }. Returning error response`, + { + tokenPayload, + } + ) + + throw new Oauth2ServerErrorResponseError( + { + error: Oauth2ErrorCodes.CredentialRequestDenied, + }, + { + internalMessage: `No issuance session found for incoming credential request for issuer ${issuer.issuerId} and access token data`, + } + ) + } + + // Verify the issuance session subject + if (issuanceSession.authorization?.subject) { + if (issuanceSession.authorization.subject !== tokenPayload.sub) { + throw new Oauth2ServerErrorResponseError( + { + error: Oauth2ErrorCodes.CredentialRequestDenied, + }, + { + internalMessage: `Issuance session authorization subject does not match with the token payload subject for issuance session '${issuanceSession.id}'. Returning error response`, + } + ) + } + } + // Statefull session expired + else if ( + Date.now() > + addSecondsToDate(issuanceSession.createdAt, config.statefullCredentialOfferExpirationInSeconds).getTime() + ) { + issuanceSession.errorMessage = 'Credential offer has expired' + await openId4VcIssuerService.updateState(agentContext, issuanceSession, OpenId4VcIssuanceSessionState.Error) + throw new Oauth2ServerErrorResponseError({ + // What is the best error here? + error: Oauth2ErrorCodes.CredentialRequestDenied, + error_description: 'Session expired', + }) + } else { + issuanceSession.authorization = { + ...issuanceSession.authorization, + subject: tokenPayload.sub, + } + await issuanceSessionRepository.update(agentContext, issuanceSession) + } + } + + if (!issuanceSession && config.allowDynamicIssuanceSessions) { + agentContext.config.logger.warn( + `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`, + { + tokenPayload, + } + ) + + // All credential configurations that match the request scope and credential request + // This is just so we don't create an issuance session that will fail immediately after + const credentialConfigurationsForToken = getCredentialConfigurationsMatchingRequestFormat({ + credentialConfigurations: getCredentialConfigurationsSupportedForScopes( + issuerMetadata.credentialIssuer.credential_configurations_supported, + tokenPayload.scope?.split(' ') ?? [] + ), + requestFormat: parsedCredentialRequest.format, + }) + + if (Object.keys(credentialConfigurationsForToken).length === 0) { + throw new Oauth2ResourceUnauthorizedError( + 'No credential configurationss match credential request and access token scope', + { + scheme, + error: Oauth2ErrorCodes.InsufficientScope, + } + ) + } + + issuanceSession = new OpenId4VcIssuanceSessionRecord({ + credentialOfferPayload: { + credential_configuration_ids: Object.keys(credentialConfigurationsForToken), + credential_issuer: issuerMetadata.credentialIssuer.credential_issuer, + }, + issuerId: issuer.issuerId, + state: OpenId4VcIssuanceSessionState.CredentialRequestReceived, + clientId: tokenPayload.client_id, + authorization: { + subject: tokenPayload.sub, + }, + }) + + // Save and update + await issuanceSessionRepository.save(agentContext, issuanceSession) + openId4VcIssuerService.emitStateChangedEvent(agentContext, issuanceSession, null) + } else if (!issuanceSession) { + throw new Oauth2ServerErrorResponseError( + { + error: Oauth2ErrorCodes.CredentialRequestDenied, + }, + { + 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 statefull credential offer, or enable 'allowDynamicIssuanceSessions'.`, + } + ) + } + + const { credentialResponse } = await openId4VcIssuerService.createCredentialResponse(agentContext, { + issuanceSession, + credentialRequest, + authorization: { + authorizationServer, + accessToken: { + payload: tokenPayload, + value: accessToken, + }, + }, + }) + return credentialResponse + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts index fc27ced0ed..88e80c3c13 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts @@ -1,18 +1,9 @@ import type { OpenId4VcIssuanceRequest } from './requestContext' import type { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' -import type { HttpMethod } from '@animo-id/oauth2' import type { Router, Response } from 'express' -import { - Oauth2ErrorCodes, - Oauth2ServerErrorResponseError, - Oauth2ResourceUnauthorizedError, - SupportedAuthenticationScheme, -} from '@animo-id/oauth2' -import { getCredentialConfigurationsMatchingRequestFormat } from '@animo-id/oid4vci' -import { joinUriParts } from '@credo-ts/core' +import { Oauth2ServerErrorResponseError, Oauth2ResourceUnauthorizedError } from '@animo-id/oauth2' -import { getCredentialConfigurationsSupportedForScopes } from '../../shared' import { getRequestContext, sendJsonResponse, @@ -20,248 +11,24 @@ import { sendUnauthorizedError, sendUnknownServerErrorResponse, } from '../../shared/router' -import { addSecondsToDate } from '../../shared/utils' -import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState' -import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -import { OpenId4VcIssuanceSessionRecord, OpenId4VcIssuanceSessionRepository } from '../repository' +import { configureCredentialEndpointHandler } from '../handler/credentialEndpointHandler' export function configureCredentialEndpoint(router: Router, config: OpenId4VcIssuerModuleConfig) { + const credentialEndpointHandler = configureCredentialEndpointHandler(config) router.post(config.credentialEndpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => { - const { agentContext, issuer } = getRequestContext(request) - const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) - const issuerMetadata = await openId4VcIssuerService.getIssuerMetadata(agentContext, issuer, true) - const vcIssuer = openId4VcIssuerService.getIssuer(agentContext) - const resourceServer = openId4VcIssuerService.getResourceServer(agentContext, issuer) - - const fullRequestUrl = joinUriParts(issuerMetadata.credentialIssuer.credential_issuer, [ - config.credentialEndpointPath, - ]) - const resourceRequestResult = await resourceServer - .verifyResourceRequest({ - authorizationServers: issuerMetadata.authorizationServers, - resourceServer: issuerMetadata.credentialIssuer.credential_issuer, - allowedAuthenticationSchemes: config.dpopRequired ? [SupportedAuthenticationScheme.DPoP] : undefined, - request: { - headers: new Headers(request.headers as Record), - method: request.method as HttpMethod, - url: fullRequestUrl, - }, - }) - .catch((error) => { - sendUnauthorizedError(response, next, agentContext.config.logger, error) - }) - if (!resourceRequestResult) return - const { tokenPayload, accessToken, scheme, authorizationServer } = resourceRequestResult - - const credentialRequest = request.body - const issuanceSessionRepository = agentContext.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository) - - const parsedCredentialRequest = vcIssuer.parseCredentialRequest({ - credentialRequest, - }) - - let issuanceSession: OpenId4VcIssuanceSessionRecord | null = null - const preAuthorizedCode = - typeof tokenPayload['pre-authorized_code'] === 'string' ? tokenPayload['pre-authorized_code'] : undefined - const issuerState = typeof tokenPayload.issuer_state === 'string' ? tokenPayload.issuer_state : undefined - - const subject = tokenPayload.sub - if (!subject) { - return sendOauth2ErrorResponse( - response, - next, - agentContext.config.logger, - new Oauth2ServerErrorResponseError( - { - error: Oauth2ErrorCodes.ServerError, - }, - { - internalMessage: `Received token without 'sub' claim. Subject is required for binding issuance session`, - } - ) - ) - } - - // Already handle request without format. Simplifies next code sections - if (!parsedCredentialRequest.format) { - return sendOauth2ErrorResponse( - response, - next, - agentContext.config.logger, - new Oauth2ServerErrorResponseError({ - error: parsedCredentialRequest.credentialIdentifier - ? Oauth2ErrorCodes.InvalidCredentialRequest - : Oauth2ErrorCodes.UnsupportedCredentialFormat, - error_description: parsedCredentialRequest.credentialIdentifier - ? `Credential request containing 'credential_identifier' not supported` - : `Credential format '${parsedCredentialRequest.credentialRequest.format}' not supported`, - }) - ) - } - - if (preAuthorizedCode || issuerState) { - issuanceSession = await issuanceSessionRepository.findSingleByQuery(agentContext, { - issuerId: issuer.issuerId, - preAuthorizedCode, - issuerState, - }) - - if (!issuanceSession) { - agentContext.config.logger.warn( - `No issuance session found for incoming credential request for issuer ${ - issuer.issuerId - } but access token data has ${ - issuerState ? 'issuer_state' : 'pre-authorized_code' - }. Returning error response`, - { - tokenPayload, - } - ) - - return sendOauth2ErrorResponse( - response, - next, - agentContext.config.logger, - new Oauth2ServerErrorResponseError( - { - error: Oauth2ErrorCodes.CredentialRequestDenied, - }, - { - internalMessage: `No issuance session found for incoming credential request for issuer ${issuer.issuerId} and access token data`, - } - ) - ) - } - - // Verify the issuance session subject - if (issuanceSession.authorization?.subject) { - if (issuanceSession.authorization.subject !== tokenPayload.sub) { - return sendOauth2ErrorResponse( - response, - next, - agentContext.config.logger, - new Oauth2ServerErrorResponseError( - { - error: Oauth2ErrorCodes.CredentialRequestDenied, - }, - { - internalMessage: `Issuance session authorization subject does not match with the token payload subject for issuance session '${issuanceSession.id}'. Returning error response`, - } - ) - ) - } - } - // Statefull session expired - else if ( - Date.now() > - addSecondsToDate(issuanceSession.createdAt, config.statefullCredentialOfferExpirationInSeconds).getTime() - ) { - issuanceSession.errorMessage = 'Credential offer has expired' - await openId4VcIssuerService.updateState(agentContext, issuanceSession, OpenId4VcIssuanceSessionState.Error) - throw new Oauth2ServerErrorResponseError({ - // What is the best error here? - error: Oauth2ErrorCodes.CredentialRequestDenied, - error_description: 'Session expired', - }) - } else { - issuanceSession.authorization = { - ...issuanceSession.authorization, - subject: tokenPayload.sub, - } - await issuanceSessionRepository.update(agentContext, issuanceSession) - } - } - - if (!issuanceSession && config.allowDynamicIssuanceSessions) { - agentContext.config.logger.warn( - `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`, - { - tokenPayload, - } - ) - - // All credential configurations that match the request scope and credential request - // This is just so we don't create an issuance session that will fail immediately after - const credentialConfigurationsForToken = getCredentialConfigurationsMatchingRequestFormat({ - credentialConfigurations: getCredentialConfigurationsSupportedForScopes( - issuerMetadata.credentialIssuer.credential_configurations_supported, - tokenPayload.scope?.split(' ') ?? [] - ), - requestFormat: parsedCredentialRequest.format, - }) - - if (Object.keys(credentialConfigurationsForToken).length === 0) { - return sendUnauthorizedError( - response, - next, - agentContext.config.logger, - new Oauth2ResourceUnauthorizedError( - 'No credential configurationss match credential request and access token scope', - { - scheme, - error: Oauth2ErrorCodes.InsufficientScope, - } - ), - // Forbidden for InsufficientScope - 403 - ) - } - - issuanceSession = new OpenId4VcIssuanceSessionRecord({ - credentialOfferPayload: { - credential_configuration_ids: Object.keys(credentialConfigurationsForToken), - credential_issuer: issuerMetadata.credentialIssuer.credential_issuer, - }, - issuerId: issuer.issuerId, - state: OpenId4VcIssuanceSessionState.CredentialRequestReceived, - clientId: tokenPayload.client_id, - authorization: { - subject: tokenPayload.sub, - }, - }) - - // Save and update - await issuanceSessionRepository.save(agentContext, issuanceSession) - openId4VcIssuerService.emitStateChangedEvent(agentContext, issuanceSession, null) - } else if (!issuanceSession) { - return sendOauth2ErrorResponse( - response, - next, - agentContext.config.logger, - new Oauth2ServerErrorResponseError( - { - error: Oauth2ErrorCodes.CredentialRequestDenied, - }, - { - 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 statefull credential offer, or enable 'allowDynamicIssuanceSessions'.`, - } - ) - ) - } - + const requestContext = getRequestContext(request) try { - const { credentialResponse } = await openId4VcIssuerService.createCredentialResponse(agentContext, { - issuanceSession, - credentialRequest, - authorization: { - authorizationServer, - accessToken: { - payload: tokenPayload, - value: accessToken, - }, - }, - }) - + const credentialResponse = await credentialEndpointHandler(request.body, request, requestContext) return sendJsonResponse(response, next, credentialResponse) } catch (error) { if (error instanceof Oauth2ServerErrorResponseError) { - return sendOauth2ErrorResponse(response, next, agentContext.config.logger, error) + return sendOauth2ErrorResponse(response, next, requestContext.agentContext.config.logger, error) } if (error instanceof Oauth2ResourceUnauthorizedError) { - return sendUnauthorizedError(response, next, agentContext.config.logger, error) + return sendUnauthorizedError(response, next, requestContext.agentContext.config.logger, error) } - return sendUnknownServerErrorResponse(response, next, agentContext.config.logger, error) + return sendUnknownServerErrorResponse(response, next, requestContext.agentContext.config.logger, error) } }) } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts index 69e0caadb3..2818c5f567 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts @@ -1,4 +1,5 @@ -import type { OpenId4VcRequest } from '../../shared/router' +import type { OpenId4VcRequest, OpenId4VcRequestContext } from '../../shared/router' import type { OpenId4VcIssuerRecord } from '../repository' export type OpenId4VcIssuanceRequest = OpenId4VcRequest<{ issuer: OpenId4VcIssuerRecord }> +export type OpenId4VCIssuanceRequestContext = OpenId4VcRequestContext<{ issuer: OpenId4VcIssuerRecord }> diff --git a/packages/openid4vc/src/shared/router/context.ts b/packages/openid4vc/src/shared/router/context.ts index cc383d693f..a6218cb05a 100644 --- a/packages/openid4vc/src/shared/router/context.ts +++ b/packages/openid4vc/src/shared/router/context.ts @@ -6,10 +6,10 @@ import { Oauth2ResourceUnauthorizedError, SupportedAuthenticationScheme } from ' import { CredoError } from '@credo-ts/core' export interface OpenId4VcRequest = Record> extends Request { - requestContext?: RC & OpenId4VcRequestContext + requestContext?: OpenId4VcRequestContext } -export interface OpenId4VcRequestContext { +export type OpenId4VcRequestContext = Record> = RC & { agentContext: AgentContext } diff --git a/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts b/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts index e90f587ba9..79ed47602a 100644 --- a/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts @@ -83,7 +83,10 @@ describe('OpenId4Vc Presentation During Issuance', () => { await issuer.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) // We let AFJ create the router, so we have a fresh one each time - expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) + expressApp.use( + '/oid4vci', + (issuer.agent.dependencyManager.registeredModules['openId4VcIssuer'] as OpenId4VcIssuerModule).contextRouter + ) clearNock = setupNockToExpress(baseUrl, expressApp) }) diff --git a/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts b/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts index 43b76f6b5a..c41705d0be 100644 --- a/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts @@ -161,7 +161,10 @@ describe('OpenId4Vc Presentation During Issuance', () => { issuer.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) // We let AFJ create the router, so we have a fresh one each time - expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) + expressApp.use( + '/oid4vci', + (issuer.agent.dependencyManager.registeredModules['openId4VcIssuer'] as OpenId4VcIssuerModule).contextRouter + ) expressApp.use('/oid4vp', issuer.agent.modules.openId4VcVerifier.config.router) clearNock = setupNockToExpress(baseUrl, expressApp) diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index edd125832b..14f01d171a 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -194,7 +194,10 @@ describe('OpenId4Vc', () => { verifier2 = await createTenantForAgent(verifier.agent, 'vTenant2') // We let AFJ create the router, so we have a fresh one each time - expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) + expressApp.use( + '/oid4vci', + (issuer.agent.dependencyManager.registeredModules['openId4VcIssuer'] as OpenId4VcIssuerModule).contextRouter + ) expressApp.use('/oid4vp', verifier.agent.modules.openId4VcVerifier.config.router) expressServer = expressApp.listen(serverPort)