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/angular
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.npm cap sync
: Copy the web app and any new plugins into the native applications.npm cap open android
: Open Android Studio in order to build, run, and debug the application on Android.npm cap open ios
: Open Xcode in order to build, run, and debug the application on iOS.
Let's get started.
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.
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.
Install Identity Vault
In order to install Identity Vault, you will need to use ionic enterprise register
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:
Create the SessionVaultService
Our tutorial application will have a single vault that simulates storing our application's authentication session
information. The vault is managed via our SessionVaultService
. Generate that now.
Also create a simple "Vault Factory" class.
The purpose of this class is two-fold:
- It hides the fact that a different vault is used on native vs. browser platforms.
- It facilitates mocking the vault, which makes unit testing easier.
Create and Initialize 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 service 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 using the APP_INITIALIZER and await its completion.
Awaiting the completion of initialization in this manner is a best-practice that should always be followed.
We will build this service up to perform the vault creation and initialization.
Create the vault using our factory class.
Create an initialize()
method 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 src/main.ts
use an APP_INITIALIZER to make sure our vault is
fully initialized on startup before the AppComponent
is mounted.
We will build this service up to perform the vault creation and initialization.
Create the vault using our factory class.
Create an initialize()
method 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 src/main.ts
use an APP_INITIALIZER to make sure our vault is
fully initialized on startup before the AppComponent
is mounted.
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:
- Define our session information.
- Add a method to
SessionVaultService
to store a session. - Add a button to
Tab1Page
to store a fake session.
First, let's define the shape of our authentication session data via:
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/app/core/session-vault.service.ts
to store the session.
Notice that we have created a very light wrapper around the vault's setValue()
method. This is often all that is
required. You may be tempted to just make the SessionVaultService
's vault
property public and then directly use
the Identity Vault methods directly on the vault. It is best-practice, however, to encapsulate the vault in a service
like this one and only expose the functionality that makes sense for your application.
With the "store session" feature properly abstracted, add method properly dd a button to the Tab1Page
that will
simulate logging in by storing some fake authentication data in the vault.
We are currently displaying the generic starter "Explore Container" data.
Replace the explore container with a list containing a button.
Import the Ionic components that we added to the page template.
Inject the vault service.
Store some made up test data.
We are currently displaying the generic starter "Explore Container" data.
Replace the explore container with a list containing a button.
Import the Ionic components that we added to the page template.
Inject the vault service.
Store some made up test data.
We have stored data in our vault. The next step is to get the data back out of the vault.
Get a Value
The first step is to add a method to our SessionVaultService
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.
In order to better illustrate the operation of the vault, we will modify the Tab1Page
to display our session if one
is stored.
The Tab1Page
currently stores the session information.
Add a session
property.
Get the session when the page is initialized.
Also get the session immediately after it is stored.
In the page's template, add a div
to display the session
.
The Tab1Page
currently stores the session information.
Add a session
property.
Get the session when the page is initialized.
Also get the session immediately after it is stored.
In the page's template, add a div
to display the session
.
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. In 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 vault 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.
Modify src/app/tab1/tab1.page.ts
and src/app/tab1/tab1.page.html
to have a "Clear" button.
Update 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..
Here is the src/app/core/session-vault.service.ts
that we have created thus far.
The UnlockMode
specifies the logical combinations of settings we wish to support within our application.
Add an updateUnlockMode()
method to the class. Take a single argument for the mode.
The vault's updateConfig()
method takes a full vault configuration object, so pass our current config
. Cast it
to IdentityVaultConfig
to signify that we know the value is not undefined
at this point.
Update the type
based on the specified mode
.
Update the deviceSecurityType
based on the value of the type
.
Update the deviceSecurityType
based on the value of the type
.
Here is the src/app/core/session-vault.service.ts
that we have created thus far.
The UnlockMode
specifies the logical combinations of settings we wish to support within our application.
Add an updateUnlockMode()
method to the class. Take a single argument for the mode.
The vault's updateConfig()
method takes a full vault configuration object, so pass our current config
. Cast it
to IdentityVaultConfig
to signify that we know the value is not undefined
at this point.
Update the type
based on the specified mode
.
Update the deviceSecurityType
based on the value of the type
.
Update the deviceSecurityType
based on the value of the type
.
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:
- 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.
- 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 Tab1Page
We can now add some buttons to the Tab1Page
in order to try out the different vault types. Update the
src/app/tab1/tab1.page.ts
and src/app/tab1/tab1.page.html
as shown below.
Build the application and run it on a device upon which you have biometrics enabled. Perform the following steps for each type of vault:
- Press the "Store" button to put data in the vault.
- Choose a vault type via one of the "Use" buttons.
- Close the application (do not just put it in the background, but close it).
- 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
.
Biometrics should work on the iPhone at this point.
Lock and Unlock 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/app/core/session-vault.service.ts
, wrap the vault's lock()
method so we can use it in our Tab1Page
.
Add a lock button in src/app/tab1/tab1.page.ts
and src/app/tab1/tab1.page.html
.
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 In 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()
method. When it calls the
vault's getValue()
, the getValue()
will attempt to unlock the vault.
Add the following code to src/app/tab1/tab1.page.ts
and src/app/tab1/tab1.page.html
:
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 when we initialize the vault. This will lock the vault when the application is resumed if the app was backgrounded for the configured amount of time. Here we are setting it to 2000 milliseconds.
If you now switch the app to use a mode that locks, like Biometrics, and then put the app in the background for two seconds or more, the vault will lock even though you won't really know it.
One way to deal with this is to create an Observable
in our service that indicates if the vault is currently locked
or unlocked. We can then subscribe to that Observable
in our Tab1Page
and remove the session data from our page when
the vault locks.
Here are our SessionVaultService
and Tab1Page
classes so far.
Create a private Subject (lockedSubject
) and expose it publicly as an Observable (locked$
).
Emit the lock status using the onLock
and onUnlock
events. Never access the vault in pause
or resume
events. Always use
the onLock
and onUnlock
events to control interactions with the vault upon lock or unlock.
Subscribe to the Observable in the Tab1Page
and clear page's session data if the vault locks.
For proper housekeeping, we should save a reference to the subscription so we can unsubscribe when the page is destroyed.
Here are our SessionVaultService
and Tab1Page
classes so far.
Create a private Subject (lockedSubject
) and expose it publicly as an Observable (locked$
).
Emit the lock status using the onLock
and onUnlock
events. Never access the vault in pause
or resume
events. Always use
the onLock
and onUnlock
events to control interactions with the vault upon lock or unlock.
Subscribe to the Observable in the Tab1Page
and clear page's session data if the vault locks.
For proper housekeeping, we should save a reference to the subscription so we can unsubscribe when the page is destroyed.
Architectural Considerations
Construction vs. Initialization
Have a look at the src/app/core/session-vault.service.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. This is bad.
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 via the APP_INITIALIZER and make sure that the code is properly
await
ing its completion.
Control Unlocking on Startup and Navigation
Our code is currently automatically unlocking the vault upon startup due to the code in ngOnInit()
. 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. For example if we have AuthGuards and HTTP Interceptors also trying to access the vault
at the same time. 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.
If you are using lockAfterBackgrounded
do not interact with the vault, directly or indirectly, in a resume
event handler. Doing so will cause a race condition resulting in indeterminate behavior. Always manage the state
of the vault through the onLock
and onUnlock
event handlers, never the resume
event.
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:
_10await this.vault.initialize({_10 key: 'io.ionic.gettingstartediv',_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:
- What type of data is stored?
- 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.