Skip to content

Commit

Permalink
feat(IT Wallet): [SIW-1522] Get credential status attestation after i…
Browse files Browse the repository at this point in the history
…ssuance (#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 <[email protected]>
Co-authored-by: Federico Mastrini <[email protected]>
  • Loading branch information
3 people authored Sep 17, 2024
1 parent 485a3ad commit 97797da
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 35 deletions.
74 changes: 63 additions & 11 deletions ts/features/itwallet/machine/credential/__tests__/machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand All @@ -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();

Expand All @@ -127,7 +144,11 @@ describe("itwCredentialIssuanceMachine", () => {
obtainCredential: fromPromise<
ObtainCredentialActorOutput,
ObtainCredentialActorInput
>(obtainCredential)
>(obtainCredential),
obtainStatusAttestation: fromPromise<
StoredCredential,
ObtainStatusAttestationActorInput
>(obtainStatusAttestation)
},
guards: {
isSessionExpired
Expand All @@ -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();

Expand Down Expand Up @@ -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<Partial<Context>>({
credential: ItwStoredCredentialsMocks.ts
});
expect(actor.getSnapshot().context).toEqual(
expect.objectContaining<Partial<Context>>({
credential: {
...ItwStoredCredentialsMocks.ts,
storedStatusAttestation: T_STORED_STATUS_ATTESTATION
}
})
);
expect(actor.getSnapshot().tags).toStrictEqual(new Set([]));
expect(navigateToCredentialPreviewScreen).toHaveBeenCalledTimes(1);

Expand Down Expand Up @@ -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<Partial<Context>>({
Expand Down
27 changes: 26 additions & 1 deletion ts/features/itwallet/machine/credential/actors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof credentialIssuanceUtils.initializeWallet>
Expand All @@ -25,6 +28,8 @@ export type ObtainCredentialActorOutput = Awaited<
ReturnType<typeof credentialIssuanceUtils.obtainCredential>
>;

export type ObtainStatusAttestationActorInput = Pick<Context, "credential">;

export default (store: ReturnType<typeof useIOStore>) => {
const initializeWallet = fromPromise<InitializeWalletActorOutput>(
async () => {
Expand Down Expand Up @@ -99,9 +104,29 @@ export default (store: ReturnType<typeof useIOStore>) => {
});
});

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
};
};
79 changes: 56 additions & 23 deletions ts/features/itwallet/machine/credential/machine.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -46,6 +48,10 @@ export const itwCredentialIssuanceMachine = setup({
obtainCredential: fromPromise<
ObtainCredentialActorOutput,
ObtainCredentialActorInput
>(notImplemented),
obtainStatusAttestation: fromPromise<
StoredCredential,
ObtainStatusAttestationActorInput
>(notImplemented)
},
guards: {
Expand Down Expand Up @@ -132,43 +138,70 @@ 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: {
// If this step takes more than 4 seconds, we navigate to the next screen and display a loading indicator
4000: {
actions: "navigateToCredentialPreviewScreen"
}
},
onDone: {
target: "DisplayingCredentialPreview"
}
},
DisplayingCredentialPreview: {
Expand All @@ -192,7 +225,7 @@ export const itwCredentialIssuanceMachine = setup({
target: "Idle"
},
retry: {
target: "#itwCredentialIssuanceMachine.DisplayingTrustIssuer"
target: "#itwCredentialIssuanceMachine.RequestingCredential"
}
}
},
Expand Down

0 comments on commit 97797da

Please sign in to comment.