Skip to main content
Version: 5.0

Getting Started with Identity Vault

Generate the Application

Before we explore the use of Identity Vault, we need to scaffold an application. In this section, we will generate a tabs-based @ionic/react application, perform some basic configuration, and add the iOS and Android platforms.

If you need to refresh your memory on the overall developer workflow for Capacitor, please do so now. However, here is a synopsis of the commands you will use the most while performing this tutorial:

  • npm start: Start the development server so the application can be run in the browser.
  • npm run build: Build the web portion of the application.
  • npx cap sync: Copy the web app and any new plugins into the native applications.
  • npx cap open android: Open Android Studio in order to build, run, and debug the application on Android.
  • npx cap open ios: Open Xcode in order to build, run, and debug the application on iOS.

Let's get started.

Terminal

_10
ionic start iv-getting-started tabs --type=react

Use the Ionic CLI to generate the application.

Terminal

_10
ionic start iv-getting-started tabs --type=react
_10
cd iv-getting-started

Change directory into the newly generated project.

Terminal
capacitor.config.ts

_12
import { CapacitorConfig } from '@capacitor/cli';
_12
_12
const config: CapacitorConfig = {
_12
appId: 'io.ionic.gettingstartediv',
_12
appName: 'iv-getting-started',
_12
webDir: 'dist',
_12
server: {
_12
androidScheme: 'https',
_12
},
_12
};
_12
_12
export default config;

Change the appId to be something unique. The appId is used as the bundle ID / application ID. Therefore it should be a string that is unique to your organization and application. We will use io.ionic.gettingstartediv for this application.

It is best to do this before adding the iOS and Android platforms to ensure they are setup properly from the start.

Terminal
capacitor.config.ts

_10
ionic start iv-getting-started-ac tabs --type=react
_10
cd iv-getting-started
_10
npm run build
_10
ionic cap add android
_10
ionic cap add ios

Build the application and install the platforms.

Terminal
capacitor.config.ts
package.json

_15
{
_15
"name": "iv-getting-started",
_15
"version": "0.0.1",
_15
"author": "Ionic Framework",
_15
"homepage": "https://ionicframework.com/",
_15
"scripts": {
_15
"dev": "vite",
_15
"build": "tsc && vite build && cap sync",
_15
"preview": "vite preview",
_15
"test.e2e": "cypress run",
_15
"test.unit": "vitest",
_15
"lint": "eslint"
_15
},
_15
...
_15
}

We should do a cap sync with each build. Change the scripts in package.json to do this.

Use the Ionic CLI to generate the application.

Change directory into the newly generated project.

Change the appId to be something unique. The appId is used as the bundle ID / application ID. Therefore it should be a string that is unique to your organization and application. We will use io.ionic.gettingstartediv for this application.

It is best to do this before adding the iOS and Android platforms to ensure they are setup properly from the start.

Build the application and install the platforms.

We should do a cap sync with each build. Change the scripts in package.json to do this.

Terminal

_10
ionic start iv-getting-started tabs --type=react

Install Identity Vault

In order to install Identity Vault, you will need to use the ionic enterprise register command to register your product key. This will create a .npmrc file containing the product key.

If you have already performed that step for your production application, you can just copy the .npmrc file from your production project. Since this application is for learning purposes only, you don't need to obtain another key.

You can now install Identity Vault and sync the platforms:

Terminal

_10
npm install @ionic-enterprise/identity-vault
_10
npx cap sync

Create the Session Model

We first need to define the shape of the authentication session data via a TypeScript interface. Create a src/models directory and a src/models/Session.ts file.

src/models/Session.ts

_10
export interface Session {
_10
firstName: string;
_10
lastName: string;
_10
email: string;
_10
accessToken: string;
_10
refreshToken: string;
_10
}

Create the Utility Files

Our tutorial application will have a single vault that simulates storing our application's authentication session information. To manage this vault, we will create two utility files:

  • vault-factory: Builds either a Vault or BrowserVault depending on the whether our application is running in a web-native or web context, respectively. The BrowserVault only mimics the basic behavior of the Vault for the purpose of running on the web and provides no security, as there as none available in the browser context.
  • session-vault: Manages the vault and its contents.

vault-factory

Create a src/util folder and add a file named src/util/vault-factory.ts with the following contents:

src/util/vault-factory.ts

_10
import { Capacitor } from '@capacitor/core';
_10
import { BrowserVault, Vault } from '@ionic-enterprise/identity-vault';
_10
_10
export const createVault = (): Vault | BrowserVault => {
_10
return Capacitor.isNativePlatform() ? new Vault() : new BrowserVault();
_10
};

session-vault

The session-vault file will contain the functions that are used to manage the session vault for the application. Create the src/util/session-vault.ts file with the following contents. We will build the necessary functions in this tutorial.

src/util/session-vault.ts

_10
import { createVault } from './vault-factory';

Create and Initialization the Vault

Before we use Identity Vault, we need to make sure that our vault is properly created and initialized. It is important to note that creation and initialization are different processes. Creation is performed when the module for the vault-factory utility file is constructed and is limited to the creation of a JavaScript object.

The initialization involves communication with the native layer. As such it is asynchronous. Since initialization needs to complete before we can begin normal operation of the application, we run the initialization and await its completion before the main application component is mounted.

warning

Awaiting the completion of initialization in this manner is a best-practice that should always be followed.

src/util/session-vault.ts
src/main.tsx

_10
import {
_10
BrowserVault,
_10
Vault
_10
} from '@ionic-enterprise/identity-vault';
_10
import { createVault } from './vault-factory';
_10
_10
const vault: Vault | BrowserVault = createVault();

Create the vault using our factory function.

src/util/session-vault.ts
src/main.tsx

_10
import {
_10
BrowserVault,
_10
Vault
_10
} from '@ionic-enterprise/identity-vault';
_10
import { createVault } from './vault-factory';
_10
import { Session } from '../models/Session';
_10
_10
const vault: Vault | BrowserVault = createVault();
_10
let session: Session | null = null;

Create the session variable. To sync the store with React, we will use useSyncExternalStore.

src/util/session-vault.ts
src/main.tsx

_23
import {
_23
BrowserVault,
_23
Vault,
_23
VaultType,
_23
DeviceSecurityType,
_23
} from '@ionic-enterprise/identity-vault';
_23
import { createVault } from './vault-factory';
_23
import { Session } from '../models/Session';
_23
_23
const vault: Vault | BrowserVault = createVault();
_23
let session: Session | null = null;
_23
_23
export const initializeVault = async (): Promise<void> => {
_23
try {
_23
await vault.initialize({
_23
key: 'io.ionic.gettingstartedivreact',
_23
type: VaultType.SecureStorage,
_23
deviceSecurityType: DeviceSecurityType.None,
_23
});
_23
} catch (e: unknown) {
_23
await vault.clear();
_23
}
_23
};

Create the initializeVault function from which we will perform all vault initialization. At this time, the only thing we need to do is pass a configuration object to our vault. The meaning of the configuration properties will be explained later.

If the initialize() fails the best thing to do with the vault is to clear it.

src/util/session-vault.ts
src/main.tsx

_13
import React from 'react';
_13
import { createRoot } from 'react-dom/client';
_13
import App from './App';
_13
_13
import { initializeVault } from './util/session-vault';
_13
_13
const container = document.getElementById('root');
_13
const root = createRoot(container!);
_13
initializeVault().then(() => root.render(
_13
<React.StrictMode>
_13
<App />
_13
</React.StrictMode>
_13
));

In our src/main.tsx, execute the initializeVault function prior to rendering the React app.

Create the vault using our factory function.

Create the session variable. To sync the store with React, we will use useSyncExternalStore.

Create the initializeVault function from which we will perform all vault initialization. At this time, the only thing we need to do is pass a configuration object to our vault. The meaning of the configuration properties will be explained later.

If the initialize() fails the best thing to do with the vault is to clear it.

In our src/main.tsx, execute the initializeVault function prior to rendering the React app.

src/util/session-vault.ts
src/main.tsx

_10
import {
_10
BrowserVault,
_10
Vault
_10
} from '@ionic-enterprise/identity-vault';
_10
import { createVault } from './vault-factory';
_10
_10
const vault: Vault | BrowserVault = createVault();

In this section, we created a vault using the key io.ionic.gettingstartediv. Our vault is a "Secure Storage" vault, which means that the information we store in the vault is encrypted in the keychain / keystore and is only visible to our application, but the vault is never locked. We will explore other types of vaults later in this tutorial.

Store a Value

Let's store some data in the vault. Here, we will:

  • Add the necessary code to utilize useSyncExternalStore.
  • Add a function to session-vault to store a session.
  • Add a button to our Tab1 page to store a fake session.
src/util/session-vault.ts

_41
import {
_41
BrowserVault,
_41
Vault,
_41
VaultType,
_41
DeviceSecurityType,
_41
} from '@ionic-enterprise/identity-vault';
_41
import { createVault } from './vault-factory';
_41
import { Session } from '../models/Session';
_41
_41
const vault: Vault | BrowserVault = createVault();
_41
let session: Session | null = null;
_41
let listeners: any[] = [];
_41
_41
export const initializeVault = async (): Promise<void> => {
_41
try {
_41
await vault.initialize({
_41
key: 'io.ionic.gettingstartedivreact',
_41
type: VaultType.SecureStorage,
_41
deviceSecurityType: DeviceSecurityType.None,
_41
});
_41
} catch (e: unknown) {
_41
await vault.clear();
_41
}
_41
};
_41
_41
export const getSnapshot = (): Session | null => {
_41
return session;
_41
}
_41
_41
export const subscribe = (listener: any) => {
_41
listeners = [...listeners, listener];
_41
return () => {
_41
listeners = listeners.filter((l) => l !== listener);
_41
};
_41
};
_41
_41
export const emitChange = () => {
_41
for (let listener of listeners) {
_41
listener();
_41
}
_41
};

Before we add the logic to update the vault, we first need to add a small bit of boilerplate code in order to track the state of our session variable.

src/util/session-vault.ts

_47
import {
_47
BrowserVault,
_47
Vault,
_47
VaultType,
_47
DeviceSecurityType,
_47
} from '@ionic-enterprise/identity-vault';
_47
import { createVault } from './vault-factory';
_47
import { Session } from '../models/Session';
_47
_47
const vault: Vault | BrowserVault = createVault();
_47
let session: Session | null = null;
_47
let listeners: any[] = [];
_47
_47
export const initializeVault = async (): Promise<void> => {
_47
try {
_47
await vault.initialize({
_47
key: 'io.ionic.gettingstartedivreact',
_47
type: VaultType.SecureStorage,
_47
deviceSecurityType: DeviceSecurityType.None,
_47
});
_47
} catch (e: unknown) {
_47
await vault.clear();
_47
}
_47
};
_47
_47
export const getSnapshot = (): Session | null => {
_47
return session;
_47
}
_47
_47
export const subscribe = (listener: any) => {
_47
listeners = [...listeners, listener];
_47
return () => {
_47
listeners = listeners.filter((l) => l !== listener);
_47
};
_47
};
_47
_47
export const emitChange = () => {
_47
for (let listener of listeners) {
_47
listener();
_47
}
_47
};
_47
_47
export const storeSession = async (newSession: Session): Promise<void> => {
_47
await vault.setValue('session', newSession);
_47
session = newSession;
_47
emitChange();
_47
}

Now that we can track the state of our session, we can create our function to store our data. We can store multiple items within the vault, each with their own key. For this application, we will store a single item with the key of session. The vault has a setValue() method that is used for this purpose. Modify src/util/session-vault.ts to store the session.

Before we add the logic to update the vault, we first need to add a small bit of boilerplate code in order to track the state of our session variable.

Now that we can track the state of our session, we can create our function to store our data. We can store multiple items within the vault, each with their own key. For this application, we will store a single item with the key of session. The vault has a setValue() method that is used for this purpose. Modify src/util/session-vault.ts to store the session.

src/util/session-vault.ts

_41
import {
_41
BrowserVault,
_41
Vault,
_41
VaultType,
_41
DeviceSecurityType,
_41
} from '@ionic-enterprise/identity-vault';
_41
import { createVault } from './vault-factory';
_41
import { Session } from '../models/Session';
_41
_41
const vault: Vault | BrowserVault = createVault();
_41
let session: Session | null = null;
_41
let listeners: any[] = [];
_41
_41
export const initializeVault = async (): Promise<void> => {
_41
try {
_41
await vault.initialize({
_41
key: 'io.ionic.gettingstartedivreact',
_41
type: VaultType.SecureStorage,
_41
deviceSecurityType: DeviceSecurityType.None,
_41
});
_41
} catch (e: unknown) {
_41
await vault.clear();
_41
}
_41
};
_41
_41
export const getSnapshot = (): Session | null => {
_41
return session;
_41
}
_41
_41
export const subscribe = (listener: any) => {
_41
listeners = [...listeners, listener];
_41
return () => {
_41
listeners = listeners.filter((l) => l !== listener);
_41
};
_41
};
_41
_41
export const emitChange = () => {
_41
for (let listener of listeners) {
_41
listener();
_41
}
_41
};

Notice that we have created a very light wrapper around the vault's setValue() method. This is often all that is required. It is best-practice to encapsulate the vault in a function like this one and only expose the functionality that makes sense for your application.

With the "store session" feature properly abstracted, add a button to the Tab1 page that will simulate logging in by storing some fake authentication data in the vault.

src/pages/Tab1.tsx

_31
import {
_31
IonContent,
_31
IonHeader,
_31
IonPage,
_31
IonTitle,
_31
IonToolbar,
_31
} from '@ionic/react';
_31
import ExploreContainer from '../components/ExploreContainer';
_31
import './Tab1.css';
_31
_31
const Tab1: React.FC = () => {
_31
return (
_31
<IonPage>
_31
<IonHeader>
_31
<IonToolbar>
_31
<IonTitle>Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<IonContent fullscreen>
_31
<IonHeader collapse="condense">
_31
<IonToolbar>
_31
<IonTitle size="large">Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<ExploreContainer name="Tab 1 page" />
_31
</IonContent>
_31
</IonPage>
_31
);
_31
};
_31
_31
export default Tab1;

We are currently displaying the generic starter "Explore Container" data.

src/pages/Tab1.tsx

_41
import {
_41
IonButton,
_41
IonContent,
_41
IonHeader,
_41
IonItem,
_41
IonLabel,
_41
IonList,
_41
IonPage,
_41
IonTitle,
_41
IonToolbar,
_41
} from '@ionic/react';
_41
import './Tab1.css';
_41
_41
const Tab1: React.FC = () => {
_41
return (
_41
<IonPage>
_41
<IonHeader>
_41
<IonToolbar>
_41
<IonTitle>Tab 1</IonTitle>
_41
</IonToolbar>
_41
</IonHeader>
_41
<IonContent fullscreen>
_41
<IonHeader collapse="condense">
_41
<IonToolbar>
_41
<IonTitle size="large">Tab 1</IonTitle>
_41
</IonToolbar>
_41
</IonHeader>
_41
_41
<IonList>
_41
<IonItem>
_41
<IonLabel>
_41
<IonButton expand="block">Store</IonButton>
_41
</IonLabel>
_41
</IonItem>
_41
</IonList>
_41
</IonContent>
_41
</IonPage>
_41
);
_41
};
_41
_41
export default Tab1;

Replace the explore container with a list containing a button.

src/pages/Tab1.tsx

_52
import {
_52
IonButton,
_52
IonContent,
_52
IonHeader,
_52
IonItem,
_52
IonLabel,
_52
IonList,
_52
IonPage,
_52
IonTitle,
_52
IonToolbar,
_52
} from '@ionic/react';
_52
import './Tab1.css';
_52
import { storeSession } from '../util/session-vault';
_52
_52
const Tab1: React.FC = () => {
_52
const storeClicked = async (): Promise<void> => {
_52
await storeSession({
_52
email: 'test@ionic.io',
_52
firstName: 'Tessa',
_52
lastName: 'Testsmith',
_52
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_52
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_52
});
_52
};
_52
_52
return (
_52
<IonPage>
_52
<IonHeader>
_52
<IonToolbar>
_52
<IonTitle>Tab 1</IonTitle>
_52
</IonToolbar>
_52
</IonHeader>
_52
<IonContent fullscreen>
_52
<IonHeader collapse="condense">
_52
<IonToolbar>
_52
<IonTitle size="large">Tab 1</IonTitle>
_52
</IonToolbar>
_52
</IonHeader>
_52
_52
<IonList>
_52
<IonItem>
_52
<IonLabel>
_52
<IonButton expand="block" onClick={storeClicked}>Store</IonButton>
_52
</IonLabel>
_52
</IonItem>
_52
</IonList>
_52
</IonContent>
_52
</IonPage>
_52
);
_52
};
_52
_52
export default Tab1;

Store some fake test data.

We are currently displaying the generic starter "Explore Container" data.

Replace the explore container with a list containing a button.

Store some fake test data.

src/pages/Tab1.tsx

_31
import {
_31
IonContent,
_31
IonHeader,
_31
IonPage,
_31
IonTitle,
_31
IonToolbar,
_31
} from '@ionic/react';
_31
import ExploreContainer from '../components/ExploreContainer';
_31
import './Tab1.css';
_31
_31
const Tab1: React.FC = () => {
_31
return (
_31
<IonPage>
_31
<IonHeader>
_31
<IonToolbar>
_31
<IonTitle>Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<IonContent fullscreen>
_31
<IonHeader collapse="condense">
_31
<IonToolbar>
_31
<IonTitle size="large">Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<ExploreContainer name="Tab 1 page" />
_31
</IonContent>
_31
</IonPage>
_31
);
_31
};
_31
_31
export default Tab1;

We have stored data in our vault. The next step is to get the data back out of the vault.

Get a Value

In order to better illustrate the operation of the vault, we will modify the Tab1Page to display our session if one is stored. We can get the current session data with useSyncExternalStore.

src/pages/Tab1.tsx

_64
import {
_64
IonButton,
_64
IonContent,
_64
IonHeader,
_64
IonItem,
_64
IonLabel,
_64
IonList,
_64
IonPage,
_64
IonTitle,
_64
IonToolbar,
_64
} from '@ionic/react';
_64
import { useSyncExternalStore } from 'react';
_64
import './Tab1.css';
_64
import { storeSession, subscribe, getSnapshot } from '../util/session-vault';
_64
_64
const Tab1: React.FC = () => {
_64
const session = useSyncExternalStore(subscribe, getSnapshot);
_64
_64
const storeClicked = async (): Promise<void> => {
_64
await storeSession({
_64
email: 'test@ionic.io',
_64
firstName: 'Tessa',
_64
lastName: 'Testsmith',
_64
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_64
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_64
});
_64
};
_64
_64
return (
_64
<IonPage>
_64
<IonHeader>
_64
<IonToolbar>
_64
<IonTitle>Tab 1</IonTitle>
_64
</IonToolbar>
_64
</IonHeader>
_64
<IonContent fullscreen>
_64
<IonHeader collapse="condense">
_64
<IonToolbar>
_64
<IonTitle size="large">Tab 1</IonTitle>
_64
</IonToolbar>
_64
</IonHeader>
_64
_64
<IonList>
_64
<IonItem>
_64
<IonLabel>
_64
<IonButton expand="block" onClick={storeClicked}>Store</IonButton>
_64
</IonLabel>
_64
</IonItem>
_64
_64
<IonItem>
_64
<div>
_64
<div>{ session?.email }</div>
_64
<div>{ session?.firstName } { session?.lastName }</div>
_64
<div>{ session?.accessToken }</div>
_64
<div>{ session?.refreshToken }</div>
_64
</div>
_64
</IonItem>
_64
</IonList>
_64
</IonContent>
_64
</IonPage>
_64
);
_64
};
_64
_64
export default Tab1;

This displays the session when the user presses the "Store" button. However, if you refresh the browser or restart the application, the session data is no longer displayed. That is because our session variable was cleared.

src/util/session-vault.ts

_55
import {
_55
BrowserVault,
_55
Vault,
_55
VaultType,
_55
DeviceSecurityType,
_55
} from '@ionic-enterprise/identity-vault';
_55
import { createVault } from './vault-factory';
_55
import { Session } from '../models/Session';
_55
_55
const vault: Vault | BrowserVault = createVault();
_55
let session: Session | null = null;
_55
let listeners: any[] = [];
_55
_55
export const initializeVault = async (): Promise<void> => {
_55
try {
_55
await vault.initialize({
_55
key: 'io.ionic.gettingstartedivreact',
_55
type: VaultType.SecureStorage,
_55
deviceSecurityType: DeviceSecurityType.None,
_55
});
_55
} catch (e: unknown) {
_55
await vault.clear();
_55
}
_55
};
_55
_55
export const getSnapshot = (): Session | null => {
_55
return session;
_55
}
_55
_55
export const subscribe = (listener: any) => {
_55
listeners = [...listeners, listener];
_55
return () => {
_55
listeners = listeners.filter((l) => l !== listener);
_55
};
_55
};
_55
_55
export const emitChange = () => {
_55
for (let listener of listeners) {
_55
listener();
_55
}
_55
};
_55
_55
export const storeSession = async (newSession: Session): Promise<void> => {
_55
await vault.setValue('session', newSession);
_55
session = newSession;
_55
emitChange();
_55
}
_55
_55
export const getSession = async (): Promise<void> => {
_55
if (session === null) {
_55
if (await vault.isEmpty()) session = null;
_55
else session = await vault.getValue<Session>('session');
_55
}
_55
emitChange();
_55
};

Add a function to session-vault that encapsulates getting the session. Checking if the vault is empty first ensures that we don't try to unlock a vault that may be locked but empty, which can happen in some cases.

src/util/session-vault.ts

_57
import {
_57
BrowserVault,
_57
Vault,
_57
VaultType,
_57
DeviceSecurityType,
_57
} from '@ionic-enterprise/identity-vault';
_57
import { createVault } from './vault-factory';
_57
import { Session } from '../models/Session';
_57
_57
const vault: Vault | BrowserVault = createVault();
_57
let session: Session | null = null;
_57
let listeners: any[] = [];
_57
_57
export const initializeVault = async (): Promise<void> => {
_57
try {
_57
await vault.initialize({
_57
key: 'io.ionic.gettingstartedivreact',
_57
type: VaultType.SecureStorage,
_57
deviceSecurityType: DeviceSecurityType.None,
_57
});
_57
} catch (e: unknown) {
_57
await vault.clear();
_57
}
_57
_57
await getSession();
_57
};
_57
_57
export const getSnapshot = (): Session | null => {
_57
return session;
_57
}
_57
_57
export const subscribe = (listener: any) => {
_57
listeners = [...listeners, listener];
_57
return () => {
_57
listeners = listeners.filter((l) => l !== listener);
_57
};
_57
};
_57
_57
export const emitChange = () => {
_57
for (let listener of listeners) {
_57
listener();
_57
}
_57
};
_57
_57
export const storeSession = async (newSession: Session): Promise<void> => {
_57
await vault.setValue('session', newSession);
_57
session = newSession;
_57
emitChange();
_57
}
_57
_57
export const getSession = async (): Promise<void> => {
_57
if (session === null) {
_57
if (await vault.isEmpty()) session = null;
_57
else session = await vault.getValue<Session>('session');
_57
}
_57
emitChange();
_57
};

Add our getSession function to the initializeVault function so that we fetch vault data upon initialization.

Add a function to session-vault that encapsulates getting the session. Checking if the vault is empty first ensures that we don't try to unlock a vault that may be locked but empty, which can happen in some cases.

Add our getSession function to the initializeVault function so that we fetch vault data upon initialization.

src/util/session-vault.ts

_55
import {
_55
BrowserVault,
_55
Vault,
_55
VaultType,
_55
DeviceSecurityType,
_55
} from '@ionic-enterprise/identity-vault';
_55
import { createVault } from './vault-factory';
_55
import { Session } from '../models/Session';
_55
_55
const vault: Vault | BrowserVault = createVault();
_55
let session: Session | null = null;
_55
let listeners: any[] = [];
_55
_55
export const initializeVault = async (): Promise<void> => {
_55
try {
_55
await vault.initialize({
_55
key: 'io.ionic.gettingstartedivreact',
_55
type: VaultType.SecureStorage,
_55
deviceSecurityType: DeviceSecurityType.None,
_55
});
_55
} catch (e: unknown) {
_55
await vault.clear();
_55
}
_55
};
_55
_55
export const getSnapshot = (): Session | null => {
_55
return session;
_55
}
_55
_55
export const subscribe = (listener: any) => {
_55
listeners = [...listeners, listener];
_55
return () => {
_55
listeners = listeners.filter((l) => l !== listener);
_55
};
_55
};
_55
_55
export const emitChange = () => {
_55
for (let listener of listeners) {
_55
listener();
_55
}
_55
};
_55
_55
export const storeSession = async (newSession: Session): Promise<void> => {
_55
await vault.setValue('session', newSession);
_55
session = newSession;
_55
emitChange();
_55
}
_55
_55
export const getSession = async (): Promise<void> => {
_55
if (session === null) {
_55
if (await vault.isEmpty()) session = null;
_55
else session = await vault.getValue<Session>('session');
_55
}
_55
emitChange();
_55
};

We now have a way to store and retrieve the session. When you first run the application, the session area will be blank. When you press the Store button you will see the session information on the page. If you restart the application, you will see the session information.

If you would like to clear the session information at this point, remove the application from your device (physical or simulated) and re-install it. On the web, you can close the running tab and open new one.

Next we will see how to remove this data from within our application.

Remove the Session from the Vault

The vault has two different methods that we can use to remove the data:

  • clear: Clear all of the data stored in the vault and remove the vault from the keystore / keychain.
    • This operation does not require the vault to be unlocked.
    • This operation will remove the existing vault from the keychain / keystore.
    • Subsequent operations on the vault such as storing a new session will not require the vault to be unlocked since the value had been removed.
    • Use this method if your vault stores a single logical entity, even if it uses multiple entries to do so.
  • removeValue: Clear just the data stored with the specified key.
    • This operation does require the vault to be unlocked.
    • This operation will not remove the existing vault from the keychain / keystore even though the vault may be empty.
    • Subsequent operations on the vault such as storing a new session may require the vault to be unlocked since the vault had been removed.
    • Use this method if your vault stores multiple logical entities.
note

We will address locking and unlocking a vault later in this tutorial.

Our vault stores session information. Having a single vault that stores only the session information is the best-practice for this type of data, and it is the practice we are using here. Thus we will use the clear() method to clear the session.

src/util/session-vault.ts

_63
import {
_63
BrowserVault,
_63
Vault,
_63
VaultType,
_63
DeviceSecurityType,
_63
} from '@ionic-enterprise/identity-vault';
_63
import { createVault } from './vault-factory';
_63
export const clearSession = async (): Promise<void> => {

Add a "Clear" button to the Tab1 page.

src/pages/Tab1.tsx

_77
import {
_77
IonButton,
_77
IonContent,
_77
IonHeader,
_77
IonItem,
_77
IonLabel,
_77
IonList,
_77
IonPage,
_77
IonTitle,
_77
IonToolbar,
_77
} from '@ionic/react';
_77
import { useSyncExternalStore } from 'react';
_77
import './Tab1.css';
_77
import {
_77
storeSession,
_77
subscribe,
_77
getSnapshot,
_77
clearSession,
_77
} from '../util/session-vault';
_77
_77
const Tab1: React.FC = () => {
_77
const session = useSyncExternalStore(subscribe, getSnapshot);
_77
_77
const storeClicked = async (): Promise<void> => {
_77
await storeSession({
_77
email: 'test@ionic.io',
_77
firstName: 'Tessa',
_77
lastName: 'Testsmith',
_77
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_77
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_77
});
_77
};
_77
_77
return (
_77
<IonPage>
_77
<IonHeader>
_77
<IonToolbar>
_77
<IonTitle>Tab 1</IonTitle>
_77
</IonToolbar>
_77
</IonHeader>
_77
<IonContent fullscreen>
_77
<IonHeader collapse="condense">
_77
<IonToolbar>
_77
<IonTitle size="large">Tab 1</IonTitle>
_77
</IonToolbar>
_77
</IonHeader>
_77
_77
<IonList>
_77
<IonItem>
_77
<IonButton expand="block" color="danger" onClick={clearSession}>

Upon clicking the "Clear" button, the session data will be removed from the vault and will no longer render.

Updating the Vault Type

We are currently using a "Secure Storage" vault, but there are several other vault types. In this section, we will explore the DeviceSecurity, InMemory, and SecureStorage types.

Setting the Vault Type

We can use the vault's updateConfig() method to change the type of vault that the application is using.

src/util/session-vault.ts

_63
import {
_63
BrowserVault,
_63
Vault,
_63
VaultType,
_63
DeviceSecurityType,
_63
} from '@ionic-enterprise/identity-vault';
_63
import { createVault } from './vault-factory';
_63
import { Session } from '../models/Session';
_63
_63
const vault: Vault | BrowserVault = createVault();
_63
let session: Session | null = null;
_63
let listeners: any[] = [];
_63
_63
export const initializeVault = async (): Promise<void> => {
_63
try {
_63
await vault.initialize({
_63
key: 'io.ionic.gettingstartedivreact',
_63
type: VaultType.SecureStorage,
_63
deviceSecurityType: DeviceSecurityType.None,
_63
});
_63
} catch (e: unknown) {
_63
await vault.clear();
_63
}
_63
_63
await getSession();
_63
};
_63
_63
export const getSnapshot = (): Session | null => {
_63
return session;
_63
}
_63
_63
export const subscribe = (listener: any) => {
_63
listeners = [...listeners, listener];
_63
return () => {
_63
listeners = listeners.filter((l) => l !== listener);
_63
};
_63
};
_63
_63
export const emitChange = () => {
_63
for (let listener of listeners) {
_63
listener();
_63
}
_63
};
_63
_63
export const storeSession = async (newSession: Session): Promise<void> => {
_63
await vault.setValue('session', newSession);
_63
session = newSession;
_63
emitChange();
_63
}
_63
_63
export const getSession = async (): Promise<void> => {
_63
if (session === null) {
_63
if (await vault.isEmpty()) session = null;
_63
else session = await vault.getValue<Session>('session');
_63
}
_63
emitChange();
_63
};
_63
_63
export const clearSession = async (): Promise<void> => {
_63
await vault.clear();
_63
session = null;
_63
emitChange();
_63
}

Here is the session-vault.ts we've created thus far.

src/util/session-vault.ts

_68
import {
_68
BrowserVault,
_68
Vault,
_68
VaultType,
_68
DeviceSecurityType,
_68
} from '@ionic-enterprise/identity-vault';
_68
import { createVault } from './vault-factory';
_68
import { Session } from '../models/Session';
_68
_68
export type UnlockMode =
_68
| 'BiometricsWithPasscode'
_68
| 'InMemory'
_68
| 'SecureStorage';
_68
_68
const vault: Vault | BrowserVault = createVault();
_68
let session: Session | null = null;
_68
let listeners: any[] = [];
_68
_68
export const initializeVault = async (): Promise<void> => {
_68
try {
_68
await vault.initialize({
_68
key: 'io.ionic.gettingstartedivreact',
_68
type: VaultType.SecureStorage,
_68
deviceSecurityType: DeviceSecurityType.None,
_68
});
_68
} catch (e: unknown) {
_68
await vault.clear();
_68
}
_68
_68
await getSession();
_68
};
_68
_68
export const getSnapshot = (): Session | null => {
_68
return session;
_68
}
_68
_68
export const subscribe = (listener: any) => {
_68
listeners = [...listeners, listener];
_68
return () => {
_68
listeners = listeners.filter((l) => l !== listener);
_68
};
_68
};
_68
_68
export const emitChange = () => {
_68
for (let listener of listeners) {
_68
listener();
_68
}
_68
};
_68
_68
export const storeSession = async (newSession: Session): Promise<void> => {
_68
await vault.setValue('session', newSession);
_68
session = newSession;
_68
emitChange();
_68
}
_68
_68
export const getSession = async (): Promise<void> => {
_68
if (session === null) {
_68
if (await vault.isEmpty()) session = null;
_68
else session = await vault.getValue<Session>('session');
_68
}
_68
emitChange();
_68
};
_68
_68
export const clearSession = async (): Promise<void> => {
_68
await vault.clear();
_68
session = null;
_68
emitChange();
_68
}

The UnlockMode specifies the logical combinations of settings we wish to support within our application.

src/util/session-vault.ts

_72
import {
_72
BrowserVault,
_72
Vault,
_72
VaultType,
_72
DeviceSecurityType,
_72
} from '@ionic-enterprise/identity-vault';
_72
import { createVault } from './vault-factory';
_72
import { Session } from '../models/Session';
_72
_72
export type UnlockMode =
_72
| 'BiometricsWithPasscode'
_72
| 'InMemory'
_72
| 'SecureStorage';
_72
_72
const vault: Vault | BrowserVault = createVault();
_72
let session: Session | null = null;
_72
let listeners: any[] = [];
_72
_72
export const initializeVault = async (): Promise<void> => {
_72
try {
_72
await vault.initialize({
_72
key: 'io.ionic.gettingstartedivreact',
_72
type: VaultType.SecureStorage,
_72
deviceSecurityType: DeviceSecurityType.None,
_72
});
_72
} catch (e: unknown) {
_72
await vault.clear();
_72
}
_72
_72
await getSession();
_72
};
_72
_72
export const getSnapshot = (): Session | null => {
_72
return session;
_72
}
_72
_72
export const subscribe = (listener: any) => {
_72
listeners = [...listeners, listener];
_72
return () => {
_72
listeners = listeners.filter((l) => l !== listener);
_72
};
_72
};
_72
_72
export const emitChange = () => {
_72
for (let listener of listeners) {
_72
listener();
_72
}
_72
};
_72
_72
export const storeSession = async (newSession: Session): Promise<void> => {
_72
await vault.setValue('session', newSession);
_72
session = newSession;
_72
emitChange();
_72
}
_72
_72
export const getSession = async (): Promise<void> => {
_72
if (session === null) {
_72
if (await vault.isEmpty()) session = null;
_72
else session = await vault.getValue<Session>('session');
_72
}
_72
emitChange();
_72
};
_72
_72
export const clearSession = async (): Promise<void> => {
_72
await vault.clear();
_72
session = null;
_72
emitChange();
_72
}
_72
_72
export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_72
_72
};

Add an updateUnlockMode() function. Take a single argument for the mode.

src/util/session-vault.ts

_75
import {
_75
BrowserVault,
_75
Vault,
_75
VaultType,
_75
DeviceSecurityType,
_75
IdentityVaultConfig,
_75
} from '@ionic-enterprise/identity-vault';
_75
import { createVault } from './vault-factory';
_75
import { Session } from '../models/Session';
_75
_75
export type UnlockMode =
_75
| 'BiometricsWithPasscode'
_75
| 'InMemory'
_75
| 'SecureStorage';
_75
_75
const vault: Vault | BrowserVault = createVault();
_75
let session: Session | null = null;
_75
let listeners: any[] = [];
_75
_75
export const initializeVault = async (): Promise<void> => {
_75
try {
_75
await vault.initialize({
_75
key: 'io.ionic.gettingstartedivreact',
_75
type: VaultType.SecureStorage,
_75
deviceSecurityType: DeviceSecurityType.None,
_75
});
_75
} catch (e: unknown) {
_75
await vault.clear();
_75
}
_75
_75
await getSession();
_75
};
_75
_75
export const getSnapshot = (): Session | null => {
_75
return session;
_75
}
_75
_75
export const subscribe = (listener: any) => {
_75
listeners = [...listeners, listener];
_75
return () => {
_75
listeners = listeners.filter((l) => l !== listener);
_75
};
_75
};
_75
_75
export const emitChange = () => {
_75
for (let listener of listeners) {
_75
listener();
_75
}
_75
};
_75
_75
export const storeSession = async (newSession: Session): Promise<void> => {
_75
await vault.setValue('session', newSession);
_75
session = newSession;
_75
emitChange();
_75
}
_75
_75
export const getSession = async (): Promise<void> => {
_75
if (session === null) {
_75
if (await vault.isEmpty()) session = null;
_75
else session = await vault.getValue<Session>('session');
_75
}
_75
emitChange();
_75
};
_75
_75
export const clearSession = async (): Promise<void> => {
_75
await vault.clear();
_75
session = null;
_75
emitChange();
_75
}
_75
_75
export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_75
const newConfig = { ...(vault.config as IdentityVaultConfig) };
_75
_75
await vault.updateConfig(newConfig);
_75
};

The vault's updateConfig() method takes a full vault configuration object. We can grab our current config and cast it to IdentityVaultConfig to signify that we know the value is not undefined at this point. The new config will be used in the updateConfig() method.

src/util/session-vault.ts

_90
import {
_90
BrowserVault,
_90
Vault,
_90
VaultType,
_90
DeviceSecurityType,
_90
IdentityVaultConfig,
_90
} from '@ionic-enterprise/identity-vault';
_90
import { createVault } from './vault-factory';
_90
import { Session } from '../models/Session';
_90
_90
export type UnlockMode =
_90
| 'BiometricsWithPasscode'
_90
| 'InMemory'
_90
| 'SecureStorage';
_90
_90
const vault: Vault | BrowserVault = createVault();
_90
let session: Session | null = null;
_90
let listeners: any[] = [];
_90
_90
export const initializeVault = async (): Promise<void> => {
_90
try {
_90
await vault.initialize({
_90
key: 'io.ionic.gettingstartedivreact',
_90
type: VaultType.SecureStorage,
_90
deviceSecurityType: DeviceSecurityType.None,
_90
});
_90
} catch (e: unknown) {
_90
await vault.clear();
_90
}
_90
_90
await getSession();
_90
};
_90
_90
export const getSnapshot = (): Session | null => {
_90
return session;
_90
}
_90
_90
export const subscribe = (listener: any) => {
_90
listeners = [...listeners, listener];
_90
return () => {
_90
listeners = listeners.filter((l) => l !== listener);
_90
};
_90
};
_90
_90
export const emitChange = () => {
_90
for (let listener of listeners) {
_90
listener();
_90
}
_90
};
_90
_90
export const storeSession = async (newSession: Session): Promise<void> => {
_90
await vault.setValue('session', newSession);
_90
session = newSession;
_90
emitChange();
_90
}
_90
_90
export const getSession = async (): Promise<void> => {
_90
if (session === null) {
_90
if (await vault.isEmpty()) session = null;
_90
else session = await vault.getValue<Session>('session');
_90
}
_90
emitChange();
_90
};
_90
_90
export const clearSession = async (): Promise<void> => {
_90
await vault.clear();
_90
session = null;
_90
emitChange();
_90
}
_90
_90
export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_90
const newConfig = { ...(vault.config as IdentityVaultConfig) };
_90
_90
switch (mode) {
_90
case 'BiometricsWithPasscode': {
_90
newConfig.type = VaultType.DeviceSecurity;
_90
break;
_90
}
_90
case 'InMemory': {
_90
newConfig.type = VaultType.InMemory;
_90
break;
_90
}
_90
default: {
_90
newConfig.type = VaultType.SecureStorage;
_90
break;
_90
}
_90
}
_90
_90
await vault.updateConfig(newConfig);
_90
};

Update the type based on the specified mode.

src/util/session-vault.ts

_93
import {
_93
BrowserVault,
_93
Vault,
_93
VaultType,
_93
DeviceSecurityType,
_93
IdentityVaultConfig,
_93
} from '@ionic-enterprise/identity-vault';
_93
import { createVault } from './vault-factory';
_93
import { Session } from '../models/Session';
_93
_93
export type UnlockMode =
_93
| 'BiometricsWithPasscode'
_93
| 'InMemory'
_93
| 'SecureStorage';
_93
_93
const vault: Vault | BrowserVault = createVault();
_93
let session: Session | null = null;
_93
let listeners: any[] = [];
_93
_93
export const initializeVault = async (): Promise<void> => {
_93
try {
_93
await vault.initialize({
_93
key: 'io.ionic.gettingstartedivreact',
_93
type: VaultType.SecureStorage,
_93
deviceSecurityType: DeviceSecurityType.None,
_93
});
_93
} catch (e: unknown) {
_93
await vault.clear();
_93
}
_93
_93
await getSession();
_93
};
_93
_93
export const getSnapshot = (): Session | null => {
_93
return session;
_93
}
_93
_93
export const subscribe = (listener: any) => {
_93
listeners = [...listeners, listener];
_93
return () => {
_93
listeners = listeners.filter((l) => l !== listener);
_93
};
_93
};
_93
_93
export const emitChange = () => {
_93
for (let listener of listeners) {
_93
listener();
_93
}
_93
};
_93
_93
export const storeSession = async (newSession: Session): Promise<void> => {
_93
await vault.setValue('session', newSession);
_93
session = newSession;
_93
emitChange();
_93
}
_93
_93
export const getSession = async (): Promise<void> => {
_93
if (session === null) {
_93
if (await vault.isEmpty()) session = null;
_93
else session = await vault.getValue<Session>('session');
_93
}
_93
emitChange();
_93
};
_93
_93
export const clearSession = async (): Promise<void> => {
_93
await vault.clear();
_93
session = null;
_93
emitChange();
_93
}
_93
_93
export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_93
const newConfig = { ...(vault.config as IdentityVaultConfig) };
_93
_93
switch (mode) {
_93
case 'BiometricsWithPasscode': {
_93
newConfig.type = VaultType.DeviceSecurity;
_93
newConfig.deviceSecurityType = DeviceSecurityType.Both;
_93
break;
_93
}
_93
case 'InMemory': {
_93
newConfig.type = VaultType.InMemory;
_93
newConfig.deviceSecurityType = DeviceSecurityType.None;
_93
break;
_93
}
_93
default: {
_93
newConfig.type = VaultType.SecureStorage;
_93
newConfig.deviceSecurityType = DeviceSecurityType.None;
_93
break;
_93
}
_93
}
_93
_93
await vault.updateConfig(newConfig);
_93
};

Update the deviceSecurityType based on the value of the type.

src/util/session-vault.ts

_94
import {
_94
BrowserVault,
_94
Vault,
_94
VaultType,
_94
DeviceSecurityType,
_94
IdentityVaultConfig,
_94
} from '@ionic-enterprise/identity-vault';
_94
import { createVault } from './vault-factory';
_94
import { Session } from '../models/Session';
_94
_94
export type UnlockMode =
_94
| 'BiometricsWithPasscode'
_94
| 'InMemory'
_94
| 'SecureStorage';
_94
_94
const vault: Vault | BrowserVault = createVault();
_94
let session: Session | null = null;
_94
let listeners: any[] = [];
_94
_94
export const initializeVault = async (): Promise<void> => {
_94
try {
_94
await vault.initialize({
_94
key: 'io.ionic.gettingstartedivreact',
_94
type: VaultType.SecureStorage,
_94
deviceSecurityType: DeviceSecurityType.None,
_94
});
_94
} catch (e: unknown) {
_94
await vault.clear();
_94
await setUnlockMode('SecureStorage');
_94
}
_94
_94
await getSession();
_94
};
_94
_94
export const getSnapshot = (): Session | null => {
_94
return session;
_94
}
_94
_94
export const subscribe = (listener: any) => {
_94
listeners = [...listeners, listener];
_94
return () => {
_94
listeners = listeners.filter((l) => l !== listener);
_94
};
_94
};
_94
_94
export const emitChange = () => {
_94
for (let listener of listeners) {
_94
listener();
_94
}
_94
};
_94
_94
export const storeSession = async (newSession: Session): Promise<void> => {
_94
await vault.setValue('session', newSession);
_94
session = newSession;
_94
emitChange();
_94
}
_94
_94
export const getSession = async (): Promise<void> => {
_94
if (session === null) {
_94
if (await vault.isEmpty()) session = null;
_94
else session = await vault.getValue<Session>('session');
_94
}
_94
emitChange();
_94
};
_94
_94
export const clearSession = async (): Promise<void> => {
_94
await vault.clear();
_94
session = null;
_94
emitChange();
_94
}
_94
_94
export const updateUnlockMode = async (mode: UnlockMode): Promise<void> => {
_94
const newConfig = { ...(vault.config as IdentityVaultConfig) };
_94
_94
switch (mode) {
_94
case 'BiometricsWithPasscode': {
_94
newConfig.type = VaultType.DeviceSecurity;
_94
newConfig.deviceSecurityType = DeviceSecurityType.Both;
_94
break;
_94
}
_94
case 'InMemory': {
_94
newConfig.type = VaultType.InMemory;
_94
newConfig.deviceSecurityType = DeviceSecurityType.None;
_94
break;
_94
}
_94
default: {
_94
newConfig.type = VaultType.SecureStorage;
_94
newConfig.deviceSecurityType = DeviceSecurityType.None;
_94
break;
_94
}
_94
}
_94
_94
await vault.updateConfig(newConfig);
_94
};

If the vault fails to initialize reset it to be a SecureStorage vault since it does not rely on the device level security settings. You could also use InMemory if desired.

Here is the session-vault.ts we've created thus far.

The UnlockMode specifies the logical combinations of settings we wish to support within our application.

Add an updateUnlockMode() function. Take a single argument for the mode.

The vault's updateConfig() method takes a full vault configuration object. We can grab our current config and cast it to IdentityVaultConfig to signify that we know the value is not undefined at this point. The new config will be used in the updateConfig() method.

Update the type based on the specified mode.

Update the deviceSecurityType based on the value of the type.

If the vault fails to initialize reset it to be a SecureStorage vault since it does not rely on the device level security settings. You could also use InMemory if desired.

src/util/session-vault.ts

_63
import {
_63
BrowserVault,
_63
Vault,
_63
VaultType,
_63
DeviceSecurityType,
_63
} from '@ionic-enterprise/identity-vault';
_63
import { createVault } from './vault-factory';
_63
import { Session } from '../models/Session';
_63
_63
const vault: Vault | BrowserVault = createVault();
_63
let session: Session | null = null;
_63
let listeners: any[] = [];
_63
_63
export const initializeVault = async (): Promise<void> => {
_63
try {
_63
await vault.initialize({
_63
key: 'io.ionic.gettingstartedivreact',
_63
type: VaultType.SecureStorage,
_63
deviceSecurityType: DeviceSecurityType.None,
_63
});
_63
} catch (e: unknown) {
_63
await vault.clear();
_63
}
_63
_63
await getSession();
_63
};
_63
_63
export const getSnapshot = (): Session | null => {
_63
return session;
_63
}
_63
_63
export const subscribe = (listener: any) => {
_63
listeners = [...listeners, listener];
_63
return () => {
_63
listeners = listeners.filter((l) => l !== listener);
_63
};
_63
};
_63
_63
export const emitChange = () => {
_63
for (let listener of listeners) {
_63
listener();
_63
}
_63
};
_63
_63
export const storeSession = async (newSession: Session): Promise<void> => {
_63
await vault.setValue('session', newSession);
_63
session = newSession;
_63
emitChange();
_63
}
_63
_63
export const getSession = async (): Promise<void> => {
_63
if (session === null) {
_63
if (await vault.isEmpty()) session = null;
_63
else session = await vault.getValue<Session>('session');
_63
}
_63
emitChange();
_63
};
_63
_63
export const clearSession = async (): Promise<void> => {
_63
await vault.clear();
_63
session = null;
_63
emitChange();
_63
}

Why the UnlockMode?

One natural question from above may be "why create an UnlockMode type when you can pass in the VaultType and figure things out from there?" The answer to that is that any time you incorporate a third-party library into your code like this, you should create an "adapter" service that utilizes the library within the domain of your application.

This has two major benefits:

  1. It insulates the rest of the application from change. If the next major version of Identity Vault has breaking changes that need to be addressed, the only place in the code they need to be addressed is in this service. The rest of the code continues to interact with the vault via the interface defined by the service.
  2. It reduces vendor tie-in, making it easier to swap to different libraries in the future if need be.

The ultimate goal is for the only modules in the application directly import from @ionic-enterprise/identity-vault to be services like this one that encapsulate operations on a vault.

Setting the deviceSecurityType Value

The deviceSecurityType property only applies when the type is set to DeviceSecurity. We could use any of the following DeviceSecurityType values:

  • Biometrics: Use the system's default biometric option to unlock the vault.
  • SystemPasscode: Use the system's designated system passcode (PIN, Pattern, etc.) to unlock the vault.
  • Both: Primarily use the biometric hardware to unlock the vault, but use the system passcode as a backup for cases where the biometric hardware is not configured or biometric authentication has failed.

For our application, we will just keep it simple and use Both when using DeviceSecurity vault. This is a very versatile option and makes the most sense for most applications.

With vault types other than DeviceSecurity, always use DeviceSecurityType.None.

Update the Tab1 Page

We can now add some buttons to the Tab1Page in order to try out the different vault types. Update the src/pages/Tab1.tsx as shown below.

src/pages/Tab1.tsx

_114
import {
_114
IonButton,
_114
IonContent,
_114
IonHeader,
_114
IonItem,
_114
IonLabel,
_114
IonList,
_114
IonPage,
_114
IonTitle,
_114
IonToolbar,
_114
} from '@ionic/react';
_114
import { useSyncExternalStore } from 'react';
_114
import './Tab1.css';
_114
import {
_114
storeSession,
_114
subscribe,
_114
getSnapshot,
_114
clearSession,
_114
updateUnlockMode,
_114
} from '../util/session-vault';
_114
_114
const Tab1: React.FC = () => {
_114
const session = useSyncExternalStore(subscribe, getSnapshot);
_114
_114
const storeClicked = async (): Promise<void> => {
_114
await storeSession({
_114
email: 'test@ionic.io',
_114
firstName: 'Tessa',
_114
lastName: 'Testsmith',
_114
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_114
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_114
});
_114
};
_114
_114
return (
_114
<IonPage>
_114
<IonHeader>
_114
<IonToolbar>
_114
<IonTitle>Tab 1</IonTitle>
_114
</IonToolbar>
_114
</IonHeader>
_114
<IonContent fullscreen>
_114
<IonHeader collapse="condense">
_114
<IonToolbar>
_114
<IonTitle size="large">Tab 1</IonTitle>
_114
</IonToolbar>
_114
</IonHeader>
_114
_114
<IonList>
_114
<IonItem>
_114
<IonLabel>
_114
<IonButton expand="block" onClick={storeClicked}>Store</IonButton>
_114
</IonLabel>
_114
</IonItem>
_114
_114
<IonItem>
_114
<IonLabel>
_114
<IonButton
_114
expand="block"
_114
color="secondary"
_114
onClick={() => updateUnlockMode('BiometricsWithPasscode')}
_114
>
_114
Use Biometrics
_114
</IonButton>
_114
</IonLabel>
_114
</IonItem>
_114
_114
<IonItem>
_114
<IonLabel>
_114
<IonButton
_114
expand="block"
_114
color="secondary"
_114
onClick={() => updateUnlockMode('InMemory')}
_114
>
_114
Use In Memory
_114
</IonButton>
_114
</IonLabel>
_114
</IonItem>
_114
_114
<IonItem>

Build the application and run it on a device upon which you have biometrics enabled. Perform the following steps for each type of vault:

  1. Press the "Store" button to put data in the vault.
  2. Choose a vault type via one of the "Use" buttons.
  3. Close the application (do not just put it in the background, but close it).
  4. Restart the application.

You should see the following results:

  • "Use Biometrics": On an iPhone with FaceID, this will fail. We will fix that next. On all other devices, however, a biometric prompt will be displayed to unlock the vault. The data will be displayed once the vault is unlocked.
  • "Use In Memory": The data is no longer set. As the name implies, there is no persistence of this data.
  • "Use Secure Storage": The stored data is displayed without unlocking.

Native Configuration

If you tried the tests above on an iPhone with Face ID, your app should have crashed upon restarting when using a biometric vault. If you run npx cap sync you will see what is missing.


_10
[warn] Configuration required for @ionic-enterprise/identity-vault.
_10
Add the following to Info.plist:
_10
<key>NSFaceIDUsageDescription</key>
_10
<string>Use Face ID to authenticate yourself and login</string>

Open the ios/App/App/Info.plist file and add the specified configuration. The actual string value can be anything you want, but the key must be NSFaceIDUsageDescription.

ios/App/App/Info.plist

_51
<?xml version="1.0" encoding="UTF-8"?>
_51
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
_51
<plist version="1.0">
_51
<dict>
_51
<key>CFBundleDevelopmentRegion</key>
_51
<string>Use Face ID to authenticate yourself and login</string>

Biometrics should work on the iPhone at this point.

Locking and Unlocking the Vault

Going forward we will begin exploring functionality that only works when the application is run on a device. As such, you should begin testing on a device instead of using the development server.

Right now, the only way to "lock" the vault is to close the application. In this section we will look at a couple of other ways to lock the vault as well as ways to unlock it.

Manually Locking the Vault

In src/util/session-vault.ts, wrap the vault's lock() method so we can use it in our Tab1Page.

src/util/session-vault.ts

_100
import {
_100
BrowserVault,
_100
Vault,
_100
VaultType,
_100
DeviceSecurityType,
_100
IdentityVaultConfig,
_100
} from '@ionic-enterprise/identity-vault';
_100
import { createVault } from './vault-factory';
_100
import { Session } from '../models/Session';
_100
_100
export type UnlockMode =
_100
| 'BiometricsWithPasscode'
_100
| 'InMemory'
_100
| 'SecureStorage';
_100
_100
const vault: Vault | BrowserVault = createVault();
_100
let session: Session | null = null;
_100
let listeners: any[] = [];
_100
_100
export const lockSession = async (): Promise<void> => {

Add a lock button in src/pages/Tab1.tsx.

src/pages/Tab1.tsx

_123
import {
_123
IonButton,
_123
IonContent,
_123
IonHeader,
_123
IonItem,
_123
IonLabel,
_123
IonList,
_123
IonPage,
_123
IonTitle,
_123
IonToolbar,
_123
} from '@ionic/react';
_123
import { useSyncExternalStore } from 'react';
_123
import './Tab1.css';
_123
import {
_123
storeSession,
_123
subscribe,
_123
getSnapshot,
_123
clearSession,
_123
updateUnlockMode,
_123
lockSession,
_123
} from '../util/session-vault';
_123
_123
const Tab1: React.FC = () => {
_123
const session = useSyncExternalStore(subscribe, getSnapshot);
_123
_123
const storeClicked = async (): Promise<void> => {
_123
await storeSession({
_123
email: 'test@ionic.io',
_123
firstName: 'Tessa',
_123
lastName: 'Testsmith',
_123
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_123
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_123
});
_123
};
_123
_123
return (
_123
<IonPage>
_123
<IonHeader>
_123
<IonToolbar>
_123
<IonTitle>Tab 1</IonTitle>
_123
</IonToolbar>
_123
</IonHeader>
_123
<IonContent fullscreen>
_123
<IonHeader collapse="condense">
_123
<IonToolbar>
_123
<IonTitle size="large">Tab 1</IonTitle>
_123
</IonToolbar>
_123
</IonHeader>
_123
_123
<IonButton expand="block" color="warning" onClick={lockSession}>

When we press the "Lock" button, the session data is no longer displayed. The actual status of the vault depends on the last "unlock mode" button pressed prior to locking the vault.

  • "Use Biometrics": The vault has been locked and the session data will not be accessible until it is unlocked.
  • "Use In Memory": The session data no longer exists.
  • "Use Secure Storage": The session data is in the vault, but is not locked.

Unlocking the Vault

To verify the behaviors noted above, you need to be able to unlock the vault. To do this you can use the vault's unlock() method or you can perform an operation that requires the vault to be unlocked. When we unlock the vault, we need to restore the session data in our page, so we can just use our getSession() function. When it calls the vault's getValue(), the getValue() will attempt to unlock the vault.

Add the following code to src/pages/Tab1.tsx:

src/pages/Tab1.tsx

_132
import {
_132
IonButton,
_132
IonContent,
_132
IonHeader,
_132
IonItem,
_132
IonLabel,
_132
IonList,
_132
IonPage,
_132
IonTitle,
_132
IonToolbar,
_132
} from '@ionic/react';
_132
import { useSyncExternalStore } from 'react';
_132
import './Tab1.css';
_132
import {
_132
storeSession,
_132
subscribe,
_132
getSnapshot,
_132
clearSession,
_132
updateUnlockMode,
_132
lockSession,
_132
getSession,
_132
} from '../util/session-vault';
_132
_132
const Tab1: React.FC = () => {
_132
const session = useSyncExternalStore(subscribe, getSnapshot);
_132
_132
const storeClicked = async (): Promise<void> => {
_132
await storeSession({
_132
email: 'test@ionic.io',
_132
firstName: 'Tessa',
_132
lastName: 'Testsmith',
_132
accessToken: '4abf1d79-143c-4b89-b478-19607eb5ce97',
_132
refreshToken: '565111b6-66c3-4527-9238-6ea2cc017126',
_132
});
_132
};
_132
_132
return (
_132
<IonPage>
_132
<IonHeader>
_132
<IonToolbar>
_132
<IonTitle>Tab 1</IonTitle>
_132
</IonToolbar>
_132
</IonHeader>
_132
<IonContent fullscreen>
_132
<IonButton expand="block" color="warning" onClick={getSession}>

We can now use the "Lock" and "Unlock" buttons to verify the behavior of each of our unlock modes.

Locking in the Background

We can manually lock our vault, but it would be nice if the vault locked for us automatically. This can be accomplished by setting lockAfterBackgrounded which will lock the vault when the application is resumed, if the app was backgrounded for the configured amount of time. We can configure this by doing two actions when initializing the vault:

  • Set the lockAfterBackgrounded value in the config. This value is specified in milliseconds.
  • Set the onLock callback so the session is cleared on lock.
src/util/session-vault.ts

_106
import {
_106
BrowserVault,
_106
Vault,
_106
VaultType,
_106
DeviceSecurityType,
_106
IdentityVaultConfig,
_106
} from '@ionic-enterprise/identity-vault';
_106
import { createVault } from './vault-factory';
_106
import { Session } from '../models/Session';
_106
_106
export type UnlockMode =
_106
| 'BiometricsWithPasscode'
_106
| 'InMemory'
_106
| 'SecureStorage';
_106
_106
const vault: Vault | BrowserVault = createVault();
_106
let session: Session | null = null;
_106
let listeners: any[] = [];
_106
_106
lockAfterBackgrounded: 2000,

Architectural Considerations

Construction vs. Initialization

Have a look at the src/util/session-vault.ts file. Notice that it is very intentional about separating construction and initialization. This is very important.

Identity Vault allows you to pass the configuration object via the new Vault(cfg) constructor. This, however, will make asynchronous calls which makes construction indeterminate.

Always use a pattern of:

  • Construct the vault via new Vault() (default constructor, no configuration).
  • Pass the configuration to the vault.initialize(cfg) function.
  • Perform the initialization itself prior to mounting the application and make sure that the code is properly awaiting its completion.

Control Unlocking on Startup and Navigation

Our code is currently automatically unlocking the vault upon startup due to our getSession function being invoked as part of our initialization logic. This is OK for our app, but it could be a problem if we had situations where multiple calls to get data from a locked vault all happened simultaneously. Always make sure you are controlling the vault lock status in such situations to ensure that only one unlock attempt is being made at a time.

We will see various strategies for this in later tutorials. You can also refer to our troubleshooting guide for further guidance.

Initial Vault Type Configuration

When we first initialize the vault we use the following configuration:


_10
await vault.initialize({
_10
key: 'io.ionic.gettingstartedivreact',
_10
type: VaultType.SecureStorage,
_10
deviceSecurityType: DeviceSecurityType.None,
_10
lockAfterBackgrounded: 2000,
_10
});

It is important to note that this is an initial configuration. Once a vault is created, it (and its current configuration) persist between invocations of the application. Thus, if the configuration of the vault is updated by the application, the updated configuration will be read when the application is reopened. For example, if the lockAfterBackgrounded has been updated to 5000 milliseconds, then when we start the application again with the vault already existing, lockAfterBackgrounded will remain set to 5000 milliseconds. The configuration we pass here is only used if we later destroy and re-create this vault.

Notice that we are specifying a type of VaultType.SecureStorage. It is best to use either VaultType.SecureStorage or VaultType.InMemeory when calling initialize() to avoid the potential of creating a vault of a type that cannot be supported. We can always update the type later after and the updated type will "stick." We want to start, however, with an option that will always word regardless of the device's configuration.

Single Vault vs Multiple Vaults

Identity Vault is ideal for storing small chunks of data such as authentication information or encryption keys. Our sample application contains a single vault. However, it may make sense to use multiple vaults within your application's architecture.

Ask yourself the following questions:

  1. What type of data is stored?
  2. Under what conditions should the data be available to the application?

Let's say the application is storing the following information:

  • The authentication session data.
  • A set of encryption keys.

You can use a single vault to store this data if all of the following are true:

  • You only want to access the vault via a single service.
  • The requirements for when the data is accessible is identical.

You should use multiple vaults to store this data if any of the following are true:

  • You logically want to use different services for different types of data.
  • You logically would like to use different services to access different types of data.
  • The requirements for when the data is accessible differs in some way. For example, perhaps the authentication information is locked behind a biometric key while access to the encryption keys requires a custom set in-app code to be entered.

If you decide to use multiple vaults, a best-practice is to create a separate service for each vault. That is, in the interest of proper organization within your code, each vault service should only manage a single vault.