From 97797daff2d9f20454b2de008b8e49ff4af7f94a Mon Sep 17 00:00:00 2001 From: Gianluca Spada Date: Tue, 17 Sep 2024 18:03:55 +0200 Subject: [PATCH] feat(IT Wallet): [SIW-1522] Get credential status attestation after issuance (#6162) ## Short description This PR modifies the credential issuance flow to get a status attestation immediately after the credential has been issued, before it is saved to the wallet. If the credential is revoked or the `/status` endpoint returns an error, the entire credential issuance fails, as per design review. ## List of changes proposed in this pull request - Modified the credential issuance machine to add the `Issuance` compound state - Modified credential issuance tests - Changed the target state for `retry` because retried requests were using stale data ## How to test #### Valid credential 1. Get a credential 2. Check the network to see the `/status` request immediately after `/credential` 3. Check the store `features.itWallet.credentials`: it should contain the property `storedStatusAttestation` #### Revoked credential/error Use a proxy to force a 404 or 500 error. 1. Get a credential 2. The issuance should fail and the failure screen should be displayed 3. No credential should be added to the store `features.itWallet.credentials` --------- Co-authored-by: LazyAfternoons Co-authored-by: Federico Mastrini --- .../credential/__tests__/machine.test.ts | 74 ++++++++++++++--- .../itwallet/machine/credential/actors.ts | 27 ++++++- .../itwallet/machine/credential/machine.ts | 79 +++++++++++++------ 3 files changed, 145 insertions(+), 35 deletions(-) diff --git a/ts/features/itwallet/machine/credential/__tests__/machine.test.ts b/ts/features/itwallet/machine/credential/__tests__/machine.test.ts index e2be2cfc845..61cc45a2b5f 100644 --- a/ts/features/itwallet/machine/credential/__tests__/machine.test.ts +++ b/ts/features/itwallet/machine/credential/__tests__/machine.test.ts @@ -2,18 +2,28 @@ import { AuthorizationDetail } from "@pagopa/io-react-native-wallet"; import { waitFor } from "@testing-library/react-native"; import _ from "lodash"; -import { createActor, fromPromise, StateFrom } from "xstate5"; +import { + createActor, + fromPromise, + StateFrom, + waitFor as waitForActor +} from "xstate5"; import { WalletAttestationResult } from "../../../common/utils/itwAttestationUtils"; -import { ItwStoredCredentialsMocks } from "../../../common/utils/itwMocksUtils"; +import { + ItwStatusAttestationMocks, + ItwStoredCredentialsMocks +} from "../../../common/utils/itwMocksUtils"; import { IssuerConfiguration, - RequestObject + RequestObject, + StoredCredential } from "../../../common/utils/itwTypesUtils"; import { ItwTags } from "../../tags"; import { InitializeWalletActorOutput, ObtainCredentialActorInput, ObtainCredentialActorOutput, + ObtainStatusAttestationActorInput, RequestCredentialActorInput, RequestCredentialActorOutput } from "../actors"; @@ -91,6 +101,12 @@ const T_REQUESTED_CREDENTIAL: RequestObject = { scope: "", state: "" }; +const T_STORED_STATUS_ATTESTATION: StoredCredential["storedStatusAttestation"] = + { + credentialStatus: "valid", + statusAttestation: "abcdefghijklmnopqrstuvwxyz", + parsedStatusAttestation: ItwStatusAttestationMocks.mdl + }; describe("itwCredentialIssuanceMachine", () => { const navigateToTrustIssuerScreen = jest.fn(); @@ -104,6 +120,7 @@ describe("itwCredentialIssuanceMachine", () => { const initializeWallet = jest.fn(); const requestCredential = jest.fn(); const obtainCredential = jest.fn(); + const obtainStatusAttestation = jest.fn(); const isSessionExpired = jest.fn(); @@ -127,7 +144,11 @@ describe("itwCredentialIssuanceMachine", () => { obtainCredential: fromPromise< ObtainCredentialActorOutput, ObtainCredentialActorInput - >(obtainCredential) + >(obtainCredential), + obtainStatusAttestation: fromPromise< + StoredCredential, + ObtainStatusAttestationActorInput + >(obtainStatusAttestation) }, guards: { isSessionExpired @@ -138,7 +159,7 @@ describe("itwCredentialIssuanceMachine", () => { jest.clearAllMocks(); }); - it("Should obtain a credential", async () => { + it("Should obtain a credential with a valid status attestation", async () => { const actor = createActor(mockedMachine); actor.start(); @@ -205,20 +226,48 @@ describe("itwCredentialIssuanceMachine", () => { }) ); + obtainStatusAttestation.mockImplementation(() => + Promise.resolve({ + ...ItwStoredCredentialsMocks.ts, + storedStatusAttestation: T_STORED_STATUS_ATTESTATION + }) + ); + actor.send({ type: "confirm-trust-data" }); - expect(actor.getSnapshot().value).toStrictEqual("ObtainingCredential"); expect(actor.getSnapshot().tags).toStrictEqual(new Set([ItwTags.Issuing])); - await waitFor(() => expect(obtainCredential).toHaveBeenCalledTimes(1)); + + // Step 1: get the credential + const intermediateState1 = await waitForActor(actor, snapshot => + snapshot.matches({ Issuance: "ObtainingCredential" }) + ); + expect(intermediateState1.value).toStrictEqual({ + Issuance: "ObtainingCredential" + }); + expect(obtainCredential).toHaveBeenCalledTimes(1); + + // Step 2: get the status attestation + const intermediateState2 = await waitForActor(actor, snapshot => + snapshot.matches({ Issuance: "ObtainingStatusAttestation" }) + ); + expect(intermediateState2.value).toStrictEqual({ + Issuance: "ObtainingStatusAttestation" + }); + expect(obtainStatusAttestation).toHaveBeenCalledTimes(1); expect(actor.getSnapshot().value).toStrictEqual( "DisplayingCredentialPreview" ); - expect(actor.getSnapshot().context).toMatchObject>({ - credential: ItwStoredCredentialsMocks.ts - }); + expect(actor.getSnapshot().context).toEqual( + expect.objectContaining>({ + credential: { + ...ItwStoredCredentialsMocks.ts, + storedStatusAttestation: T_STORED_STATUS_ATTESTATION + } + }) + ); expect(actor.getSnapshot().tags).toStrictEqual(new Set([])); expect(navigateToCredentialPreviewScreen).toHaveBeenCalledTimes(1); @@ -425,9 +474,12 @@ describe("itwCredentialIssuanceMachine", () => { type: "confirm-trust-data" }); - expect(actor.getSnapshot().value).toStrictEqual("ObtainingCredential"); + expect(actor.getSnapshot().value).toStrictEqual({ + Issuance: "ObtainingCredential" + }); expect(actor.getSnapshot().tags).toStrictEqual(new Set([ItwTags.Issuing])); await waitFor(() => expect(obtainCredential).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(obtainStatusAttestation).not.toHaveBeenCalled()); expect(actor.getSnapshot().value).toStrictEqual("Failure"); expect(actor.getSnapshot().context).toMatchObject>({ diff --git a/ts/features/itwallet/machine/credential/actors.ts b/ts/features/itwallet/machine/credential/actors.ts index 34742a4a9cc..89b6adee3c9 100644 --- a/ts/features/itwallet/machine/credential/actors.ts +++ b/ts/features/itwallet/machine/credential/actors.ts @@ -6,6 +6,9 @@ import * as credentialIssuanceUtils from "../../common/utils/itwCredentialIssuan import { itwCredentialsEidSelector } from "../../credentials/store/selectors"; import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors"; import { sessionTokenSelector } from "../../../../store/reducers/authentication"; +import { getCredentialStatusAttestation } from "../../common/utils/itwCredentialStatusAttestationUtils"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; +import { type Context } from "./context"; export type InitializeWalletActorOutput = Awaited< ReturnType @@ -25,6 +28,8 @@ export type ObtainCredentialActorOutput = Awaited< ReturnType >; +export type ObtainStatusAttestationActorInput = Pick; + export default (store: ReturnType) => { const initializeWallet = fromPromise( async () => { @@ -99,9 +104,29 @@ export default (store: ReturnType) => { }); }); + const obtainStatusAttestation = fromPromise< + StoredCredential, + ObtainStatusAttestationActorInput + >(async ({ input }) => { + assert(input.credential, "credential is undefined"); + + const { statusAttestation, parsedStatusAttestation } = + await getCredentialStatusAttestation(input.credential); + + return { + ...input.credential, + storedStatusAttestation: { + credentialStatus: "valid", + statusAttestation, + parsedStatusAttestation: parsedStatusAttestation.payload + } + }; + }); + return { initializeWallet, requestCredential, - obtainCredential + obtainCredential, + obtainStatusAttestation }; }; diff --git a/ts/features/itwallet/machine/credential/machine.ts b/ts/features/itwallet/machine/credential/machine.ts index 404c865fe44..1364e8a03bd 100644 --- a/ts/features/itwallet/machine/credential/machine.ts +++ b/ts/features/itwallet/machine/credential/machine.ts @@ -1,10 +1,12 @@ import { assign, fromPromise, setup } from "xstate5"; import { ItwTags } from "../tags"; import { ItwSessionExpiredError } from "../../api/client"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; import { InitializeWalletActorOutput, ObtainCredentialActorInput, ObtainCredentialActorOutput, + ObtainStatusAttestationActorInput, RequestCredentialActorInput, RequestCredentialActorOutput } from "./actors"; @@ -46,6 +48,10 @@ export const itwCredentialIssuanceMachine = setup({ obtainCredential: fromPromise< ObtainCredentialActorOutput, ObtainCredentialActorInput + >(notImplemented), + obtainStatusAttestation: fromPromise< + StoredCredential, + ObtainStatusAttestationActorInput >(notImplemented) }, guards: { @@ -132,36 +138,60 @@ export const itwCredentialIssuanceMachine = setup({ entry: "navigateToTrustIssuerScreen", on: { "confirm-trust-data": { - target: "ObtainingCredential" + target: "Issuance" }, close: { actions: ["closeIssuance"] } } }, - ObtainingCredential: { + Issuance: { + initial: "ObtainingCredential", tags: [ItwTags.Issuing], - invoke: { - src: "obtainCredential", - input: ({ context }) => ({ - credentialType: context.credentialType, - walletInstanceAttestation: context.walletInstanceAttestation, - wiaCryptoContext: context.wiaCryptoContext, - clientId: context.clientId, - codeVerifier: context.codeVerifier, - credentialDefinition: context.credentialDefinition, - requestedCredential: context.requestedCredential, - issuerConf: context.issuerConf - }), - onDone: { - target: "DisplayingCredentialPreview", - actions: assign(({ event }) => ({ - credential: event.output.credential - })) + states: { + ObtainingCredential: { + invoke: { + src: "obtainCredential", + input: ({ context }) => ({ + credentialType: context.credentialType, + walletInstanceAttestation: context.walletInstanceAttestation, + wiaCryptoContext: context.wiaCryptoContext, + clientId: context.clientId, + codeVerifier: context.codeVerifier, + credentialDefinition: context.credentialDefinition, + requestedCredential: context.requestedCredential, + issuerConf: context.issuerConf + }), + onDone: { + target: "ObtainingStatusAttestation", + actions: assign(({ event }) => ({ + credential: event.output.credential + })) + }, + onError: { + target: "#itwCredentialIssuanceMachine.Failure", + actions: "setFailure" + } + } }, - onError: { - target: "#itwCredentialIssuanceMachine.Failure", - actions: "setFailure" + ObtainingStatusAttestation: { + invoke: { + src: "obtainStatusAttestation", + input: ({ context }) => ({ credential: context.credential }), + onDone: { + target: "Completed", + actions: assign(({ event }) => ({ + credential: event.output + })) + }, + onError: { + target: "#itwCredentialIssuanceMachine.Failure", + actions: "setFailure" + } + } + }, + Completed: { + type: "final" } }, after: { @@ -169,6 +199,9 @@ export const itwCredentialIssuanceMachine = setup({ 4000: { actions: "navigateToCredentialPreviewScreen" } + }, + onDone: { + target: "DisplayingCredentialPreview" } }, DisplayingCredentialPreview: { @@ -192,7 +225,7 @@ export const itwCredentialIssuanceMachine = setup({ target: "Idle" }, retry: { - target: "#itwCredentialIssuanceMachine.DisplayingTrustIssuer" + target: "#itwCredentialIssuanceMachine.RequestingCredential" } } },