Skip to content

Commit

Permalink
feat(IT Wallet): [SIW-1789,SIW-1790] Always fetch the latest wallet i…
Browse files Browse the repository at this point in the history
…nstance status at app startup (#6474)

## Short description
This PR calls the new wallet instance status endpoint at every app
startup, without requesting an attestation. This removes the need to
cache the attestation, thus solving the issue of an outdated wallet
instance on the device.

## List of changes proposed in this pull request
- Call the new endpoint in place of the status attestation endpoint
- Modified tests

## How to test
Activate the wallet, then use IO Web to revoke it. Restart the app: the
wallet should be instantly reset on the device.

Co-authored-by: Federico Mastrini <[email protected]>
  • Loading branch information
gispada and mastro993 authored Dec 4, 2024
1 parent fcca9d5 commit 2a7cd4d
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 77 deletions.
15 changes: 15 additions & 0 deletions ts/features/itwallet/common/utils/itwAttestationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,18 @@ export const isWalletInstanceAttestationValid = (
const now = new Date();
return now < expiryDate;
};

/**
* Get the wallet instance status from the Wallet Provider.
* This operation is more lightweight than getting a new attestation to check the status.
* @param hardwareKeyTag The hardware key tag used to create the wallet instance
*/
export const getWalletInstanceStatus = (
hardwareKeyTag: string,
sessionToken: SessionToken
) =>
WalletInstance.getWalletInstanceStatus({
id: hardwareKeyTag,
walletProviderBaseUrl: itwWalletProviderBaseUrl,
appFetch: createItWalletFetch(itwWalletProviderBaseUrl, sessionToken)
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { type DeepPartial } from "redux";
import { expectSaga } from "redux-saga-test-plan";
import * as matchers from "redux-saga-test-plan/matchers";
import { throwError } from "redux-saga-test-plan/providers";
import { Errors } from "@pagopa/io-react-native-wallet";
import * as O from "fp-ts/lib/Option";
import {
checkWalletInstanceStateSaga,
getAttestationOrResetWalletInstance
getStatusOrResetWalletInstance
} from "../checkWalletInstanceStateSaga";
import { ItwLifecycleState } from "../../store/reducers";
import { GlobalState } from "../../../../../store/reducers/types";
import { getAttestation } from "../../../common/utils/itwAttestationUtils";
import { getWalletInstanceStatus } from "../../../common/utils/itwAttestationUtils";
import { StoredCredential } from "../../../common/utils/itwTypesUtils";
import { sessionTokenSelector } from "../../../../../store/reducers/authentication";
import { handleWalletInstanceResetSaga } from "../handleWalletInstanceResetSaga";
Expand All @@ -22,13 +20,6 @@ jest.mock("@pagopa/io-react-native-crypto", () => ({
deleteKey: jest.fn
}));

const WALLET_REVOKED_ERROR = new Errors.WalletProviderResponseError({
code: Errors.WalletProviderResponseErrorCodes.WalletInstanceRevoked,
message: "Revoked",
reason: {},
statusCode: 404
});

describe("checkWalletInstanceStateSaga", () => {
// TODO: improve the mocked store's typing, do not use DeepPartial
it("Does not check the wallet state when the wallet is INSTALLED", () => {
Expand All @@ -45,11 +36,11 @@ describe("checkWalletInstanceStateSaga", () => {
.withState(store)
.provide([[matchers.call.fn(ensureIntegrityServiceIsReady), true]])
.call.fn(ensureIntegrityServiceIsReady)
.not.call.fn(getAttestationOrResetWalletInstance)
.not.call.fn(getStatusOrResetWalletInstance)
.run();
});

it("Checks the wallet state when the wallet is OPERATIONAL and the attestation is valid", () => {
it("Checks the wallet state when the wallet is OPERATIONAL", () => {
const store: DeepPartial<GlobalState> = {
features: {
itWallet: {
Expand All @@ -69,19 +60,16 @@ describe("checkWalletInstanceStateSaga", () => {
[matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"],
[matchers.select(itwIsWalletInstanceAttestationValidSelector), false],
[matchers.select(itwIntegrityServiceReadySelector), true],
[
matchers.call.fn(getAttestation),
"aac6e82a-e27e-4293-9b55-94a9fab22763"
],
[matchers.call.fn(getWalletInstanceStatus), { is_revoked: false }],
[matchers.call.fn(ensureIntegrityServiceIsReady), true]
])
.call.fn(ensureIntegrityServiceIsReady)
.call.fn(getAttestationOrResetWalletInstance)
.call.fn(getStatusOrResetWalletInstance)
.not.call.fn(handleWalletInstanceResetSaga)
.run();
});

it("Checks and resets the wallet state when the wallet is OPERATIONAL and the attestation is not valid", () => {
it("Checks and resets the wallet state when the wallet is OPERATIONAL and the instance was revoked", () => {
const store: DeepPartial<GlobalState> = {
features: {
itWallet: {
Expand All @@ -99,16 +87,16 @@ describe("checkWalletInstanceStateSaga", () => {
.provide([
[matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"],
[matchers.select(itwIsWalletInstanceAttestationValidSelector), false],
[matchers.call.fn(getAttestation), throwError(WALLET_REVOKED_ERROR)],
[matchers.call.fn(getWalletInstanceStatus), { is_revoked: true }],
[matchers.call.fn(ensureIntegrityServiceIsReady), true]
])
.call.fn(ensureIntegrityServiceIsReady)
.call.fn(getAttestationOrResetWalletInstance)
.call.fn(getStatusOrResetWalletInstance)
.call.fn(handleWalletInstanceResetSaga)
.run();
});

it("Checks the wallet state when the wallet is VALID and the attestation is valid", () => {
it("Checks the wallet state when the wallet is VALID", () => {
const store: DeepPartial<GlobalState> = {
features: {
itWallet: {
Expand All @@ -126,19 +114,16 @@ describe("checkWalletInstanceStateSaga", () => {
.provide([
[matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"],
[matchers.select(itwIsWalletInstanceAttestationValidSelector), false],
[
matchers.call.fn(getAttestation),
"3396d31e-ac6a-4357-8083-cb5d3cda4d74"
],
[matchers.call.fn(getWalletInstanceStatus), { is_revoked: false }],
[matchers.call.fn(ensureIntegrityServiceIsReady), true]
])
.call.fn(ensureIntegrityServiceIsReady)
.call.fn(getAttestationOrResetWalletInstance)
.call.fn(getStatusOrResetWalletInstance)
.not.call.fn(handleWalletInstanceResetSaga)
.run();
});

it("Checks and resets the wallet state when the wallet is VALID and the attestation is not valid", () => {
it("Checks and resets the wallet state when the wallet is VALID and the instance was revoked", () => {
const store: DeepPartial<GlobalState> = {
features: {
itWallet: {
Expand All @@ -156,31 +141,12 @@ describe("checkWalletInstanceStateSaga", () => {
.provide([
[matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"],
[matchers.select(itwIsWalletInstanceAttestationValidSelector), false],
[matchers.call.fn(getAttestation), throwError(WALLET_REVOKED_ERROR)],
[matchers.call.fn(getWalletInstanceStatus), { is_revoked: true }],
[matchers.call.fn(ensureIntegrityServiceIsReady), true]
])
.call.fn(ensureIntegrityServiceIsReady)
.call.fn(getAttestationOrResetWalletInstance)
.call.fn(getStatusOrResetWalletInstance)
.call.fn(handleWalletInstanceResetSaga)
.run();
});
});

describe("getAttestationOrResetWalletInstance", () => {
it("should obtain a new WIA if it doesn't exist or has expired", () =>
expectSaga(getAttestationOrResetWalletInstance, "")
.provide([
[matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"],
[matchers.select(itwIsWalletInstanceAttestationValidSelector), false]
])
.call.fn(getAttestation)
.run());
it("should skip WIA obtainment if it exists and has not yet expired", () =>
expectSaga(getAttestationOrResetWalletInstance, "")
.provide([
[matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"],
[matchers.select(itwIsWalletInstanceAttestationValidSelector), true]
])
.not.call.fn(getAttestation)
.run());
});
Original file line number Diff line number Diff line change
@@ -1,46 +1,27 @@
import { Errors } from "@pagopa/io-react-native-wallet";
import * as O from "fp-ts/lib/Option";
import { call, put, select } from "typed-redux-saga/macro";
import { sessionTokenSelector } from "../../../../store/reducers/authentication";
import { ReduxSagaEffect } from "../../../../types/utils";
import { assert } from "../../../../utils/assert";
import { getAttestation } from "../../common/utils/itwAttestationUtils";
import { getWalletInstanceStatus } from "../../common/utils/itwAttestationUtils";
import { ensureIntegrityServiceIsReady } from "../../common/utils/itwIntegrityUtils";
import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors";
import { itwIsWalletInstanceAttestationValidSelector } from "../../walletInstance/store/reducers";
import { itwLifecycleIsOperationalOrValid } from "../store/selectors";
import { itwIntegritySetServiceIsReady } from "../../issuance/store/actions";
import { handleWalletInstanceResetSaga } from "./handleWalletInstanceResetSaga";

const {
isWalletProviderResponseError,
WalletProviderResponseErrorCodes: Codes
} = Errors;

export function* getAttestationOrResetWalletInstance(integrityKeyTag: string) {
export function* getStatusOrResetWalletInstance(integrityKeyTag: string) {
const sessionToken = yield* select(sessionTokenSelector);
assert(sessionToken, "Missing session token");

const isWalletInstanceAttestationValid = yield* select(
itwIsWalletInstanceAttestationValidSelector
const walletInstanceStatus = yield* call(
getWalletInstanceStatus,
integrityKeyTag,
sessionToken
);

if (isWalletInstanceAttestationValid) {
// The Wallet Instance Attestation is present and has not yet expired
return;
}

// If the Wallet Instance Attestation is not present or it has expired
// we need to request a new one
try {
yield* call(getAttestation, integrityKeyTag, sessionToken);
} catch (err) {
if (
isWalletProviderResponseError(err, Codes.WalletInstanceRevoked) ||
isWalletProviderResponseError(err, Codes.WalletInstanceNotFound)
) {
yield* call(handleWalletInstanceResetSaga);
}
if (walletInstanceStatus.is_revoked) {
yield* call(handleWalletInstanceResetSaga);
}
}

Expand All @@ -66,7 +47,7 @@ export function* checkWalletInstanceStateSaga(): Generator<

// Only operational or valid wallet instances can be revoked.
if (isItwOperationalOrValid && O.isSome(integrityKeyTag)) {
yield* call(getAttestationOrResetWalletInstance, integrityKeyTag.value);
yield* call(getStatusOrResetWalletInstance, integrityKeyTag.value);
}
} catch (e) {
// Ignore the error, the integrity service is not available and an error will occur if the wallet requests an attestation
Expand Down

0 comments on commit 2a7cd4d

Please sign in to comment.