From 72e178a8a31c8c9b84d609d03d9a74dfb6b7d1d3 Mon Sep 17 00:00:00 2001 From: "A.G.J. Cate" Date: Thu, 7 Nov 2024 14:32:02 +0100 Subject: [PATCH] chore: refactor issuer branding states oid4vci-holder --- .../oid4vci-holder/src/agent/OID4VCIHolder.ts | 85 +++++++++++-------- .../src/agent/OID4VCIHolderService.ts | 4 +- .../src/machine/oid4vciMachine.ts | 36 ++++++-- .../src/types/IOID4VCIHolder.ts | 33 +++++-- 4 files changed, 107 insertions(+), 51 deletions(-) diff --git a/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts b/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts index eb54b961c..084915b4e 100644 --- a/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts +++ b/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts @@ -38,9 +38,9 @@ import { IBasicIssuerLocaleBranding, Identity, IdentityOrigin, - IIssuerBranding, + IIssuerLocaleBranding, NonPersistedIdentity, - Party, + Party } from '@sphereon/ssi-sdk.data-store' import { CredentialMapper, @@ -72,7 +72,7 @@ import { v4 as uuidv4 } from 'uuid' import { OID4VCIMachine } from '../machine/oid4vciMachine' import { AddContactIdentityArgs, - AddIssuerBrandingArgs, + StoreIssuerBrandingArgs, AssertValidCredentialsArgs, Attribute, createCredentialsToSelectFromArgs, @@ -102,7 +102,8 @@ import { StoreCredentialsArgs, VerificationResult, VerifyEBSICredentialIssuerArgs, - VerifyEBSICredentialIssuerResult + VerifyEBSICredentialIssuerResult, + GetIssuerBrandingArgs } from '../types/IOID4VCIHolder' import { getBasicIssuerLocaleBranding, @@ -216,6 +217,8 @@ export class OID4VCIHolder implements IAgentPlugin { oid4vciHolderStoreCredentialBranding: this.oid4vciHolderStoreCredentialBranding.bind(this), oid4vciHolderStoreCredentials: this.oid4vciHolderStoreCredentials.bind(this), oid4vciHolderSendNotification: this.oid4vciHolderSendNotification.bind(this), + oid4vciHolderGetIssuerBranding: this.oid4vciHolderGetIssuerBranding.bind(this), + oid4vciHolderStoreIssuerBranding: this.oid4vciHolderStoreIssuerBranding.bind(this), } private readonly vcFormatPreferences: Array = ['vc+sd-jwt', 'mso_mdoc', 'jwt_vc_json', 'jwt_vc', 'ldp_vc'] @@ -312,10 +315,10 @@ export class OID4VCIHolder implements IAgentPlugin { ), createCredentialsToSelectFrom: (args: createCredentialsToSelectFromArgs) => this.oid4vciHoldercreateCredentialsToSelectFrom(args, context), getContact: (args: GetContactArgs) => this.oid4vciHolderGetContact(args, context), - getCredentials: (args: GetCredentialsArgs) => - this.oid4vciHolderGetCredentials({ accessTokenOpts: args.accessTokenOpts ?? opts.accessTokenOpts, ...args }, context), + getCredentials: (args: GetCredentialsArgs) => this.oid4vciHolderGetCredentials({ accessTokenOpts: args.accessTokenOpts ?? opts.accessTokenOpts, ...args }, context), addContactIdentity: (args: AddContactIdentityArgs) => this.oid4vciHolderAddContactIdentity(args, context), - addIssuerBranding: (args: AddIssuerBrandingArgs) => this.oid4vciHolderAddIssuerBranding(args, context), + getIssuerBranding: (args: GetIssuerBrandingArgs) => this.oid4vciHolderGetIssuerBranding(args, context), + storeIssuerBranding: (args: StoreIssuerBrandingArgs) => this.oid4vciHolderStoreIssuerBranding(args, context), assertValidCredentials: (args: AssertValidCredentialsArgs) => this.oid4vciHolderAssertValidCredentials(args, context), storeCredentialBranding: (args: StoreCredentialBrandingArgs) => this.oid4vciHolderStoreCredentialBranding(args, context), storeCredentials: (args: StoreCredentialsArgs) => this.oid4vciHolderStoreCredentials(args, context), @@ -455,10 +458,7 @@ export class OID4VCIHolder implements IAgentPlugin { } } - private async oid4vciHoldercreateCredentialsToSelectFrom( - args: createCredentialsToSelectFromArgs, - context: RequiredContext, - ): Promise> { + private async oid4vciHoldercreateCredentialsToSelectFrom(args: createCredentialsToSelectFromArgs, context: RequiredContext): Promise> { const { credentialBranding, locale, selectedCredentials /*, openID4VCIClientState*/, credentialsSupported } = args // const client = await OpenID4VCIClient.fromState({ state: openID4VCIClientState! }) // TODO see if we need the check openID4VCIClientState defined @@ -730,33 +730,50 @@ export class OID4VCIHolder implements IAgentPlugin { return context.agent.cmAddIdentity({ contactId: contact.id, identity }) } - private async oid4vciHolderAddIssuerBranding(args: AddIssuerBrandingArgs, context: RequiredContext): Promise { + private async oid4vciHolderGetIssuerBranding(args: GetIssuerBrandingArgs, context: RequiredContext): Promise> { const { serverMetadata, contact } = args - if (!contact) { - return logger.warning('Missing contact in context, so cannot get issuer branding') - } - if (serverMetadata?.credentialIssuerMetadata?.display) { - const issuerCorrelationId: string = - contact.identities - .filter((identity) => identity.roles.includes(CredentialRole.ISSUER)) - .map((identity) => identity.identifier.correlationId)[0] ?? undefined - - const brandings: IIssuerBranding[] = await context.agent.ibGetIssuerBranding({ filter: [{ issuerCorrelationId }] }) - // todo: Probably wise to look at last updated at and update in case it has been a while - if (!brandings || brandings.length === 0) { - const basicIssuerLocaleBrandings: IBasicIssuerLocaleBranding[] = await getBasicIssuerLocaleBranding({ - display: serverMetadata.credentialIssuerMetadata.display, - context, - }) - if (basicIssuerLocaleBrandings && basicIssuerLocaleBrandings.length > 0) { - await context.agent.ibAddIssuerBranding({ - localeBranding: basicIssuerLocaleBrandings, - issuerCorrelationId, - }) - } + // Here we are fetching issuer branding for a contact. If no contact is found that means we encounter this contact for the first time. This also means we do not have any branding for the contact. + const issuerCorrelationId = contact?.identities + .filter((identity) => identity.roles.includes(CredentialRole.ISSUER)) + .map((identity) => identity.identifier.correlationId)[0] + + if (issuerCorrelationId) { + const branding = await context.agent.ibGetIssuerBranding({ filter: [{ issuerCorrelationId }] }) + if (branding.length > 0) { + return branding[0].localeBranding } } + + // We should have serverMetadata in the context else something went wrong + if (!serverMetadata) { + return Promise.reject(Error('Missing serverMetadata in context')); + } + + return getBasicIssuerLocaleBranding({ + display: serverMetadata.credentialIssuerMetadata?.display ?? [], + context, + }) + } + + private async oid4vciHolderStoreIssuerBranding(args: StoreIssuerBrandingArgs, context: RequiredContext): Promise { + const { issuerBranding, contact } = args + if (!issuerBranding || issuerBranding.length === 0 || (>issuerBranding)[0].id) { // FIXME we need better separation between a contact(issuer) we encountered before and it's branding vs a new contact and it's branding + return + } + + if (!contact) { + return Promise.reject(Error('Missing contact in context')); + } + + const issuerCorrelationId = contact?.identities + .filter((identity) => identity.roles.includes(CredentialRole.ISSUER)) + .map((identity) => identity.identifier.correlationId)[0] + + await context.agent.ibAddIssuerBranding({ + localeBranding: issuerBranding as Array, + issuerCorrelationId, + }) } private async oid4vciHolderAssertValidCredentials(args: AssertValidCredentialsArgs, context: RequiredContext): Promise { diff --git a/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts b/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts index 78979b1f4..f82efd6e2 100644 --- a/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts +++ b/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts @@ -45,7 +45,7 @@ import { GetIssuanceCryptoSuiteArgs, GetIssuanceDidMethodArgs, GetIssuanceOptsArgs, - GetIssuerBrandingArgs, + GetBasicIssuerLocaleBrandingArgs, GetPreferredCredentialFormatsArgs, IssuanceOpts, MapCredentialToAcceptArgs, @@ -81,7 +81,7 @@ export const getCredentialBranding = async (args: GetCredentialBrandingArgs): Pr return credentialBranding } -export const getBasicIssuerLocaleBranding = async (args: GetIssuerBrandingArgs): Promise> => { +export const getBasicIssuerLocaleBranding = async (args: GetBasicIssuerLocaleBrandingArgs): Promise> => { //IBasicIssuerLocaleBranding const { display, context } = args return await Promise.all( display.map(async (displayItem: MetadataDisplay): Promise => { diff --git a/packages/oid4vci-holder/src/machine/oid4vciMachine.ts b/packages/oid4vci-holder/src/machine/oid4vciMachine.ts index a215fb457..af2e309c6 100644 --- a/packages/oid4vci-holder/src/machine/oid4vciMachine.ts +++ b/packages/oid4vci-holder/src/machine/oid4vciMachine.ts @@ -1,5 +1,10 @@ import { AuthzFlowType, toAuthorizationResponsePayload } from '@sphereon/oid4vci-common' -import { Identity, Party } from '@sphereon/ssi-sdk.data-store' +import { + IBasicIssuerLocaleBranding, + Identity, + IIssuerLocaleBranding, + Party +} from '@sphereon/ssi-sdk.data-store' import { assign, createMachine, DoneInvokeEvent, interpret } from 'xstate' import { translate } from '../localization/Localization' import { @@ -158,7 +163,7 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach [OID4VCIMachineServices.getContact]: { data: Party | undefined } - [OID4VCIMachineServices.addIssuerBranding]: { + [OID4VCIMachineServices.storeIssuerBranding]: { data: void } [OID4VCIMachineServices.getCredentials]: { @@ -178,6 +183,9 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach } [OID4VCIMachineServices.getFederationTrust]: { data: Array + }, + [OID4VCIMachineServices.getIssuerBranding]: { + data: Array } }, }, @@ -238,7 +246,7 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach invoke: { src: OID4VCIMachineServices.getContact, onDone: { - target: OID4VCIMachineStates.transitionFromSetup, + target: OID4VCIMachineStates.getIssuerBranding, actions: assign({ contact: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent) => _event.data }), }, onError: { @@ -253,6 +261,18 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach }, }, }, + [OID4VCIMachineStates.getIssuerBranding]: { + id: OID4VCIMachineStates.getIssuerBranding, + invoke: { + src: OID4VCIMachineServices.getIssuerBranding, + onDone: { + target: OID4VCIMachineStates.transitionFromSetup, + actions: assign({ + issuerBranding: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent>) => _event.data + }) + }, + }, + }, [OID4VCIMachineStates.transitionFromSetup]: { id: OID4VCIMachineStates.transitionFromSetup, always: [ @@ -334,16 +354,16 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach [OID4VCIMachineAddContactStates.idle]: {}, [OID4VCIMachineAddContactStates.next]: { always: { - target: `#${OID4VCIMachineStates.addIssuerBranding}`, + target: `#${OID4VCIMachineStates.storeIssuerBranding}`, cond: OID4VCIMachineGuards.hasContactGuard, }, }, }, }, - [OID4VCIMachineStates.addIssuerBranding]: { - id: OID4VCIMachineStates.addIssuerBranding, + [OID4VCIMachineStates.storeIssuerBranding]: { + id: OID4VCIMachineStates.storeIssuerBranding, invoke: { - src: OID4VCIMachineServices.addIssuerBranding, + src: OID4VCIMachineServices.storeIssuerBranding, onDone: { target: OID4VCIMachineStates.transitionFromContactSetup, }, @@ -540,7 +560,7 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach [OID4VCIMachineStates.addIssuerBrandingAfterIdentity]: { id: OID4VCIMachineStates.addIssuerBrandingAfterIdentity, invoke: { - src: OID4VCIMachineServices.addIssuerBranding, + src: OID4VCIMachineServices.storeIssuerBranding, onDone: { target: OID4VCIMachineStates.reviewCredentials, }, diff --git a/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts b/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts index d88a081cf..463bf6708 100644 --- a/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts +++ b/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts @@ -12,7 +12,11 @@ import { MetadataDisplay, NotificationRequest, } from '@sphereon/oid4vci-common' -import { CreateOrGetIdentifierOpts, IdentifierProviderOpts, SupportedDidMethodEnum } from '@sphereon/ssi-sdk-ext.did-utils' +import { + CreateOrGetIdentifierOpts, + IdentifierProviderOpts, + SupportedDidMethodEnum +} from '@sphereon/ssi-sdk-ext.did-utils' import { IIdentifierResolution, ManagedIdentifierMethod, @@ -22,7 +26,14 @@ import { import { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service' import { IContactManager } from '@sphereon/ssi-sdk.contact-manager' import { ICredentialStore } from '@sphereon/ssi-sdk.credential-store' -import { DigitalCredential, IBasicCredentialLocaleBranding, IBasicIssuerLocaleBranding, Identity, Party } from '@sphereon/ssi-sdk.data-store' +import { + DigitalCredential, + IBasicCredentialLocaleBranding, + IBasicIssuerLocaleBranding, + Identity, + IIssuerLocaleBranding, + Party +} from '@sphereon/ssi-sdk.data-store' import { IIssuanceBranding } from '@sphereon/ssi-sdk.issuance-branding' import { ImDLMdoc } from '@sphereon/ssi-sdk.mdl-mdoc' import { ISDJwtPlugin } from '@sphereon/ssi-sdk.sd-jwt' @@ -72,7 +83,11 @@ export interface IOID4VCIHolder extends IPluginMethodMap { oid4vciHolderAddContactIdentity(args: AddContactIdentityArgs, context: RequiredContext): Promise - oid4vciHolderAssertValidCredentials(args: AssertValidCredentialsArgs, context: RequiredContext): Promise + oid4vciHolderAssertValidCredentials(args: AssertValidCredentialsArgs, context: RequiredContext): Promise> + + oid4vciHolderGetIssuerBranding(args: GetIssuerBrandingArgs, context: RequiredContext): Promise> + + oid4vciHolderStoreIssuerBranding(args: StoreIssuerBrandingArgs, context: RequiredContext): Promise oid4vciHolderStoreCredentialBranding(args: StoreCredentialBrandingArgs, context: RequiredContext): Promise @@ -135,7 +150,8 @@ export type GetCredentialsArgs = Pick< 'verificationCode' | 'openID4VCIClientState' | 'selectedCredentials' | 'didMethodPreferences' | 'issuanceOpt' | 'accessTokenOpts' > export type AddContactIdentityArgs = Pick -export type AddIssuerBrandingArgs = Pick +export type GetIssuerBrandingArgs = Pick +export type StoreIssuerBrandingArgs = Pick export type AssertValidCredentialsArgs = Pick export type StoreCredentialBrandingArgs = Pick< OID4VCIMachineContext, @@ -197,6 +213,7 @@ export type OID4VCIMachineContext = { requestData?: RequestData // TODO WAL-673 fix type as this is not always a qr code (deeplink) locale?: string authorizationCodeURL?: string + issuerBranding?: Array credentialBranding?: Record> credentialsSupported: Record serverMetadata?: EndpointMetadataResult @@ -219,7 +236,8 @@ export enum OID4VCIMachineStates { transitionFromSetup = 'transitionFromSetup', getFederationTrust = 'getFederationTrust', addContact = 'addContact', - addIssuerBranding = 'addIssuerBranding', + getIssuerBranding = 'getIssuerBranding', + storeIssuerBranding = 'storeIssuerBranding', addIssuerBrandingAfterIdentity = 'addIssuerBrandingAfterIdentity', transitionFromContactSetup = 'transitionFromContactSetup', selectCredentials = 'selectCredentials', @@ -354,7 +372,8 @@ export enum OID4VCIMachineServices { getFederationTrust = 'getFederationTrust', addContactIdentity = 'addContactIdentity', createCredentialsToSelectFrom = 'createCredentialsToSelectFrom', - addIssuerBranding = 'addIssuerBranding', + getIssuerBranding = 'getIssuerBranding', + storeIssuerBranding = 'storeIssuerBranding', createCredentialSelection = 'createCredentialSelection', getCredentials = 'getCredentials', assertValidCredentials = 'assertValidCredentials', @@ -492,7 +511,7 @@ export type GetCredentialBrandingArgs = { context: RequiredContext } -export type GetIssuerBrandingArgs = { +export type GetBasicIssuerLocaleBrandingArgs = { display: MetadataDisplay[] context: RequiredContext }