Skip to content

Commit

Permalink
feat: MWALL-715 Create notification endpoint logic in Issuer
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed Dec 22, 2024
1 parent b21a032 commit 2dff0df
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 30 deletions.
4 changes: 2 additions & 2 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
getTypesFromObject,
KID_JWK_X5C_ERROR,
NotificationRequest,
NotificationResult,
NotificationResponseResult,
OID4VCICredentialFormat,
OpenId4VCIVersion,
PKCEOpts,
Expand Down Expand Up @@ -531,7 +531,7 @@ export class OpenID4VCIClient {
credentialRequestOpts: Partial<CredentialRequestOpts>,
request: NotificationRequest,
accessToken?: string,
): Promise<NotificationResult> {
): Promise<NotificationResponseResult> {
return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/OpenID4VCIClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
getTypesFromCredentialSupported,
KID_JWK_X5C_ERROR,
NotificationRequest,
NotificationResult,
NotificationResponseResult,
OID4VCICredentialFormat,
OpenId4VCIVersion,
PKCEOpts,
Expand Down Expand Up @@ -577,7 +577,7 @@ export class OpenID4VCIClientV1_0_13 {
credentialRequestOpts: Partial<CredentialRequestOpts>,
request: NotificationRequest,
accessToken?: string,
): Promise<NotificationResult> {
): Promise<NotificationResponseResult> {
return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/functions/notifications.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,7 +7,7 @@ export async function sendNotification(
credentialRequestOpts: Partial<CredentialRequestOpts>,
request: NotificationRequest,
accessToken?: string,
): Promise<NotificationResult> {
): Promise<NotificationResponseResult> {
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`);
Expand Down
2 changes: 1 addition & 1 deletion packages/did-auth-siop-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"typescript": "5.4.5"
"typescript": "5.4.5"
},
"engines": {
"node": ">=18"
Expand Down
37 changes: 27 additions & 10 deletions packages/issuer-rest/lib/oid4vci-api-functions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { uuidv4 } from '@sphereon/oid4vc-common'
import {
ACCESS_TOKEN_ISSUER_REQUIRED_ERROR,
AccessTokenRequest,
adjustUrl,
AuthorizationRequest,
CredentialOfferRESTRequest,
Expand Down Expand Up @@ -216,23 +217,39 @@ export function notificationEndpoint<DIDDoc extends object>(
})
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(
Expand Down
78 changes: 68 additions & 10 deletions packages/issuer/lib/VcIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<DIDDoc extends object> {
private readonly _issuerMetadata: CredentialIssuerMetadataOptsV1_0_13
private readonly _authorizationServerMetadata: AuthorizationServerMetadata
Expand Down Expand Up @@ -94,6 +97,31 @@ export class VcIssuer<DIDDoc extends object> {
return new LookupStateManager<URIState, CredentialOfferSession>(this.uris, this._credentialOfferSessions, 'uri').getAsserted(id)
}

public async processNotification({
preAuthorizedCode,
issuerState,
notification,
}: {
preAuthorizedCode?: string
issuerState?: string
notification: NotificationRequest
}): Promise<Error | CredentialOfferSession> {
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<string>
Expand Down Expand Up @@ -399,35 +427,65 @@ export class VcIssuer<DIDDoc extends object> {
}
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)
}
}
if (issuerState) {
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)
}
}
Expand Down Expand Up @@ -569,7 +627,7 @@ export class VcIssuer<DIDDoc extends object> {

return { jwtVerifyResult, preAuthorizedCode, preAuthSession, issuerState, authSession, cNonceState }
} catch (error: unknown) {
await this.updateErrorStatus({ preAuthorizedCode, issuerState, error })
await this.updateSession({ preAuthorizedCode, issuerState, error })
throw error
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/oid4vci-common/lib/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
2 changes: 1 addition & 1 deletion packages/oid4vci-common/lib/types/Generic.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
6 changes: 5 additions & 1 deletion packages/oid4vci-common/lib/types/StateManager.types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
}
Expand All @@ -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',
}

Expand Down
2 changes: 1 addition & 1 deletion packages/siop-oid4vp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 2dff0df

Please sign in to comment.