Skip to main content
Version: 5.0

Using the Device API

Overview

When we created our other Identity Vault tutorial applications we implemented several pieces of functionality without taking the capabilities and current configuration of the device into account. We will address that now with the help of the Device API.

In this tutorial we will:

  • Examine the methods available in the Device API.
  • Disable options based on the current device capabilities and configurations.
  • Show the privacy screen when the application is in the task switcher.
  • Progressively enhance the vault type when the user logs in.
  • Handle the iOS Face ID permissions before setting the vault to use biometric unlock.

Let's Code

This tutorial builds upon the application created when doing the startup strategies tutorial. If you have the code from when you performed that tutorial, then you are ready to go. If you need the code you can make a copy from our GitHub repository.

The Device API

The functions in the Device API can be separated into three different categories:

  • Capabilities: The values returned by these functions are defined by the capabilities of the device.
  • Configuration and Status: The values returned by these functions are determined by the device's current configuration and/or status.
  • Procedures: These functions perform some kind of device related process such as setting a configuration value or displaying a prompt.

We will use the Tab2Page to demonstrate how these functions work.

Capabilities

The "capabilities" functions return information about the capabilities of the current device. The values returned by these functions are constant for a given device.

  • getAvailableHardware: Resolves an array of the types of biometric hardware that a device supports.
  • hasSecureHardware: Resolves true if the device has hardware dedicated to storing secure data. Most modern devices do.
  • isBiometricsSupported: Resolves true if the device supports some form of biometrics regardless of whether or not biometrics is configured. Most modern devices support biometrics.

Update the Tab2Page to display the results of these methods.

src/app/tab2/tab2.page.ts
src/app/tab2/tab2.page.html

_46
import { CommonModule } from '@angular/common';
_46
import { Component, OnInit } from '@angular/core';
_46
import { Device } from '@ionic-enterprise/identity-vault';
_46
import {
_46
IonContent,
_46
IonHeader,
_46
IonItem,
_46
IonLabel,
_46
IonList,
_46
IonListHeader,
_46
IonNote,
_46
IonTitle,
_46
IonToolbar,
_46
} from '@ionic/angular/standalone';
_46
_46
@Component({
_46
selector: 'app-tab2',
_46
templateUrl: 'tab2.page.html',
_46
styleUrls: ['tab2.page.scss'],
_46
standalone: true,
_46
imports: [
_46
CommonModule,
_46
IonContent,
_46
IonHeader,
_46
IonItem,
_46
IonLabel,
_46
IonList,
_46
IonListHeader,
_46
IonNote,
_46
IonTitle,
_46
IonToolbar,
_46
],
_46
})
_46
export class Tab2Page implements OnInit {
_46
hasSecureHardware: Boolean = false;
_46
isBiometricsSupported: Boolean = false;
_46
availableHardware: Array<string> = [];
_46
_46
constructor() {}
_46
_46
async ngOnInit(): Promise<void> {
_46
this.hasSecureHardware = await Device.hasSecureHardware();
_46
this.isBiometricsSupported = await Device.isBiometricsSupported();
_46
this.availableHardware = await Device.getAvailableHardware();
_46
}
_46
}

Note that these value are consistent on a particular device, regardless of whether or not biometrics is currently configured on the device.

warning
Results may vary on a device where the user does not have a system passcode set.

Configuration and Status

  • getBiometricStrengthLevel: On iOS, this function always resolves to strong. On Android, the result depends on the Biometrics Classes of whichever biometrics are currently configured on the device. If at least one of the configured biometric options is Class 3, it will return strong, otherwise it will return weak.

  • isBiometricsAllowed: On iOS Face ID based devices this method resolves to one of the following:

    • Prompt: The user needs to be asked for permission to use Face ID.
    • Denied: The user has declined permission.
    • Granted: The user has granted permission.

    On all other devices, which do not require such permissions, this method always resolves to Granted.

  • isBiometricsEnabled: Resolves true if biometrics have been configured for the current user, false otherwise.

  • isHideScreenOnBackgroundEnabled: Resolves true if the "hide screen" will be displayed when the app is placed in the background, false otherwise.

  • isLockedOutOfBiometrics: Resolves true if the user is locked out of biometrics after too many failed attempts. On iOS, this information may be known upon app launch. On Android, an attempt to unlock with the current app session needs to be performed before this can be known.

  • isSystemPasscodeSet: Resolves true if the user has established a system passcode that can be used to unlock the app, false otherwise.

Update the Tab2Page to display the results of these methods.

src/app/tab2/tab2.page.ts
src/app/tab2/tab2.page.html

_60
import { CommonModule } from '@angular/common';
_60
import { Component, OnInit } from '@angular/core';
_60
import { Device } from '@ionic-enterprise/identity-vault';
_60
import {
_60
IonContent,
_60
IonHeader,
_60
IonItem,
_60
IonLabel,
_60
IonList,
_60
IonListHeader,
_60
IonNote,
_60
IonTitle,
_60
IonToolbar,
_60
} from '@ionic/angular/standalone';
_60
_60
@Component({
_60
selector: 'app-tab2',
_60
templateUrl: 'tab2.page.html',
_60
styleUrls: ['tab2.page.scss'],
_60
standalone: true,
_60
imports: [
_60
CommonModule,
_60
IonContent,
_60
IonHeader,
_60
IonItem,
_60
IonLabel,
_60
IonList,
_60
IonListHeader,
_60
IonNote,
_60
this.isHideScreenOnBackgroundEnabled = await Device.isHideScreenOnBackgroundEnabled();

Build the application and install it on a variety of devices. Then modify the configuration of the biometrics on those devices to see how the values change.

Procedures

  • setHideScreenOnBackground: Set whether or not the interface is obscured when the application is placed in the background.
  • showBiometricPrompt: Show a biometric prompt to the user. This method will resolve when the user dismisses the prompt by successfully providing biometrics, and will reject if they cancel. On iOS devices with Face ID, this method will also ask for permission as needed.

Add a couple of buttons to the Tab2Page to call these methods.

src/app/tab2/tab2.page.ts
src/app/tab2/tab2.page.html

_78
import { CommonModule } from '@angular/common';
_78
import { Component, OnInit } from '@angular/core';
_78
import { Device } from '@ionic-enterprise/identity-vault';
_78
import {
_78
IonButton,
_78
IonContent,
_78
IonHeader,
_78
IonItem,
_78
IonLabel,
_78
IonList,
_78
IonListHeader,
_78
IonNote,
_78
IonTitle,
_78
IonToolbar,
_78
} from '@ionic/angular/standalone';
_78
_78
@Component({
_78
selector: 'app-tab2',
_78
this.isHideScreenOnBackgroundEnabled = await Device.isHideScreenOnBackgroundEnabled();

Build the code and install it on a variety of different types of devices to see how the procedures behave.

note

Be sure to include the import of IonButton which is not shown above for reasons of brevity.

Common Uses of the Device API

Now that we have seen an overview of the Device API let's have a look at some common tasks that we can perform using the methods from this API. In this section, we look at how to perform the following tasks:

  • Enabling or disabling various functionality based on the capabilities of the device.
  • Setting the app to show the "privacy screen" when the app is being shown in the app switcher.
  • Progressively enhance the vault to use the most secure options available on the device.
  • Managing permissions on iOS devices that use Face ID.

Enable / Disable Functionality

We can use various methods within the Device API to enable or disable the setting of various preferences, settings, or workflows based on whether or not the current device has biometrics enabled or not. For example, if the application has a preference to toggle biometric authentication on and off, we could hide or disable the toggle for that setting based on the value of isBiometricsEnabled().

We have have an example of doing this within the Tab2Page where the button that initiates the "Show Biometric Prompt" workflow is disabled if biometrics is not currently enabled.

src/app/tab2/tab2.page.html

_10
<ion-item>
_10
<ion-label>
_10
<ion-button expand="block" [disabled]="!isBiometricsEnabled" (click)="showBiometricPrompt()"
_10
>Show Biometric Prompt</ion-button
_10
>
_10
</ion-label>
_10
</ion-item>

Using this code as a model, add similar functionality to the "Use Biometrics" button on the Tab1Page.

Show the Privacy Screen

Many applications that use Identity Vault also display sensitive data that the user may not want shown if the application is currently displayed in the app switcher. Identity Vault has a "privacy screen" feature that will obscure the page contents in this situation. On Android, a gray or black page is shown instead. On iOS, the splash screen is displayed.

In the Tab2Page we show how to use Device.setHideScreenOnBackground() to toggle this feature on and off. Most applications will either want this feature on or off without a toggle. In such cases, it is best to set the feature however you wish upon application startup.

src/main.ts

_29
import { APP_INITIALIZER, enableProdMode } from '@angular/core';
_29
import { bootstrapApplication } from '@angular/platform-browser';
_29
import { RouteReuseStrategy, provideRouter } from '@angular/router';
_29
import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular/standalone';
_29
import { AppComponent } from './app/app.component';
_29
import { routes } from './app/app.routes';
_29
import { SessionVaultService } from './app/core/session-vault.service';
_29
import { environment } from './environments/environment';
_29
import { Device } from '@ionic-enterprise/identity-vault';
_29
_29
if (environment.production) {
_29
enableProdMode();
_29
}
_29
_29
const appInitFactory =
_29
(vault: SessionVaultService): (() => Promise<void>) =>
_29
async () => {
_29
await vault.initialize();

note

You may be tempted to include the Device.setHideScreenOnBackground(true) call in the vault.initialize() method. However, it is a best-practice to keep the code separate from the vault code to avoid confusion in cases where the application needs to use multiple vaults.

Progressively Enhance the Vault

It is a best practice to initially create the vault with a configuration that all devices can support and then enhance the vault's security either based on user preferences or the capabilities of the device. In this section, we will examine how to automatically enhance the security based on the capabilities of the device.

For our application, we would like to use biometrics with a system passcode backup if possible. Otherwise, we will force the user to log in each time.

  • If the vault is already storing a session, leave it alone.
  • If the vault is not storing a session:
    • Use biometrics with a system passcode backup if it is set up.
    • Use a system passcode if biometrics is not set up.
    • If neither biometrics nor a system passcode are set, force the user to login in each time.
src/app/core/session-vault.service.ts

_94
import { Injectable } from '@angular/core';
_94
import {
_94
BrowserVault,
_94
DeviceSecurityType,
_94
IdentityVaultConfig,
_94
Vault,
_94
VaultType,
_94
} from '@ionic-enterprise/identity-vault';
_94
import { Session } from '../models/session';
_94
import { VaultFactory } from './vault.factory';
_94
import { Observable, Subject } from 'rxjs';
_94
_94
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_94
_94
@Injectable({
_94
providedIn: 'root',
_94
})
_94
export class SessionVaultService {
_94
private lockedSubject: Subject<boolean>;
_94
private vault: BrowserVault | Vault;
_94
_94
constructor() {
_94
this.vault = VaultFactory.create();
_94
this.lockedSubject = new Subject<boolean>();
_94
}
_94
_94
get locked$(): Observable<boolean> {
_94
return this.lockedSubject.asObservable();
_94
}
_94
_94
async initialize(): Promise<void> {
_94
try {
_94
await this.vault.initialize({
_94
key: 'io.ionic.gettingstartediv',
_94
type: VaultType.SecureStorage,
_94
deviceSecurityType: DeviceSecurityType.None,
_94
lockAfterBackgrounded: 2000,
_94
});
_94
} catch (e: unknown) {
_94
await this.vault.clear();
_94
await this.updateUnlockMode('SecureStorage');
_94
}
_94
_94
this.vault.onLock(() => this.lockedSubject.next(true));
_94
this.vault.onUnlock(() => this.lockedSubject.next(false));
_94
}
_94
_94
async storeSession(session: Session): Promise<void> {
_94
this.vault.setValue('session', session);
_94
}
_94
_94
async getSession(): Promise<Session | null> {
_94
if (await this.vault.isEmpty()) {
_94
return null;
_94
}
_94
return this.vault.getValue<Session>('session');
_94
}
_94
_94
async clearSession(): Promise<void> {
_94
await this.vault.clear();
_94
}
_94
_94
async lock(): Promise<void> {
_94
await this.vault.lock();
_94
}
_94
_94
async unlock(): Promise<void> {
_94
await this.vault.unlock();
_94
}
_94
_94
async isLocked(): Promise<boolean> {
_94
return (
_94
this.vault.config?.type !== VaultType.SecureStorage &&
_94
this.vault.config?.type !== VaultType.InMemory &&
_94
!(await this.vault.isEmpty()) &&
_94
(await this.vault.isLocked())
_94
);
_94
}
_94
_94
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_94
const type =
_94
mode === 'BiometricsWithPasscode'
_94
? VaultType.DeviceSecurity
_94
: mode === 'InMemory'
_94
? VaultType.InMemory
_94
: VaultType.SecureStorage;
_94
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_94
await this.vault.updateConfig({
_94
...(this.vault.config as IdentityVaultConfig),
_94
type,
_94
deviceSecurityType,
_94
});
_94
}
_94
}

Our app is written to start with a Secure Storage type vault.

src/app/core/session-vault.service.ts

_95
import { Injectable } from '@angular/core';
_95
import {
_95
BrowserVault,
_95
Device,
_95
DeviceSecurityType,
_95
IdentityVaultConfig,
_95
Vault,
_95
VaultType,
_95
} from '@ionic-enterprise/identity-vault';
_95
import { Session } from '../models/session';
_95
import { VaultFactory } from './vault.factory';
_95
import { Observable, Subject } from 'rxjs';
_95
_95
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_95
_95
@Injectable({
_95
providedIn: 'root',
_95
})
_95
export class SessionVaultService {
_95
private lockedSubject: Subject<boolean>;
_95
private vault: BrowserVault | Vault;
_95
_95
constructor() {
_95
this.vault = VaultFactory.create();
_95
this.lockedSubject = new Subject<boolean>();
_95
}
_95
_95
get locked$(): Observable<boolean> {
_95
return this.lockedSubject.asObservable();
_95
}
_95
_95
async initialize(): Promise<void> {
_95
try {
_95
await this.vault.initialize({
_95
key: 'io.ionic.gettingstartediv',
_95
type: VaultType.InMemory,
_95
deviceSecurityType: DeviceSecurityType.None,
_95
lockAfterBackgrounded: 30000,
_95
});
_95
} catch (e: unknown) {
_95
await this.vault.clear();
_95
await this.updateUnlockMode('InMemory');
_95
}
_95
_95
this.vault.onLock(() => this.lockedSubject.next(true));
_95
this.vault.onUnlock(() => this.lockedSubject.next(false));
_95
}
_95
_95
async storeSession(session: Session): Promise<void> {
_95
this.vault.setValue('session', session);
_95
}
_95
_95
async getSession(): Promise<Session | null> {
_95
if (await this.vault.isEmpty()) {
_95
return null;
_95
}
_95
return this.vault.getValue<Session>('session');
_95
}
_95
_95
async clearSession(): Promise<void> {
_95
await this.vault.clear();
_95
}
_95
_95
async lock(): Promise<void> {
_95
await this.vault.lock();
_95
}
_95
_95
async unlock(): Promise<void> {
_95
await this.vault.unlock();
_95
}
_95
_95
async isLocked(): Promise<boolean> {
_95
return (
_95
this.vault.config?.type !== VaultType.SecureStorage &&
_95
this.vault.config?.type !== VaultType.InMemory &&
_95
!(await this.vault.isEmpty()) &&
_95
(await this.vault.isLocked())
_95
);
_95
}
_95
_95
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_95
const type =
_95
mode === 'BiometricsWithPasscode'
_95
? VaultType.DeviceSecurity
_95
: mode === 'InMemory'
_95
? VaultType.InMemory
_95
: VaultType.SecureStorage;
_95
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_95
await this.vault.updateConfig({
_95
...(this.vault.config as IdentityVaultConfig),
_95
type,
_95
deviceSecurityType,
_95
});
_95
}
_95
}

Import the Device class so we can use the API.

Change the type to be InMemory and increase the background lock time from 2 seconds to 30 seconds.

src/app/core/session-vault.service.ts

_101
import { Injectable } from '@angular/core';
_101
import {
_101
BrowserVault,
_101
Device,
_101
DeviceSecurityType,
_101
IdentityVaultConfig,
_101
Vault,
_101
VaultType,
_101
} from '@ionic-enterprise/identity-vault';
_101
import { Session } from '../models/session';
_101
import { VaultFactory } from './vault.factory';
_101
import { Observable, Subject } from 'rxjs';
_101
_101
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_101
_101
@Injectable({
_101
providedIn: 'root',
_101
})
_101
export class SessionVaultService {
_101
private lockedSubject: Subject<boolean>;
_101
private vault: BrowserVault | Vault;
_101
_101
constructor() {
_101
this.vault = VaultFactory.create();
_101
this.lockedSubject = new Subject<boolean>();
_101
}
_101
_101
get locked$(): Observable<boolean> {
_101
return this.lockedSubject.asObservable();
_101
}
_101
_101
async initialize(): Promise<void> {
_101
try {
_101
await this.vault.initialize({
_101
key: 'io.ionic.gettingstartediv',
_101
type: VaultType.InMemory,
_101
deviceSecurityType: DeviceSecurityType.None,
_101
lockAfterBackgrounded: 30000,
_101
});
_101
} catch (e: unknown) {
_101
await this.vault.clear();
_101
await this.updateUnlockMode('InMemory');
_101
}
_101
_101
this.vault.onLock(() => this.lockedSubject.next(true));
_101
this.vault.onUnlock(() => this.lockedSubject.next(false));
_101
}
_101
_101
async storeSession(session: Session): Promise<void> {
_101
this.vault.setValue('session', session);
_101
}
_101
_101
async getSession(): Promise<Session | null> {
_101
if (await this.vault.isEmpty()) {
_101
return null;
_101
}
_101
return this.vault.getValue<Session>('session');
_101
}
_101
_101
async clearSession(): Promise<void> {
_101
await this.vault.clear();
_101
}
_101
_101
async lock(): Promise<void> {
_101
await this.vault.lock();
_101
}
_101
_101
async unlock(): Promise<void> {
_101
await this.vault.unlock();
_101
}
_101
_101
async isLocked(): Promise<boolean> {
_101
return (
_101
this.vault.config?.type !== VaultType.SecureStorage &&
_101
this.vault.config?.type !== VaultType.InMemory &&
_101
!(await this.vault.isEmpty()) &&
_101
(await this.vault.isLocked())
_101
);
_101
}
_101
_101
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_101
const type =
_101
mode === 'BiometricsWithPasscode'
_101
? VaultType.DeviceSecurity
_101
: mode === 'InMemory'
_101
? VaultType.InMemory
_101
: VaultType.SecureStorage;
_101
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_101
await this.vault.updateConfig({
_101
...(this.vault.config as IdentityVaultConfig),
_101
type,
_101
deviceSecurityType,
_101
});
_101
}
_101
_101
private async enhanceVault(): Promise<void> {
_101
if (!(await this.vault.isEmpty())) {
_101
return;
_101
}
_101
}
_101
}

Add a private method called enhanceVault(). Per our first requirement, the vault should not be changed if it is already in use.

src/app/core/session-vault.service.ts

_109
import { Injectable } from '@angular/core';
_109
import {
_109
BrowserVault,
_109
Device,
_109
DeviceSecurityType,
_109
IdentityVaultConfig,
_109
Vault,
_109
VaultType,
_109
} from '@ionic-enterprise/identity-vault';
_109
import { Session } from '../models/session';
_109
import { VaultFactory } from './vault.factory';
_109
import { Observable, Subject } from 'rxjs';
_109
_109
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_109
_109
@Injectable({
_109
providedIn: 'root',
_109
})
_109
export class SessionVaultService {
_109
private lockedSubject: Subject<boolean>;
_109
private vault: BrowserVault | Vault;
_109
_109
constructor() {
_109
this.vault = VaultFactory.create();
_109
this.lockedSubject = new Subject<boolean>();
_109
}
_109
_109
get locked$(): Observable<boolean> {
_109
return this.lockedSubject.asObservable();
_109
}
_109
_109
async initialize(): Promise<void> {
_109
try {
_109
await this.vault.initialize({
_109
key: 'io.ionic.gettingstartediv',
_109
type: VaultType.InMemory,
_109
deviceSecurityType: DeviceSecurityType.None,
_109
lockAfterBackgrounded: 30000,
_109
});
_109
} catch (e: unknown) {
_109
await this.vault.clear();
_109
await this.updateUnlockMode('InMemory');
_109
}
_109
_109
this.vault.onLock(() => this.lockedSubject.next(true));
_109
this.vault.onUnlock(() => this.lockedSubject.next(false));
_109
}
_109
_109
async storeSession(session: Session): Promise<void> {
_109
this.vault.setValue('session', session);
_109
}
_109
_109
async getSession(): Promise<Session | null> {
_109
if (await this.vault.isEmpty()) {
_109
return null;
_109
}
_109
return this.vault.getValue<Session>('session');
_109
}
_109
_109
async clearSession(): Promise<void> {
_109
await this.vault.clear();
_109
}
_109
_109
async lock(): Promise<void> {
_109
await this.vault.lock();
_109
}
_109
_109
async unlock(): Promise<void> {
_109
await this.vault.unlock();
_109
}
_109
_109
async isLocked(): Promise<boolean> {
_109
return (
_109
this.vault.config?.type !== VaultType.SecureStorage &&
_109
this.vault.config?.type !== VaultType.InMemory &&
_109
!(await this.vault.isEmpty()) &&
_109
(await this.vault.isLocked())
_109
);
_109
}
_109
_109
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_109
const type =
_109
mode === 'BiometricsWithPasscode'
_109
? VaultType.DeviceSecurity
_109
: mode === 'InMemory'
_109
? VaultType.InMemory
_109
: VaultType.SecureStorage;
_109
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_109
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_109
await this.vault.updateConfig({
_109
...(this.vault.config as IdentityVaultConfig),
_109
type,
_109
deviceSecurityType,
_109
lockAfterBackgrounded,
_109
});
_109
}
_109
_109
private async enhanceVault(): Promise<void> {
_109
if (!(await this.vault.isEmpty())) {
_109
return;
_109
}
_109
_109
if (await Device.isSystemPasscodeSet()) {
_109
await this.updateUnlockMode('BiometricsWithPasscode');
_109
} else {
_109
await this.updateUnlockMode('InMemory');
_109
}
_109
}
_109
}

Use the proper vault type based on whether or not a system passcode is set. Adjust the lock time as well.

src/app/core/session-vault.service.ts

_111
import { Injectable } from '@angular/core';
_111
import {
_111
BrowserVault,
_111
Device,
_111
DeviceSecurityType,
_111
IdentityVaultConfig,
_111
Vault,
_111
VaultType,
_111
} from '@ionic-enterprise/identity-vault';
_111
import { Session } from '../models/session';
_111
import { VaultFactory } from './vault.factory';
_111
import { Observable, Subject } from 'rxjs';
_111
_111
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_111
_111
@Injectable({
_111
providedIn: 'root',
_111
})
_111
export class SessionVaultService {
_111
private lockedSubject: Subject<boolean>;
_111
private vault: BrowserVault | Vault;
_111
_111
constructor() {
_111
this.vault = VaultFactory.create();
_111
this.lockedSubject = new Subject<boolean>();
_111
}
_111
_111
get locked$(): Observable<boolean> {
_111
return this.lockedSubject.asObservable();
_111
}
_111
_111
async initialize(): Promise<void> {
_111
try {
_111
await this.vault.initialize({
_111
key: 'io.ionic.gettingstartediv',
_111
type: VaultType.InMemory,
_111
deviceSecurityType: DeviceSecurityType.None,
_111
lockAfterBackgrounded: 30000,
_111
});
_111
} catch (e: unknown) {
_111
await this.vault.clear();
_111
await this.updateUnlockMode('InMemory');
_111
}
_111
_111
await this.enhanceVault();
_111
_111
this.vault.onLock(() => this.lockedSubject.next(true));
_111
this.vault.onUnlock(() => this.lockedSubject.next(false));
_111
}
_111
_111
async storeSession(session: Session): Promise<void> {
_111
this.vault.setValue('session', session);
_111
}
_111
_111
async getSession(): Promise<Session | null> {
_111
if (await this.vault.isEmpty()) {
_111
return null;
_111
}
_111
return this.vault.getValue<Session>('session');
_111
}
_111
_111
async clearSession(): Promise<void> {
_111
await this.vault.clear();
_111
}
_111
_111
async lock(): Promise<void> {
_111
await this.vault.lock();
_111
}
_111
_111
async unlock(): Promise<void> {
_111
await this.vault.unlock();
_111
}
_111
_111
async isLocked(): Promise<boolean> {
_111
return (
_111
this.vault.config?.type !== VaultType.SecureStorage &&
_111
this.vault.config?.type !== VaultType.InMemory &&
_111
!(await this.vault.isEmpty()) &&
_111
(await this.vault.isLocked())
_111
);
_111
}
_111
_111
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_111
const type =
_111
mode === 'BiometricsWithPasscode'
_111
? VaultType.DeviceSecurity
_111
: mode === 'InMemory'
_111
? VaultType.InMemory
_111
: VaultType.SecureStorage;
_111
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_111
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_111
await this.vault.updateConfig({
_111
...(this.vault.config as IdentityVaultConfig),
_111
type,
_111
deviceSecurityType,
_111
lockAfterBackgrounded,
_111
});
_111
}
_111
_111
private async enhanceVault(): Promise<void> {
_111
if (!(await this.vault.isEmpty())) {
_111
return;
_111
}
_111
_111
if (await Device.isSystemPasscodeSet()) {
_111
await this.updateUnlockMode('BiometricsWithPasscode');
_111
} else {
_111
await this.updateUnlockMode('InMemory');
_111
}
_111
}
_111
}

Enhance the vault as part of the initialization process.

Our app is written to start with a Secure Storage type vault.

Import the Device class so we can use the API.

Change the type to be InMemory and increase the background lock time from 2 seconds to 30 seconds.

Add a private method called enhanceVault(). Per our first requirement, the vault should not be changed if it is already in use.

Use the proper vault type based on whether or not a system passcode is set. Adjust the lock time as well.

Enhance the vault as part of the initialization process.

src/app/core/session-vault.service.ts

_94
import { Injectable } from '@angular/core';
_94
import {
_94
BrowserVault,
_94
DeviceSecurityType,
_94
IdentityVaultConfig,
_94
Vault,
_94
VaultType,
_94
} from '@ionic-enterprise/identity-vault';
_94
import { Session } from '../models/session';
_94
import { VaultFactory } from './vault.factory';
_94
import { Observable, Subject } from 'rxjs';
_94
_94
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_94
_94
@Injectable({
_94
providedIn: 'root',
_94
})
_94
export class SessionVaultService {
_94
private lockedSubject: Subject<boolean>;
_94
private vault: BrowserVault | Vault;
_94
_94
constructor() {
_94
this.vault = VaultFactory.create();
_94
this.lockedSubject = new Subject<boolean>();
_94
}
_94
_94
get locked$(): Observable<boolean> {
_94
return this.lockedSubject.asObservable();
_94
}
_94
_94
async initialize(): Promise<void> {
_94
try {
_94
await this.vault.initialize({
_94
key: 'io.ionic.gettingstartediv',
_94
type: VaultType.SecureStorage,
_94
deviceSecurityType: DeviceSecurityType.None,
_94
lockAfterBackgrounded: 2000,
_94
});
_94
} catch (e: unknown) {
_94
await this.vault.clear();
_94
await this.updateUnlockMode('SecureStorage');
_94
}
_94
_94
this.vault.onLock(() => this.lockedSubject.next(true));
_94
this.vault.onUnlock(() => this.lockedSubject.next(false));
_94
}
_94
_94
async storeSession(session: Session): Promise<void> {
_94
this.vault.setValue('session', session);
_94
}
_94
_94
async getSession(): Promise<Session | null> {
_94
if (await this.vault.isEmpty()) {
_94
return null;
_94
}
_94
return this.vault.getValue<Session>('session');
_94
}
_94
_94
async clearSession(): Promise<void> {
_94
await this.vault.clear();
_94
}
_94
_94
async lock(): Promise<void> {
_94
await this.vault.lock();
_94
}
_94
_94
async unlock(): Promise<void> {
_94
await this.vault.unlock();
_94
}
_94
_94
async isLocked(): Promise<boolean> {
_94
return (
_94
this.vault.config?.type !== VaultType.SecureStorage &&
_94
this.vault.config?.type !== VaultType.InMemory &&
_94
!(await this.vault.isEmpty()) &&
_94
(await this.vault.isLocked())
_94
);
_94
}
_94
_94
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_94
const type =
_94
mode === 'BiometricsWithPasscode'
_94
? VaultType.DeviceSecurity
_94
: mode === 'InMemory'
_94
? VaultType.InMemory
_94
: VaultType.SecureStorage;
_94
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_94
await this.vault.updateConfig({
_94
...(this.vault.config as IdentityVaultConfig),
_94
type,
_94
deviceSecurityType,
_94
});
_94
}
_94
}

Build the application and test it on a variety of devices using a variety of screen unlock settings. You should see the following behaviors:

  • On devices without a system passcode or biometrics the user should be forced to login any time the application is restarted or is put in the background for more than 30 seconds. We have made this timeout longer than our standard 2 second timeout because the InMemory vault will destroy the authentication result after the timeout rather than storing it.
  • On devices with a system passcode but without biometrics set up, the session is stored between invocations and is unlocked via the system passcode. The application locks after 2 seconds in the background.
  • On devices with a system passcode and with biometrics set up, the session is stored between invocations and is unlocked via biometrics using the system passcode as backup. The application locks after 2 seconds in the background.
  • On all devices, if "Use Secure Storage" is selected, then a secure storage vault will be used. This can be tested by pressing "Use Secure Storage" and closing the app. When it comes back the user should still be logged in. This should repeat until the user logs out prior to closing the app.
note

We are taking advantage of the fact that DeviceSecurityType.Both uses the system passcode not only when biometrics fails, but also if biometrics is not set up at all. Perhaps we want our app to behave in a more specific manner such as any of the following:

  • Use only Biometrics with a system passcode backup only if biometrics is set up.
  • Use Biometrics without a system passcode backup, but use system passcode if biometrics is not set up.
  • Use Biometrics with a system passcode backup only if biometrics is set up. Allow the user to set a custom passcode in other circumstances.

Identity Vault and the Device API give you the flexibility to support any of those scenarios and more. Please contact us if you would like to discuss your specific scenario.

Handle iOS Permissions

On iOS devices that use Face ID, the user needs to give permission in order for the application to use their face to unlock the app. The prompt for this permission is not provided to the user until your app tries to use Face ID for the first time. As a result, our application is not asking for permission to use the user's face until the first time it tries to unlock the vault. It would be better if it asked for permission before setting the vault to use biometrics. Let's do that now.

The requirements we have from our design team are;

  • Keep the vault type logic as it is now. That is:
    • If there is a system passcode and biometrics, use biometrics with a system passcode backup.
    • If there is a system passcode but no biometrics, use the system passcode.
    • If there no system passcode force the user to log in each time.
  • In addition, if biometrics is enabled:
    • Determine if the user needs to be prompted for permission.
    • If the user needs to be prompted for permission, show a biometric prompt. This will also trigger the permissions prompt in cases where permission is required.
    • If the user allows Face ID to be used, set the vault type to DeviceSecurity.
    • If the user denies permission to use Face ID, set the vault type to InMemory.
src/app/core/session-vault.service.ts

_122
import { Injectable } from '@angular/core';
_122
import {
_122
BiometricPermissionState,
_122
BrowserVault,
_122
Device,
_122
DeviceSecurityType,
_122
IdentityVaultConfig,
_122
Vault,
_122
VaultType,
_122
} from '@ionic-enterprise/identity-vault';
_122
import { Session } from '../models/session';
_122
import { VaultFactory } from './vault.factory';
_122
import { Observable, Subject } from 'rxjs';
_122
_122
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_122
_122
@Injectable({
_122
providedIn: 'root',
_122
})
_122
export class SessionVaultService {
_122
private lockedSubject: Subject<boolean>;
_122
private vault: BrowserVault | Vault;
_122
_122
constructor() {
_122
this.vault = VaultFactory.create();
_122
this.lockedSubject = new Subject<boolean>();
_122
}
_122
_122
get locked$(): Observable<boolean> {
_122
return this.lockedSubject.asObservable();
_122
}
_122
_122
async initialize(): Promise<void> {
_122
try {
_122
await this.vault.initialize({
_122
key: 'io.ionic.gettingstartediv',
_122
type: VaultType.InMemory,
_122
deviceSecurityType: DeviceSecurityType.None,
_122
lockAfterBackgrounded: 30000,
_122
});
_122
} catch (e: unknown) {
_122
await this.vault.clear();
_122
await this.updateUnlockMode('InMemory');
_122
}
_122
_122
await this.enhanceVault();
_122
_122
this.vault.onLock(() => this.lockedSubject.next(true));
_122
this.vault.onUnlock(() => this.lockedSubject.next(false));
_122
}
_122
_122
async storeSession(session: Session): Promise<void> {
_122
this.vault.setValue('session', session);
_122
}
_122
_122
async getSession(): Promise<Session | null> {
_122
if (await this.vault.isEmpty()) {
_122
return null;
_122
}
_122
return this.vault.getValue<Session>('session');
_122
}
_122
_122
async clearSession(): Promise<void> {
_122
await this.vault.clear();
_122
}
_122
_122
async lock(): Promise<void> {
_122
await this.vault.lock();
_122
}
_122
_122
async unlock(): Promise<void> {
_122
await this.vault.unlock();
_122
}
_122
_122
async isLocked(): Promise<boolean> {
_122
return (
_122
this.vault.config?.type !== VaultType.SecureStorage &&
_122
this.vault.config?.type !== VaultType.InMemory &&
_122
!(await this.vault.isEmpty()) &&
_122
(await this.vault.isLocked())
_122
);
_122
}
_122
_122
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_122
const type =
_122
mode === 'BiometricsWithPasscode'
_122
? VaultType.DeviceSecurity
_122
: mode === 'InMemory'
_122
? VaultType.InMemory
_122
: VaultType.SecureStorage;
_122
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_122
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_122
await this.vault.updateConfig({
_122
...(this.vault.config as IdentityVaultConfig),
_122
type,
_122
deviceSecurityType,
_122
lockAfterBackgrounded,
_122
});
_122
}
_122
_122
private async provisionBiometrics(): Promise<void> {
_122
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_122
try {
_122
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_122
} catch (error) {
_122
null;
_122
}
_122
}
_122
}
_122
_122
private async enhanceVault(): Promise<void> {
_122
if (!(await this.vault.isEmpty())) {
_122
return;
_122
}
_122
_122
if (await Device.isSystemPasscodeSet()) {
_122
await this.updateUnlockMode('BiometricsWithPasscode');
_122
} else {
_122
await this.updateUnlockMode('InMemory');
_122
}
_122
}
_122
}

Add a private method called provisionBiometrics(). Display the biometric prompt if permissions need to be prompted.

src/app/core/session-vault.service.ts

_125
import { Injectable } from '@angular/core';
_125
import {
_125
BiometricPermissionState,
_125
BrowserVault,
_125
Device,
_125
DeviceSecurityType,
_125
IdentityVaultConfig,
_125
Vault,
_125
VaultType,
_125
} from '@ionic-enterprise/identity-vault';
_125
import { Session } from '../models/session';
_125
import { VaultFactory } from './vault.factory';
_125
import { Observable, Subject } from 'rxjs';
_125
_125
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_125
_125
@Injectable({
_125
providedIn: 'root',
_125
})
_125
export class SessionVaultService {
_125
private lockedSubject: Subject<boolean>;
_125
private vault: BrowserVault | Vault;
_125
_125
constructor() {
_125
this.vault = VaultFactory.create();
_125
this.lockedSubject = new Subject<boolean>();
_125
}
_125
_125
get locked$(): Observable<boolean> {
_125
return this.lockedSubject.asObservable();
_125
}
_125
_125
async initialize(): Promise<void> {
_125
try {
_125
await this.vault.initialize({
_125
key: 'io.ionic.gettingstartediv',
_125
type: VaultType.InMemory,
_125
deviceSecurityType: DeviceSecurityType.None,
_125
lockAfterBackgrounded: 30000,
_125
});
_125
} catch (e: unknown) {
_125
await this.vault.clear();
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
_125
await this.enhanceVault();
_125
_125
this.vault.onLock(() => this.lockedSubject.next(true));
_125
this.vault.onUnlock(() => this.lockedSubject.next(false));
_125
}
_125
_125
async storeSession(session: Session): Promise<void> {
_125
this.vault.setValue('session', session);
_125
}
_125
_125
async getSession(): Promise<Session | null> {
_125
if (await this.vault.isEmpty()) {
_125
return null;
_125
}
_125
return this.vault.getValue<Session>('session');
_125
}
_125
_125
async clearSession(): Promise<void> {
_125
await this.vault.clear();
_125
}
_125
_125
async lock(): Promise<void> {
_125
await this.vault.lock();
_125
}
_125
_125
async unlock(): Promise<void> {
_125
await this.vault.unlock();
_125
}
_125
_125
async isLocked(): Promise<boolean> {
_125
return (
_125
this.vault.config?.type !== VaultType.SecureStorage &&
_125
this.vault.config?.type !== VaultType.InMemory &&
_125
!(await this.vault.isEmpty()) &&
_125
(await this.vault.isLocked())
_125
);
_125
}
_125
_125
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_125
const type = await this.getVaultType(mode);
_125
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_125
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_125
await this.vault.updateConfig({
_125
...(this.vault.config as IdentityVaultConfig),
_125
type,
_125
deviceSecurityType,
_125
lockAfterBackgrounded,
_125
});
_125
}
_125
_125
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_125
return mode === 'BiometricsWithPasscode'
_125
? VaultType.DeviceSecurity
_125
: mode === 'InMemory'
_125
? VaultType.InMemory
_125
: VaultType.SecureStorage;
_125
}
_125
_125
private async provisionBiometrics(): Promise<void> {
_125
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_125
try {
_125
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_125
} catch (error) {
_125
null;
_125
}
_125
}
_125
}
_125
_125
private async enhanceVault(): Promise<void> {
_125
if (!(await this.vault.isEmpty())) {
_125
return;
_125
}
_125
_125
if (await Device.isSystemPasscodeSet()) {
_125
await this.updateUnlockMode('BiometricsWithPasscode');
_125
} else {
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
}
_125
}

Determining the proper vault type is going to get more complex, so let's start by abstracting that logic into its own method.

src/app/core/session-vault.service.ts

_125
import { Injectable } from '@angular/core';
_125
import {
_125
BiometricPermissionState,
_125
BrowserVault,
_125
Device,
_125
DeviceSecurityType,
_125
IdentityVaultConfig,
_125
Vault,
_125
VaultType,
_125
} from '@ionic-enterprise/identity-vault';
_125
import { Session } from '../models/session';
_125
import { VaultFactory } from './vault.factory';
_125
import { Observable, Subject } from 'rxjs';
_125
_125
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_125
_125
@Injectable({
_125
providedIn: 'root',
_125
})
_125
export class SessionVaultService {
_125
private lockedSubject: Subject<boolean>;
_125
private vault: BrowserVault | Vault;
_125
_125
constructor() {
_125
this.vault = VaultFactory.create();
_125
this.lockedSubject = new Subject<boolean>();
_125
}
_125
_125
get locked$(): Observable<boolean> {
_125
return this.lockedSubject.asObservable();
_125
}
_125
_125
async initialize(): Promise<void> {
_125
try {
_125
await this.vault.initialize({
_125
key: 'io.ionic.gettingstartediv',
_125
type: VaultType.InMemory,
_125
deviceSecurityType: DeviceSecurityType.None,
_125
lockAfterBackgrounded: 30000,
_125
});
_125
} catch (e: unknown) {
_125
await this.vault.clear();
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
_125
await this.enhanceVault();
_125
_125
this.vault.onLock(() => this.lockedSubject.next(true));
_125
this.vault.onUnlock(() => this.lockedSubject.next(false));
_125
}
_125
_125
async storeSession(session: Session): Promise<void> {
_125
this.vault.setValue('session', session);
_125
}
_125
_125
async getSession(): Promise<Session | null> {
_125
if (await this.vault.isEmpty()) {
_125
return null;
_125
}
_125
return this.vault.getValue<Session>('session');
_125
}
_125
_125
async clearSession(): Promise<void> {
_125
await this.vault.clear();
_125
}
_125
_125
async lock(): Promise<void> {
_125
await this.vault.lock();
_125
}
_125
_125
async unlock(): Promise<void> {
_125
await this.vault.unlock();
_125
}
_125
_125
async isLocked(): Promise<boolean> {
_125
return (
_125
this.vault.config?.type !== VaultType.SecureStorage &&
_125
this.vault.config?.type !== VaultType.InMemory &&
_125
!(await this.vault.isEmpty()) &&
_125
(await this.vault.isLocked())
_125
);
_125
}
_125
_125
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_125
const type = await this.getVaultType(mode);
_125
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_125
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_125
await this.vault.updateConfig({
_125
...(this.vault.config as IdentityVaultConfig),
_125
type,
_125
deviceSecurityType,
_125
lockAfterBackgrounded,
_125
});
_125
}
_125
_125
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_125
if (mode === 'BiometricsWithPasscode') {
_125
return VaultType.DeviceSecurity;
_125
}
_125
_125
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_125
}
_125
_125
private async provisionBiometrics(): Promise<void> {
_125
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_125
try {
_125
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_125
} catch (error) {
_125
null;
_125
}
_125
}
_125
}
_125
_125
private async enhanceVault(): Promise<void> {
_125
if (!(await this.vault.isEmpty())) {
_125
return;
_125
}
_125
_125
if (await Device.isSystemPasscodeSet()) {
_125
await this.updateUnlockMode('BiometricsWithPasscode');
_125
} else {
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
}
_125
}

Biometrics will be special, so let's give it its own section.

src/app/core/session-vault.service.ts

_126
import { Injectable } from '@angular/core';
_126
import {
_126
BiometricPermissionState,
_126
BrowserVault,
_126
Device,
_126
DeviceSecurityType,
_126
IdentityVaultConfig,
_126
Vault,
_126
VaultType,
_126
} from '@ionic-enterprise/identity-vault';
_126
import { Session } from '../models/session';
_126
import { VaultFactory } from './vault.factory';
_126
import { Observable, Subject } from 'rxjs';
_126
_126
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_126
_126
@Injectable({
_126
providedIn: 'root',
_126
})
_126
export class SessionVaultService {
_126
private lockedSubject: Subject<boolean>;
_126
private vault: BrowserVault | Vault;
_126
_126
constructor() {
_126
this.vault = VaultFactory.create();
_126
this.lockedSubject = new Subject<boolean>();
_126
}
_126
_126
get locked$(): Observable<boolean> {
_126
return this.lockedSubject.asObservable();
_126
}
_126
_126
async initialize(): Promise<void> {
_126
try {
_126
await this.vault.initialize({
_126
key: 'io.ionic.gettingstartediv',
_126
type: VaultType.InMemory,
_126
deviceSecurityType: DeviceSecurityType.None,
_126
lockAfterBackgrounded: 30000,
_126
});
_126
} catch (e: unknown) {
_126
await this.vault.clear();
_126
await this.updateUnlockMode('InMemory');
_126
}
_126
_126
await this.enhanceVault();
_126
_126
this.vault.onLock(() => this.lockedSubject.next(true));
_126
this.vault.onUnlock(() => this.lockedSubject.next(false));
_126
}
_126
_126
async storeSession(session: Session): Promise<void> {
_126
this.vault.setValue('session', session);
_126
}
_126
_126
async getSession(): Promise<Session | null> {
_126
if (await this.vault.isEmpty()) {
_126
return null;
_126
}
_126
return this.vault.getValue<Session>('session');
_126
}
_126
_126
async clearSession(): Promise<void> {
_126
await this.vault.clear();
_126
}
_126
_126
async lock(): Promise<void> {
_126
await this.vault.lock();
_126
}
_126
_126
async unlock(): Promise<void> {
_126
await this.vault.unlock();
_126
}
_126
_126
async isLocked(): Promise<boolean> {
_126
return (
_126
this.vault.config?.type !== VaultType.SecureStorage &&
_126
this.vault.config?.type !== VaultType.InMemory &&
_126
!(await this.vault.isEmpty()) &&
_126
(await this.vault.isLocked())
_126
);
_126
}
_126
_126
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_126
const type = await this.getVaultType(mode);
_126
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_126
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_126
await this.vault.updateConfig({
_126
...(this.vault.config as IdentityVaultConfig),
_126
type,
_126
deviceSecurityType,
_126
lockAfterBackgrounded,
_126
});
_126
}
_126
_126
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_126
if (mode === 'BiometricsWithPasscode') {
_126
await this.provisionBiometrics();
_126
return VaultType.DeviceSecurity;
_126
}
_126
_126
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_126
}
_126
_126
private async provisionBiometrics(): Promise<void> {
_126
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_126
try {
_126
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_126
} catch (error) {
_126
null;
_126
}
_126
}
_126
}
_126
_126
private async enhanceVault(): Promise<void> {
_126
if (!(await this.vault.isEmpty())) {
_126
return;
_126
}
_126
_126
if (await Device.isSystemPasscodeSet()) {
_126
await this.updateUnlockMode('BiometricsWithPasscode');
_126
} else {
_126
await this.updateUnlockMode('InMemory');
_126
}
_126
}
_126
}

Provision the Biometrics.

src/app/core/session-vault.service.ts

_129
import { Injectable } from '@angular/core';
_129
import {
_129
BiometricPermissionState,
_129
BrowserVault,
_129
Device,
_129
DeviceSecurityType,
_129
IdentityVaultConfig,
_129
Vault,
_129
VaultType,
_129
} from '@ionic-enterprise/identity-vault';
_129
import { Session } from '../models/session';
_129
import { VaultFactory } from './vault.factory';
_129
import { Observable, Subject } from 'rxjs';
_129
_129
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_129
_129
@Injectable({
_129
providedIn: 'root',
_129
})
_129
export class SessionVaultService {
_129
private lockedSubject: Subject<boolean>;
_129
private vault: BrowserVault | Vault;
_129
_129
constructor() {
_129
this.vault = VaultFactory.create();
_129
this.lockedSubject = new Subject<boolean>();
_129
}
_129
_129
get locked$(): Observable<boolean> {
_129
return this.lockedSubject.asObservable();
_129
}
_129
_129
async initialize(): Promise<void> {
_129
try {
_129
await this.vault.initialize({
_129
key: 'io.ionic.gettingstartediv',
_129
type: VaultType.InMemory,
_129
deviceSecurityType: DeviceSecurityType.None,
_129
lockAfterBackgrounded: 30000,
_129
});
_129
} catch (e: unknown) {
_129
await this.vault.clear();
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
_129
await this.enhanceVault();
_129
_129
this.vault.onLock(() => this.lockedSubject.next(true));
_129
this.vault.onUnlock(() => this.lockedSubject.next(false));
_129
}
_129
_129
async storeSession(session: Session): Promise<void> {
_129
this.vault.setValue('session', session);
_129
}
_129
_129
async getSession(): Promise<Session | null> {
_129
if (await this.vault.isEmpty()) {
_129
return null;
_129
}
_129
return this.vault.getValue<Session>('session');
_129
}
_129
_129
async clearSession(): Promise<void> {
_129
await this.vault.clear();
_129
}
_129
_129
async lock(): Promise<void> {
_129
await this.vault.lock();
_129
}
_129
_129
async unlock(): Promise<void> {
_129
await this.vault.unlock();
_129
}
_129
_129
async isLocked(): Promise<boolean> {
_129
return (
_129
this.vault.config?.type !== VaultType.SecureStorage &&
_129
this.vault.config?.type !== VaultType.InMemory &&
_129
!(await this.vault.isEmpty()) &&
_129
(await this.vault.isLocked())
_129
);
_129
}
_129
_129
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_129
const type = await this.getVaultType(mode);
_129
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_129
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_129
await this.vault.updateConfig({
_129
...(this.vault.config as IdentityVaultConfig),
_129
type,
_129
deviceSecurityType,
_129
lockAfterBackgrounded,
_129
});
_129
}
_129
_129
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_129
if (mode === 'BiometricsWithPasscode') {
_129
await this.provisionBiometrics();
_129
return (await Device.isBiometricsEnabled()) &&
_129
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_129
? VaultType.InMemory
_129
: VaultType.DeviceSecurity;
_129
}
_129
_129
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_129
}
_129
_129
private async provisionBiometrics(): Promise<void> {
_129
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_129
try {
_129
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_129
} catch (error) {
_129
null;
_129
}
_129
}
_129
}
_129
_129
private async enhanceVault(): Promise<void> {
_129
if (!(await this.vault.isEmpty())) {
_129
return;
_129
}
_129
_129
if (await Device.isSystemPasscodeSet()) {
_129
await this.updateUnlockMode('BiometricsWithPasscode');
_129
} else {
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
}
_129
}

If biometrics has been set up, return the vault type based on whether the user has granted or denied access to Face ID.

Add a private method called provisionBiometrics(). Display the biometric prompt if permissions need to be prompted.

Determining the proper vault type is going to get more complex, so let's start by abstracting that logic into its own method.

Biometrics will be special, so let's give it its own section.

Provision the Biometrics.

If biometrics has been set up, return the vault type based on whether the user has granted or denied access to Face ID.

src/app/core/session-vault.service.ts

_122
import { Injectable } from '@angular/core';
_122
import {
_122
BiometricPermissionState,
_122
BrowserVault,
_122
Device,
_122
DeviceSecurityType,
_122
IdentityVaultConfig,
_122
Vault,
_122
VaultType,
_122
} from '@ionic-enterprise/identity-vault';
_122
import { Session } from '../models/session';
_122
import { VaultFactory } from './vault.factory';
_122
import { Observable, Subject } from 'rxjs';
_122
_122
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_122
_122
@Injectable({
_122
providedIn: 'root',
_122
})
_122
export class SessionVaultService {
_122
private lockedSubject: Subject<boolean>;
_122
private vault: BrowserVault | Vault;
_122
_122
constructor() {
_122
this.vault = VaultFactory.create();
_122
this.lockedSubject = new Subject<boolean>();
_122
}
_122
_122
get locked$(): Observable<boolean> {
_122
return this.lockedSubject.asObservable();
_122
}
_122
_122
async initialize(): Promise<void> {
_122
try {
_122
await this.vault.initialize({
_122
key: 'io.ionic.gettingstartediv',
_122
type: VaultType.InMemory,
_122
deviceSecurityType: DeviceSecurityType.None,
_122
lockAfterBackgrounded: 30000,
_122
});
_122
} catch (e: unknown) {
_122
await this.vault.clear();
_122
await this.updateUnlockMode('InMemory');
_122
}
_122
_122
await this.enhanceVault();
_122
_122
this.vault.onLock(() => this.lockedSubject.next(true));
_122
this.vault.onUnlock(() => this.lockedSubject.next(false));
_122
}
_122
_122
async storeSession(session: Session): Promise<void> {
_122
this.vault.setValue('session', session);
_122
}
_122
_122
async getSession(): Promise<Session | null> {
_122
if (await this.vault.isEmpty()) {
_122
return null;
_122
}
_122
return this.vault.getValue<Session>('session');
_122
}
_122
_122
async clearSession(): Promise<void> {
_122
await this.vault.clear();
_122
}
_122
_122
async lock(): Promise<void> {
_122
await this.vault.lock();
_122
}
_122
_122
async unlock(): Promise<void> {
_122
await this.vault.unlock();
_122
}
_122
_122
async isLocked(): Promise<boolean> {
_122
return (
_122
this.vault.config?.type !== VaultType.SecureStorage &&
_122
this.vault.config?.type !== VaultType.InMemory &&
_122
!(await this.vault.isEmpty()) &&
_122
(await this.vault.isLocked())
_122
);
_122
}
_122
_122
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_122
const type =
_122
mode === 'BiometricsWithPasscode'
_122
? VaultType.DeviceSecurity
_122
: mode === 'InMemory'
_122
? VaultType.InMemory
_122
: VaultType.SecureStorage;
_122
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_122
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_122
await this.vault.updateConfig({
_122
...(this.vault.config as IdentityVaultConfig),
_122
type,
_122
deviceSecurityType,
_122
lockAfterBackgrounded,
_122
});
_122
}
_122
_122
private async provisionBiometrics(): Promise<void> {
_122
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_122
try {
_122
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_122
} catch (error) {
_122
null;
_122
}
_122
}
_122
}
_122
_122
private async enhanceVault(): Promise<void> {
_122
if (!(await this.vault.isEmpty())) {
_122
return;
_122
}
_122
_122
if (await Device.isSystemPasscodeSet()) {
_122
await this.updateUnlockMode('BiometricsWithPasscode');
_122
} else {
_122
await this.updateUnlockMode('InMemory');
_122
}
_122
}
_122
}

Perform a fresh install on several different devices. On iOS devices that use Face ID, the app should ask permission to use the Face ID upon the first start after application install. If you allow the use of Face ID, the application will use Biometrics with a System Passcode backup. If you disallow the use of Face ID, the application will use an "In Memory" vault.

On all other devices, you should not receive any permission requests and application will use Biometrics with a System Passcode backup by default.

Notice that the permission request comes during application startup. This may be jarring to some users. For this application it may be better to ask for the Face ID permission right after the user logs in. Since the provisioning is tied to the enhancement of the vault to use biometrics, this means delaying the enhancement of the vault until after login.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_129
import { Injectable } from '@angular/core';
_129
import {
_129
BiometricPermissionState,
_129
BrowserVault,
_129
Device,
_129
DeviceSecurityType,
_129
IdentityVaultConfig,
_129
Vault,
_129
VaultType,
_129
} from '@ionic-enterprise/identity-vault';
_129
import { Session } from '../models/session';
_129
import { VaultFactory } from './vault.factory';
_129
import { Observable, Subject } from 'rxjs';
_129
_129
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_129
_129
@Injectable({
_129
providedIn: 'root',
_129
})
_129
export class SessionVaultService {
_129
private lockedSubject: Subject<boolean>;
_129
private vault: BrowserVault | Vault;
_129
_129
constructor() {
_129
this.vault = VaultFactory.create();
_129
this.lockedSubject = new Subject<boolean>();
_129
}
_129
_129
get locked$(): Observable<boolean> {
_129
return this.lockedSubject.asObservable();
_129
}
_129
_129
async initialize(): Promise<void> {
_129
try {
_129
await this.vault.initialize({
_129
key: 'io.ionic.gettingstartediv',
_129
type: VaultType.InMemory,
_129
deviceSecurityType: DeviceSecurityType.None,
_129
lockAfterBackgrounded: 30000,
_129
});
_129
} catch (e: unknown) {
_129
await this.vault.clear();
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
_129
await this.enhanceVault();
_129
_129
this.vault.onLock(() => this.lockedSubject.next(true));
_129
this.vault.onUnlock(() => this.lockedSubject.next(false));
_129
}
_129
_129
async storeSession(session: Session): Promise<void> {
_129
this.vault.setValue('session', session);
_129
}
_129
_129
async getSession(): Promise<Session | null> {
_129
if (await this.vault.isEmpty()) {
_129
return null;
_129
}
_129
return this.vault.getValue<Session>('session');
_129
}
_129
_129
async clearSession(): Promise<void> {
_129
await this.vault.clear();
_129
}
_129
_129
async lock(): Promise<void> {
_129
await this.vault.lock();
_129
}
_129
_129
async unlock(): Promise<void> {
_129
await this.vault.unlock();
_129
}
_129
_129
async isLocked(): Promise<boolean> {
_129
return (
_129
this.vault.config?.type !== VaultType.SecureStorage &&
_129
this.vault.config?.type !== VaultType.InMemory &&
_129
!(await this.vault.isEmpty()) &&
_129
(await this.vault.isLocked())
_129
);
_129
}
_129
_129
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_129
const type = await this.getVaultType(mode);
_129
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_129
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_129
await this.vault.updateConfig({
_129
...(this.vault.config as IdentityVaultConfig),
_129
type,
_129
deviceSecurityType,
_129
lockAfterBackgrounded,
_129
});
_129
}
_129
_129
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_129
if (mode === 'BiometricsWithPasscode') {
_129
await this.provisionBiometrics();
_129
return (await Device.isBiometricsEnabled()) &&
_129
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_129
? VaultType.InMemory
_129
: VaultType.DeviceSecurity;
_129
}
_129
_129
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_129
}
_129
_129
private async provisionBiometrics(): Promise<void> {
_129
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_129
try {
_129
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_129
} catch (error) {
_129
null;
_129
}
_129
}
_129
}
_129
_129
private async enhanceVault(): Promise<void> {
_129
if (!(await this.vault.isEmpty())) {
_129
return;
_129
}
_129
_129
if (await Device.isSystemPasscodeSet()) {
_129
await this.updateUnlockMode('BiometricsWithPasscode');
_129
} else {
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
}
_129
}

The enhanceVault() method is currently private.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_129
import { Injectable } from '@angular/core';
_129
import {
_129
BiometricPermissionState,
_129
BrowserVault,
_129
Device,
_129
DeviceSecurityType,
_129
IdentityVaultConfig,
_129
Vault,
_129
VaultType,
_129
} from '@ionic-enterprise/identity-vault';
_129
import { Session } from '../models/session';
_129
import { VaultFactory } from './vault.factory';
_129
import { Observable, Subject } from 'rxjs';
_129
_129
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_129
_129
@Injectable({
_129
providedIn: 'root',
_129
})
_129
export class SessionVaultService {
_129
private lockedSubject: Subject<boolean>;
_129
private vault: BrowserVault | Vault;
_129
_129
constructor() {
_129
this.vault = VaultFactory.create();
_129
this.lockedSubject = new Subject<boolean>();
_129
}
_129
_129
get locked$(): Observable<boolean> {
_129
return this.lockedSubject.asObservable();
_129
}
_129
_129
async initialize(): Promise<void> {
_129
try {
_129
await this.vault.initialize({
_129
key: 'io.ionic.gettingstartediv',
_129
type: VaultType.InMemory,
_129
deviceSecurityType: DeviceSecurityType.None,
_129
lockAfterBackgrounded: 30000,
_129
});
_129
} catch (e: unknown) {
_129
await this.vault.clear();
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
_129
await this.enhanceVault();
_129
_129
this.vault.onLock(() => this.lockedSubject.next(true));
_129
this.vault.onUnlock(() => this.lockedSubject.next(false));
_129
}
_129
_129
async storeSession(session: Session): Promise<void> {
_129
this.vault.setValue('session', session);
_129
}
_129
_129
async getSession(): Promise<Session | null> {
_129
if (await this.vault.isEmpty()) {
_129
return null;
_129
}
_129
return this.vault.getValue<Session>('session');
_129
}
_129
_129
async clearSession(): Promise<void> {
_129
await this.vault.clear();
_129
}
_129
_129
async lock(): Promise<void> {
_129
await this.vault.lock();
_129
}
_129
_129
async unlock(): Promise<void> {
_129
await this.vault.unlock();
_129
}
_129
_129
async isLocked(): Promise<boolean> {
_129
return (
_129
this.vault.config?.type !== VaultType.SecureStorage &&
_129
this.vault.config?.type !== VaultType.InMemory &&
_129
!(await this.vault.isEmpty()) &&
_129
(await this.vault.isLocked())
_129
);
_129
}
_129
_129
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_129
const type = await this.getVaultType(mode);
_129
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_129
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_129
await this.vault.updateConfig({
_129
...(this.vault.config as IdentityVaultConfig),
_129
type,
_129
deviceSecurityType,
_129
lockAfterBackgrounded,
_129
});
_129
}
_129
_129
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_129
if (mode === 'BiometricsWithPasscode') {
_129
await this.provisionBiometrics();
_129
return (await Device.isBiometricsEnabled()) &&
_129
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_129
? VaultType.InMemory
_129
: VaultType.DeviceSecurity;
_129
}
_129
_129
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_129
}
_129
_129
private async provisionBiometrics(): Promise<void> {
_129
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_129
try {
_129
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_129
} catch (error) {
_129
null;
_129
}
_129
}
_129
}
_129
_129
async enhanceVault(): Promise<void> {
_129
if (!(await this.vault.isEmpty())) {
_129
return;
_129
}
_129
_129
if (await Device.isSystemPasscodeSet()) {
_129
await this.updateUnlockMode('BiometricsWithPasscode');
_129
} else {
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
}
_129
}

Make it public.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_129
import { Injectable } from '@angular/core';
_129
import {
_129
BiometricPermissionState,
_129
BrowserVault,
_129
Device,
_129
DeviceSecurityType,
_129
IdentityVaultConfig,
_129
Vault,
_129
VaultType,
_129
} from '@ionic-enterprise/identity-vault';
_129
import { Session } from '../models/session';
_129
import { VaultFactory } from './vault.factory';
_129
import { Observable, Subject } from 'rxjs';
_129
_129
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_129
_129
@Injectable({
_129
providedIn: 'root',
_129
})
_129
export class SessionVaultService {
_129
private lockedSubject: Subject<boolean>;
_129
private vault: BrowserVault | Vault;
_129
_129
constructor() {
_129
this.vault = VaultFactory.create();
_129
this.lockedSubject = new Subject<boolean>();
_129
}
_129
_129
get locked$(): Observable<boolean> {
_129
return this.lockedSubject.asObservable();
_129
}
_129
_129
async initialize(): Promise<void> {
_129
try {
_129
await this.vault.initialize({
_129
key: 'io.ionic.gettingstartediv',
_129
type: VaultType.InMemory,
_129
deviceSecurityType: DeviceSecurityType.None,
_129
lockAfterBackgrounded: 30000,
_129
});
_129
} catch (e: unknown) {
_129
await this.vault.clear();
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
_129
await this.enhanceVault();
_129
_129
this.vault.onLock(() => this.lockedSubject.next(true));
_129
this.vault.onUnlock(() => this.lockedSubject.next(false));
_129
}
_129
_129
async storeSession(session: Session): Promise<void> {
_129
this.vault.setValue('session', session);
_129
}
_129
_129
async getSession(): Promise<Session | null> {
_129
if (await this.vault.isEmpty()) {
_129
return null;
_129
}
_129
return this.vault.getValue<Session>('session');
_129
}
_129
_129
async clearSession(): Promise<void> {
_129
await this.vault.clear();
_129
}
_129
_129
async lock(): Promise<void> {
_129
await this.vault.lock();
_129
}
_129
_129
async unlock(): Promise<void> {
_129
await this.vault.unlock();
_129
}
_129
_129
async isLocked(): Promise<boolean> {
_129
return (
_129
this.vault.config?.type !== VaultType.SecureStorage &&
_129
this.vault.config?.type !== VaultType.InMemory &&
_129
!(await this.vault.isEmpty()) &&
_129
(await this.vault.isLocked())
_129
);
_129
}
_129
_129
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_129
const type = await this.getVaultType(mode);
_129
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_129
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_129
await this.vault.updateConfig({
_129
...(this.vault.config as IdentityVaultConfig),
_129
type,
_129
deviceSecurityType,
_129
lockAfterBackgrounded,
_129
});
_129
}
_129
_129
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_129
if (mode === 'BiometricsWithPasscode') {
_129
await this.provisionBiometrics();
_129
return (await Device.isBiometricsEnabled()) &&
_129
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_129
? VaultType.InMemory
_129
: VaultType.DeviceSecurity;
_129
}
_129
_129
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_129
}
_129
_129
private async provisionBiometrics(): Promise<void> {
_129
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_129
try {
_129
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_129
} catch (error) {
_129
null;
_129
}
_129
}
_129
}
_129
_129
async enhanceVault(): Promise<void> {
_129
if (!(await this.vault.isEmpty())) {
_129
return;
_129
}
_129
_129
if (await Device.isSystemPasscodeSet()) {
_129
await this.updateUnlockMode('BiometricsWithPasscode');
_129
} else {
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
}
_129
}

The method does not enhance a vault that is in use.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_125
import { Injectable } from '@angular/core';
_125
import {
_125
BiometricPermissionState,
_125
BrowserVault,
_125
Device,
_125
DeviceSecurityType,
_125
IdentityVaultConfig,
_125
Vault,
_125
VaultType,
_125
} from '@ionic-enterprise/identity-vault';
_125
import { Session } from '../models/session';
_125
import { VaultFactory } from './vault.factory';
_125
import { Observable, Subject } from 'rxjs';
_125
_125
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_125
_125
@Injectable({
_125
providedIn: 'root',
_125
})
_125
export class SessionVaultService {
_125
private lockedSubject: Subject<boolean>;
_125
private vault: BrowserVault | Vault;
_125
_125
constructor() {
_125
this.vault = VaultFactory.create();
_125
this.lockedSubject = new Subject<boolean>();
_125
}
_125
_125
get locked$(): Observable<boolean> {
_125
return this.lockedSubject.asObservable();
_125
}
_125
_125
async initialize(): Promise<void> {
_125
try {
_125
await this.vault.initialize({
_125
key: 'io.ionic.gettingstartediv',
_125
type: VaultType.InMemory,
_125
deviceSecurityType: DeviceSecurityType.None,
_125
lockAfterBackgrounded: 30000,
_125
});
_125
} catch (e: unknown) {
_125
await this.vault.clear();
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
_125
await this.enhanceVault();
_125
_125
this.vault.onLock(() => this.lockedSubject.next(true));
_125
this.vault.onUnlock(() => this.lockedSubject.next(false));
_125
}
_125
_125
async storeSession(session: Session): Promise<void> {
_125
this.vault.setValue('session', session);
_125
}
_125
_125
async getSession(): Promise<Session | null> {
_125
if (await this.vault.isEmpty()) {
_125
return null;
_125
}
_125
return this.vault.getValue<Session>('session');
_125
}
_125
_125
async clearSession(): Promise<void> {
_125
await this.vault.clear();
_125
}
_125
_125
async lock(): Promise<void> {
_125
await this.vault.lock();
_125
}
_125
_125
async unlock(): Promise<void> {
_125
await this.vault.unlock();
_125
}
_125
_125
async isLocked(): Promise<boolean> {
_125
return (
_125
this.vault.config?.type !== VaultType.SecureStorage &&
_125
this.vault.config?.type !== VaultType.InMemory &&
_125
!(await this.vault.isEmpty()) &&
_125
(await this.vault.isLocked())
_125
);
_125
}
_125
_125
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_125
const type = await this.getVaultType(mode);
_125
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_125
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_125
await this.vault.updateConfig({
_125
...(this.vault.config as IdentityVaultConfig),
_125
type,
_125
deviceSecurityType,
_125
lockAfterBackgrounded,
_125
});
_125
}
_125
_125
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_125
if (mode === 'BiometricsWithPasscode') {
_125
await this.provisionBiometrics();
_125
return (await Device.isBiometricsEnabled()) &&
_125
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_125
? VaultType.InMemory
_125
: VaultType.DeviceSecurity;
_125
}
_125
_125
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_125
}
_125
_125
private async provisionBiometrics(): Promise<void> {
_125
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_125
try {
_125
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_125
} catch (error) {
_125
null;
_125
}
_125
}
_125
}
_125
_125
async enhanceVault(): Promise<void> {
_125
if (await Device.isSystemPasscodeSet()) {
_125
await this.updateUnlockMode('BiometricsWithPasscode');
_125
} else {
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
}
_125
}

Remove the isEmpty() check leaving the rest of the code in place.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_125
import { Injectable } from '@angular/core';
_125
import {
_125
BiometricPermissionState,
_125
BrowserVault,
_125
Device,
_125
DeviceSecurityType,
_125
IdentityVaultConfig,
_125
Vault,
_125
VaultType,
_125
} from '@ionic-enterprise/identity-vault';
_125
import { Session } from '../models/session';
_125
import { VaultFactory } from './vault.factory';
_125
import { Observable, Subject } from 'rxjs';
_125
_125
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_125
_125
@Injectable({
_125
providedIn: 'root',
_125
})
_125
export class SessionVaultService {
_125
private lockedSubject: Subject<boolean>;
_125
private vault: BrowserVault | Vault;
_125
_125
constructor() {
_125
this.vault = VaultFactory.create();
_125
this.lockedSubject = new Subject<boolean>();
_125
}
_125
_125
get locked$(): Observable<boolean> {
_125
return this.lockedSubject.asObservable();
_125
}
_125
_125
async initialize(): Promise<void> {
_125
try {
_125
await this.vault.initialize({
_125
key: 'io.ionic.gettingstartediv',
_125
type: VaultType.InMemory,
_125
deviceSecurityType: DeviceSecurityType.None,
_125
lockAfterBackgrounded: 30000,
_125
});
_125
} catch (e: unknown) {
_125
await this.vault.clear();
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
_125
await this.enhanceVault();
_125
_125
this.vault.onLock(() => this.lockedSubject.next(true));
_125
this.vault.onUnlock(() => this.lockedSubject.next(false));
_125
}
_125
_125
async storeSession(session: Session): Promise<void> {
_125
this.vault.setValue('session', session);
_125
}
_125
_125
async getSession(): Promise<Session | null> {
_125
if (await this.vault.isEmpty()) {
_125
return null;
_125
}
_125
return this.vault.getValue<Session>('session');
_125
}
_125
_125
async clearSession(): Promise<void> {
_125
await this.vault.clear();
_125
}
_125
_125
async lock(): Promise<void> {
_125
await this.vault.lock();
_125
}
_125
_125
async unlock(): Promise<void> {
_125
await this.vault.unlock();
_125
}
_125
_125
async isLocked(): Promise<boolean> {
_125
return (
_125
this.vault.config?.type !== VaultType.SecureStorage &&
_125
this.vault.config?.type !== VaultType.InMemory &&
_125
!(await this.vault.isEmpty()) &&
_125
(await this.vault.isLocked())
_125
);
_125
}
_125
_125
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_125
const type = await this.getVaultType(mode);
_125
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_125
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_125
await this.vault.updateConfig({
_125
...(this.vault.config as IdentityVaultConfig),
_125
type,
_125
deviceSecurityType,
_125
lockAfterBackgrounded,
_125
});
_125
}
_125
_125
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_125
if (mode === 'BiometricsWithPasscode') {
_125
await this.provisionBiometrics();
_125
return (await Device.isBiometricsEnabled()) &&
_125
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_125
? VaultType.InMemory
_125
: VaultType.DeviceSecurity;
_125
}
_125
_125
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_125
}
_125
_125
private async provisionBiometrics(): Promise<void> {
_125
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_125
try {
_125
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_125
} catch (error) {
_125
null;
_125
}
_125
}
_125
}
_125
_125
async enhanceVault(): Promise<void> {
_125
if (await Device.isSystemPasscodeSet()) {
_125
await this.updateUnlockMode('BiometricsWithPasscode');
_125
} else {
_125
await this.updateUnlockMode('InMemory');
_125
}
_125
}
_125
}

The enhanceVault() method is currently called from the initialize() method.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_127
import { Injectable } from '@angular/core';
_127
import {
_127
BiometricPermissionState,
_127
BrowserVault,
_127
Device,
_127
DeviceSecurityType,
_127
IdentityVaultConfig,
_127
Vault,
_127
VaultType,
_127
} from '@ionic-enterprise/identity-vault';
_127
import { Session } from '../models/session';
_127
import { VaultFactory } from './vault.factory';
_127
import { Observable, Subject } from 'rxjs';
_127
_127
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_127
_127
@Injectable({
_127
providedIn: 'root',
_127
})
_127
export class SessionVaultService {
_127
private lockedSubject: Subject<boolean>;
_127
private vault: BrowserVault | Vault;
_127
_127
constructor() {
_127
this.vault = VaultFactory.create();
_127
this.lockedSubject = new Subject<boolean>();
_127
}
_127
_127
get locked$(): Observable<boolean> {
_127
return this.lockedSubject.asObservable();
_127
}
_127
_127
async initialize(): Promise<void> {
_127
try {
_127
await this.vault.initialize({
_127
key: 'io.ionic.gettingstartediv',
_127
type: VaultType.InMemory,
_127
deviceSecurityType: DeviceSecurityType.None,
_127
lockAfterBackgrounded: 30000,
_127
});
_127
} catch (e: unknown) {
_127
await this.vault.clear();
_127
await this.updateUnlockMode('InMemory');
_127
}
_127
_127
this.vault.onLock(() => this.lockedSubject.next(true));
_127
this.vault.onUnlock(() => this.lockedSubject.next(false));
_127
}
_127
_127
async storeSession(session: Session): Promise<void> {
_127
this.vault.setValue('session', session);
_127
}
_127
_127
async getSession(): Promise<Session | null> {
_127
if (await this.vault.isEmpty()) {
_127
return null;
_127
}
_127
return this.vault.getValue<Session>('session');
_127
}
_127
_127
async clearSession(): Promise<void> {
_127
await this.vault.clear();
_127
}
_127
_127
async lock(): Promise<void> {
_127
await this.vault.lock();
_127
}
_127
_127
async unlock(): Promise<void> {
_127
await this.vault.unlock();
_127
}
_127
_127
async isLocked(): Promise<boolean> {
_127
return (
_127
this.vault.config?.type !== VaultType.SecureStorage &&
_127
this.vault.config?.type !== VaultType.InMemory &&
_127
!(await this.vault.isEmpty()) &&
_127
(await this.vault.isLocked())
_127
);
_127
}
_127
_127
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_127
const type = await this.getVaultType(mode);
_127
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_127
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_127
await this.vault.updateConfig({
_127
...(this.vault.config as IdentityVaultConfig),
_127
type,
_127
deviceSecurityType,
_127
lockAfterBackgrounded,
_127
});
_127
}
_127
_127
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_127
if (mode === 'BiometricsWithPasscode') {
_127
await this.provisionBiometrics();
_127
return (await Device.isBiometricsEnabled()) &&
_127
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_127
? VaultType.InMemory
_127
: VaultType.DeviceSecurity;
_127
}
_127
_127
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_127
}
_127
_127
private async provisionBiometrics(): Promise<void> {
_127
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_127
try {
_127
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_127
} catch (error) {
_127
null;
_127
}
_127
}
_127
}
_127
_127
async enhanceVault(): Promise<void> {
_127
if (!(await this.vault.isEmpty())) {
_127
return;
_127
}
_127
_127
if (await Device.isSystemPasscodeSet()) {
_127
await this.updateUnlockMode('BiometricsWithPasscode');
_127
} else {
_127
await this.updateUnlockMode('InMemory');
_127
}
_127
}
_127
}

Remove the call leaving the rest of initialize() in place.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_36
import { Component } from '@angular/core';
_36
import {
_36
IonButton,
_36
IonContent,
_36
IonHeader,
_36
IonItem,
_36
IonLabel,
_36
IonList,
_36
IonTitle,
_36
IonToolbar,
_36
NavController,
_36
} from '@ionic/angular/standalone';
_36
import { AuthenticationService } from '../core/authentication.service';
_36
_36
@Component({
_36
selector: 'app-login',
_36
templateUrl: './login.page.html',
_36
styleUrls: ['./login.page.scss'],
_36
standalone: true,
_36
imports: [IonButton, IonContent, IonHeader, IonItem, IonLabel, IonList, IonTitle, IonToolbar],
_36
})
_36
export class LoginPage {
_36
constructor(
_36
private navController: NavController,
_36
private authentication: AuthenticationService,
_36
) {}
_36
_36
async login() {
_36
try {
_36
await this.authentication.login();
_36
this.navController.navigateRoot(['tabs', 'tab1']);
_36
} catch (err: unknown) {
_36
console.error('Failed to log in', err);
_36
}
_36
}
_36
}

The login method currently logs the user in and navigates to the main page.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_39
import { Component } from '@angular/core';
_39
import {
_39
IonButton,
_39
IonContent,
_39
IonHeader,
_39
IonItem,
_39
IonLabel,
_39
IonList,
_39
IonTitle,
_39
IonToolbar,
_39
NavController,
_39
} from '@ionic/angular/standalone';
_39
import { AuthenticationService } from '../core/authentication.service';
_39
import { SessionVaultService } from '../core/session-vault.service';
_39
_39
@Component({
_39
selector: 'app-login',
_39
templateUrl: './login.page.html',
_39
styleUrls: ['./login.page.scss'],
_39
standalone: true,
_39
imports: [IonButton, IonContent, IonHeader, IonItem, IonLabel, IonList, IonTitle, IonToolbar],
_39
})
_39
export class LoginPage {
_39
constructor(
_39
private navController: NavController,
_39
private authentication: AuthenticationService,
_39
private sessionVault: SessionVaultService,
_39
) {}
_39
_39
async login() {
_39
try {
_39
await this.authentication.login();
_39
await this.sessionVault.enhanceVault();
_39
this.navController.navigateRoot(['tabs', 'tab1']);
_39
} catch (err: unknown) {
_39
console.error('Failed to log in', err);
_39
}
_39
}
_39
}

Enhance the vault as part of a successful login.

The enhanceVault() method is currently private.

Make it public.

The method does not enhance a vault that is in use.

Remove the isEmpty() check leaving the rest of the code in place.

The enhanceVault() method is currently called from the initialize() method.

Remove the call leaving the rest of initialize() in place.

The login method currently logs the user in and navigates to the main page.

Enhance the vault as part of a successful login.

src/app/core/session-vault.service.ts
src/app/login/login.page.ts

_129
import { Injectable } from '@angular/core';
_129
import {
_129
BiometricPermissionState,
_129
BrowserVault,
_129
Device,
_129
DeviceSecurityType,
_129
IdentityVaultConfig,
_129
Vault,
_129
VaultType,
_129
} from '@ionic-enterprise/identity-vault';
_129
import { Session } from '../models/session';
_129
import { VaultFactory } from './vault.factory';
_129
import { Observable, Subject } from 'rxjs';
_129
_129
export type UnlockMode = 'BiometricsWithPasscode' | 'InMemory' | 'SecureStorage';
_129
_129
@Injectable({
_129
providedIn: 'root',
_129
})
_129
export class SessionVaultService {
_129
private lockedSubject: Subject<boolean>;
_129
private vault: BrowserVault | Vault;
_129
_129
constructor() {
_129
this.vault = VaultFactory.create();
_129
this.lockedSubject = new Subject<boolean>();
_129
}
_129
_129
get locked$(): Observable<boolean> {
_129
return this.lockedSubject.asObservable();
_129
}
_129
_129
async initialize(): Promise<void> {
_129
try {
_129
await this.vault.initialize({
_129
key: 'io.ionic.gettingstartediv',
_129
type: VaultType.InMemory,
_129
deviceSecurityType: DeviceSecurityType.None,
_129
lockAfterBackgrounded: 30000,
_129
});
_129
} catch (e: unknown) {
_129
await this.vault.clear();
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
_129
await this.enhanceVault();
_129
_129
this.vault.onLock(() => this.lockedSubject.next(true));
_129
this.vault.onUnlock(() => this.lockedSubject.next(false));
_129
}
_129
_129
async storeSession(session: Session): Promise<void> {
_129
this.vault.setValue('session', session);
_129
}
_129
_129
async getSession(): Promise<Session | null> {
_129
if (await this.vault.isEmpty()) {
_129
return null;
_129
}
_129
return this.vault.getValue<Session>('session');
_129
}
_129
_129
async clearSession(): Promise<void> {
_129
await this.vault.clear();
_129
}
_129
_129
async lock(): Promise<void> {
_129
await this.vault.lock();
_129
}
_129
_129
async unlock(): Promise<void> {
_129
await this.vault.unlock();
_129
}
_129
_129
async isLocked(): Promise<boolean> {
_129
return (
_129
this.vault.config?.type !== VaultType.SecureStorage &&
_129
this.vault.config?.type !== VaultType.InMemory &&
_129
!(await this.vault.isEmpty()) &&
_129
(await this.vault.isLocked())
_129
);
_129
}
_129
_129
async updateUnlockMode(mode: UnlockMode): Promise<void> {
_129
const type = await this.getVaultType(mode);
_129
const deviceSecurityType = type === VaultType.DeviceSecurity ? DeviceSecurityType.Both : DeviceSecurityType.None;
_129
const lockAfterBackgrounded = type === VaultType.InMemory ? 30000 : 2000;
_129
await this.vault.updateConfig({
_129
...(this.vault.config as IdentityVaultConfig),
_129
type,
_129
deviceSecurityType,
_129
lockAfterBackgrounded,
_129
});
_129
}
_129
_129
private async getVaultType(mode: UnlockMode): Promise<VaultType> {
_129
if (mode === 'BiometricsWithPasscode') {
_129
await this.provisionBiometrics();
_129
return (await Device.isBiometricsEnabled()) &&
_129
(await Device.isBiometricsAllowed()) !== BiometricPermissionState.Granted
_129
? VaultType.InMemory
_129
: VaultType.DeviceSecurity;
_129
}
_129
_129
return mode === 'InMemory' ? VaultType.InMemory : VaultType.SecureStorage;
_129
}
_129
_129
private async provisionBiometrics(): Promise<void> {
_129
if ((await Device.isBiometricsAllowed()) === BiometricPermissionState.Prompt) {
_129
try {
_129
await Device.showBiometricPrompt({ iosBiometricsLocalizedReason: 'Please authenticate to continue' });
_129
} catch (error) {
_129
null;
_129
}
_129
}
_129
}
_129
_129
private async enhanceVault(): Promise<void> {
_129
if (!(await this.vault.isEmpty())) {
_129
return;
_129
}
_129
_129
if (await Device.isSystemPasscodeSet()) {
_129
await this.updateUnlockMode('BiometricsWithPasscode');
_129
} else {
_129
await this.updateUnlockMode('InMemory');
_129
}
_129
}
_129
}

The application now asks for Face ID permission only if needed and only after a successful login rather than doing so as part of the startup process. In your application you may want to tie this to something else such as setting a "Use Biometrics" preference toggle. The choice is up to you.

On the Tab1Page, the user can currently click the "Use Biometrics" button even if the user has rejected Face ID. According to the rules we are enforcing, though, we will end up using an "In Memory" vault in that case. We can enhance the "disable biometrics" code that we added earlier.

src/app/tab1/tab1.page.ts
src/app/tab1/tab1.page.html

_55
import { Component, OnInit } from '@angular/core';
_55
import {
_55
IonButton,
_55
IonContent,
_55
IonHeader,
_55
IonItem,
_55
IonLabel,
_55
IonList,
_55
IonTitle,
_55
IonToolbar,
_55
NavController,
_55
} from '@ionic/angular/standalone';
_55
import { SessionVaultService, UnlockMode } from '../core/session-vault.service';
_55
import { Session } from '../models/session';
_55
import { AuthenticationService } from '../core/authentication.service';
_55
import { BiometricPermissionState, Device } from '@ionic-enterprise/identity-vault';
_55
_55
@Component({
_55
selector: 'app-tab1',
_55
templateUrl: 'tab1.page.html',
_55
styleUrls: ['tab1.page.scss'],
_55
standalone: true,
_55
imports: [IonButton, IonContent, IonHeader, IonItem, IonLabel, IonList, IonTitle, IonToolbar, IonTitle],
_55
})
_55
export class Tab1Page implements OnInit {
_55
session: Session | null = null;
_55
disableBiometrics: boolean = false;
_55
_55
constructor(
_55
private authentication: AuthenticationService,

Next Steps

We have examined the Device API and several potential uses for it. Continue to explore how this API can be used to enhance the user experience within your application.