diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 0ec729d2..6769ebbd 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -28,7 +28,7 @@ import { getTypesFromObject, KID_JWK_X5C_ERROR, NotificationRequest, - NotificationResult, + NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, PKCEOpts, @@ -531,7 +531,7 @@ export class OpenID4VCIClient { credentialRequestOpts: Partial, request: NotificationRequest, accessToken?: string, - ): Promise { + ): Promise { return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token); } diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index 2c1329d7..4aedcc9d 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -22,7 +22,7 @@ import { getTypesFromCredentialSupported, KID_JWK_X5C_ERROR, NotificationRequest, - NotificationResult, + NotificationResponseResult, OID4VCICredentialFormat, OpenId4VCIVersion, PKCEOpts, @@ -577,7 +577,7 @@ export class OpenID4VCIClientV1_0_13 { credentialRequestOpts: Partial, request: NotificationRequest, accessToken?: string, - ): Promise { + ): Promise { return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token); } diff --git a/packages/client/lib/functions/notifications.ts b/packages/client/lib/functions/notifications.ts index 19db5642..935528e1 100644 --- a/packages/client/lib/functions/notifications.ts +++ b/packages/client/lib/functions/notifications.ts @@ -1,4 +1,4 @@ -import { NotificationErrorResponse, NotificationRequest, NotificationResult, post } from '@sphereon/oid4vci-common'; +import { NotificationErrorResponse, NotificationRequest, NotificationResponseResult, post } from '@sphereon/oid4vci-common'; import { CredentialRequestOpts } from '../CredentialRequestClient'; import { LOG } from '../types'; @@ -7,7 +7,7 @@ export async function sendNotification( credentialRequestOpts: Partial, request: NotificationRequest, accessToken?: string, -): Promise { +): Promise { LOG.info(`Sending status notification event '${request.event}' for id ${request.notification_id}`); if (!credentialRequestOpts.notificationEndpoint) { throw Error(`Cannot send notification when no notification endpoint is provided`); diff --git a/packages/did-auth-siop-adapter/package.json b/packages/did-auth-siop-adapter/package.json index decbd422..282f2b5d 100644 --- a/packages/did-auth-siop-adapter/package.json +++ b/packages/did-auth-siop-adapter/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "typescript": "5.4.5" + "typescript": "5.4.5" }, "engines": { "node": ">=18" diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index 4a045d54..e17b7ac3 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -1,6 +1,7 @@ import { uuidv4 } from '@sphereon/oid4vc-common' import { ACCESS_TOKEN_ISSUER_REQUIRED_ERROR, + AccessTokenRequest, adjustUrl, AuthorizationRequest, CredentialOfferRESTRequest, @@ -216,23 +217,39 @@ export function notificationEndpoint( }) try { const jwtResult = await validateJWT(jwt, { accessTokenVerificationCallback: opts.accessTokenVerificationCallback }) - EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, { - eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, - id: uuidv4(), - data: notificationRequest, - initiator: jwtResult.jwt, - initiatorType: InitiatorType.EXTERNAL, - system: System.OID4VCI, - subsystem: SubSystem.API, + const accessToken = jwtResult.jwt.payload as AccessTokenRequest + const errorOrSession = await issuer.processNotification({ + preAuthorizedCode: accessToken['pre-authorized_code'], + /*TODO: authorizationCode*/ notification: notificationRequest, }) + if (errorOrSession instanceof Error) { + EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, { + eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_ERROR, + id: uuidv4(), + data: notificationRequest, + initiator: jwtResult.jwt, + initiatorType: InitiatorType.EXTERNAL, + system: System.OID4VCI, + subsystem: SubSystem.API, + }) + return sendErrorResponse(response, 400, errorOrSession.message) + } else { + EVENTS.emit(NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, { + eventName: NotificationStatusEventNames.OID4VCI_NOTIFICATION_PROCESSED, + id: uuidv4(), + data: notificationRequest, + initiator: jwtResult.jwt, + initiatorType: InitiatorType.EXTERNAL, + system: System.OID4VCI, + subsystem: SubSystem.API, + }) + } } catch (e) { LOG.warning(e) return sendErrorResponse(response, 400, { error: 'invalid_token', }) } - - // TODO Send event return response.status(204).send() } catch (e) { return sendErrorResponse( diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 4a65eeaa..f8d35b82 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -30,6 +30,7 @@ import { KID_DID_NO_DID_ERROR, KID_JWK_X5C_ERROR, NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT, + NotificationRequest, OID4VCICredentialFormat, OpenId4VCIVersion, PRE_AUTH_GRANT_LITERAL, @@ -46,6 +47,8 @@ import { assertValidPinNumber, createCredentialOfferObject, createCredentialOffe import { LookupStateManager } from './state-manager' import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types' +import { LOG } from './index' + export class VcIssuer { private readonly _issuerMetadata: CredentialIssuerMetadataOptsV1_0_13 private readonly _authorizationServerMetadata: AuthorizationServerMetadata @@ -94,6 +97,31 @@ export class VcIssuer { return new LookupStateManager(this.uris, this._credentialOfferSessions, 'uri').getAsserted(id) } + public async processNotification({ + preAuthorizedCode, + issuerState, + notification, + }: { + preAuthorizedCode?: string + issuerState?: string + notification: NotificationRequest + }): Promise { + const sessionId = preAuthorizedCode ?? issuerState + const session = sessionId ? await this.getCredentialOfferSessionById(sessionId) : undefined + if (!session || !sessionId) { + LOG.error(`No session or session id found ${sessionId}`) + return Error('invalid_notification_request') + } + if (notification.notification_id !== session.notification_id) { + LOG.error(`Notification id ${notification.notification_id} not found in session. session notification id ${session.notification_id}`) + return Error('invalid_notification_id') + } else if (session.notification) { + LOG.info(`Overwriting existing notification, as a new notification came in ${session.notification_id}`) + } + await this.updateSession({ preAuthorizedCode: preAuthorizedCode, issuerState: issuerState, notification }) + LOG.info(`Processed notification ${notification} for ${session.notification_id}`) + return session + } public async createCredentialOfferURI(opts: { grants?: CredentialOfferGrantInput credential_configuration_ids?: Array @@ -399,26 +427,49 @@ export class VcIssuer { } return response } catch (error: unknown) { - await this.updateErrorStatus({ preAuthorizedCode, issuerState, error }) + await this.updateSession({ preAuthorizedCode, issuerState, error }) throw error } } - private async updateErrorStatus({ + private async updateSession({ preAuthorizedCode, error, issuerState, + notification, }: { - preAuthorizedCode: string | undefined - issuerState: string | undefined - error: unknown + preAuthorizedCode?: string + issuerState?: string + error?: unknown + notification?: NotificationRequest }) { + let issueState: IssueStatus | undefined = undefined + if (error) { + issueState = IssueStatus.ERROR + } else if (notification) { + if (notification.event == 'credential_accepted') { + issueState = IssueStatus.NOTIFICATION_CREDENTIAL_ACCEPTED + } else if (notification.event == 'credential_deleted') { + issueState = IssueStatus.NOTIFICATION_CREDENTIAL_DELETED + } else if (notification.event == 'credential_failure') { + issueState = IssueStatus.NOTIFICATION_CREDENTIAL_FAILURE + } + } + if (preAuthorizedCode) { const preAuthSession = await this._credentialOfferSessions.get(preAuthorizedCode) if (preAuthSession) { preAuthSession.lastUpdatedAt = +new Date() - preAuthSession.status = IssueStatus.ERROR - preAuthSession.error = error instanceof Error ? error.message : error?.toString() + if (issueState) { + preAuthSession.status = issueState + } + if (error) { + preAuthSession.error = error instanceof Error ? error.message : error?.toString() + } + preAuthSession.notification_id + if (notification) { + preAuthSession.notification = notification + } await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession) } } @@ -426,8 +477,15 @@ export class VcIssuer { const authSession = await this._credentialOfferSessions.get(issuerState) if (authSession) { authSession.lastUpdatedAt = +new Date() - authSession.status = IssueStatus.ERROR - authSession.error = error instanceof Error ? error.message : error?.toString() + if (issueState) { + authSession.status = issueState + } + if (error) { + authSession.error = error instanceof Error ? error.message : error?.toString() + } + if (notification) { + authSession.notification = notification + } await this._credentialOfferSessions.set(issuerState, authSession) } } @@ -569,7 +627,7 @@ export class VcIssuer { return { jwtVerifyResult, preAuthorizedCode, preAuthSession, issuerState, authSession, cNonceState } } catch (error: unknown) { - await this.updateErrorStatus({ preAuthorizedCode, issuerState, error }) + await this.updateSession({ preAuthorizedCode, issuerState, error }) throw error } } diff --git a/packages/oid4vci-common/lib/events/index.ts b/packages/oid4vci-common/lib/events/index.ts index 50fc7fe6..93905f88 100644 --- a/packages/oid4vci-common/lib/events/index.ts +++ b/packages/oid4vci-common/lib/events/index.ts @@ -15,6 +15,7 @@ export enum CredentialEventNames { export enum NotificationStatusEventNames { OID4VCI_NOTIFICATION_RECEIVED = 'OID4VCI_NOTIFICATION_RECEIVED', OID4VCI_NOTIFICATION_PROCESSED = 'OID4VCI_NOTIFICATION_PROCESSED', + OID4VCI_NOTIFICATION_ERROR = 'OID4VCI_NOTIFICATION_ERROR', } export type LogEvents = 'oid4vciLog'; export const EVENTS = EventManager.instance(); diff --git a/packages/oid4vci-common/lib/types/Generic.types.ts b/packages/oid4vci-common/lib/types/Generic.types.ts index f6b8187d..25c8ebfd 100644 --- a/packages/oid4vci-common/lib/types/Generic.types.ts +++ b/packages/oid4vci-common/lib/types/Generic.types.ts @@ -416,7 +416,7 @@ export interface NotificationRequest { export type NotificationError = 'invalid_notification_id' | 'invalid_notification_request'; -export type NotificationResult = { +export type NotificationResponseResult = { error: boolean; response?: NotificationErrorResponse; }; diff --git a/packages/oid4vci-common/lib/types/StateManager.types.ts b/packages/oid4vci-common/lib/types/StateManager.types.ts index 1b041d0c..28cf53ed 100644 --- a/packages/oid4vci-common/lib/types/StateManager.types.ts +++ b/packages/oid4vci-common/lib/types/StateManager.types.ts @@ -1,5 +1,5 @@ import { AssertedUniformCredentialOffer } from './CredentialIssuance.types'; -import { CredentialDataSupplierInput } from './Generic.types'; +import { CredentialDataSupplierInput, NotificationRequest } from './Generic.types' export interface StateType { createdAt: number; @@ -14,6 +14,7 @@ export interface CredentialOfferSession extends StateType { error?: string; lastUpdatedAt: number; notification_id: string; + notification?: NotificationRequest; issuerState?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value preAuthorizedCode?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value } @@ -25,6 +26,9 @@ export enum IssueStatus { ACCESS_TOKEN_CREATED = 'ACCESS_TOKEN_CREATED', // Optional state, given the token endpoint could also be on a separate AS CREDENTIAL_REQUEST_RECEIVED = 'CREDENTIAL_REQUEST_RECEIVED', // Credential request received. Next state would either be error or issued CREDENTIAL_ISSUED = 'CREDENTIAL_ISSUED', + NOTIFICATION_CREDENTIAL_ACCEPTED = 'NOTIFICATION_CREDENTIAL_ACCEPTED', + NOTIFICATION_CREDENTIAL_DELETED = 'NOTIFICATION_CREDENTIAL_DELETED', + NOTIFICATION_CREDENTIAL_FAILURE = 'NOTIFICATION_CREDENTIAL_FAILURE', ERROR = 'ERROR', } diff --git a/packages/siop-oid4vp/package.json b/packages/siop-oid4vp/package.json index fcb225ed..20a7c680 100644 --- a/packages/siop-oid4vp/package.json +++ b/packages/siop-oid4vp/package.json @@ -50,7 +50,7 @@ "@transmute/ed25519-signature-2018": "^0.7.0-unstable.82", "@types/debug": "^4.1.12", "@types/jest": "^29.5.11", - "@types/language-tags": "^1.0.4", + "@types/language-tags": "^1.0.4", "@types/qs": "^6.9.11", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0",