identity-guardian-logo

Mastering Identity Guardian's APIs

Daniel Neamtu -
12 MIN READ

Overview

Back in February of this year, we introduced our newest member of the MDNA Family, Identity Guardian. It provides a secure multi-factor login solution for both personal and shared devices, ensuring not only easy access but also security and privacy for the user. Identity Guardian leverages facial biometrics and supports SSO integration with various providers, including Microsoft and PingId. This allows users to easily access their applications after logging in.

In a shared device environment, user data is securely encrypted and encapsulated within a personal barcode, generated via facial recognition and can be easily discarded to erase personal data. On personally assigned devices, the user data is securely embedded within the Android framework, making it inaccessible even to the organization. On top of this, Identity Guardian offers Content Provider based APIs for applications to securely retrieve and share data between applications.

 

Working with the APIs

At the moment there are 6 available Delegation Scopes (URIs) and each of them is attributed to an API to be used: 

  • content://com.zebra.mdna.els.provider/previoussession
  • content://com.zebra.mdna.els.provider/currentsession
  • content://com.zebra.mdna.els.provider/lockscreenstatus/state
  • content://com.zebra.mdna.els.provider/lockscreenaction/startauthentication
  • content://com.zebra.mdna.els.provider/lockscreenaction/authenticationstatus
  • content://com.zebra.mdna.els.provider/lockscreenaction/logout

These APIs are working similarly like other of our solutions such as OEMInfo or Workstation Connect, meaning that you'll need your application to be whitelisted by the ZDM (Zebra Device Manager) and request approval for each one of the APIs you're planning to use through Delegation Scopes. These requests can be done with the help of the MX AccessMgr APIs through StageNow, via the EMDK or an EMM with an XML profile.

If you've never done this before, make sure you get yourself familiar at first with the AccessMgr CSP and how to deploy a profile via StageNow by setting up these params:

  • Operation Mode: "Single User without Whitelist"
  • Service Access Action: "Allow Caller to Call Service"
  • Service Identifier: <delegation scope of the API category>
  • Specify Caller Package Name: <enter app package name, e.g.: com.company.appname>
  • Caller Signature: <select signature file that contains the app certificate> 

The Caller Signature refers to the signature file (for example, Keystore) used to sign your application. This needs to be exported in either BASE64 or CERT format. If you haven't done this yet, we have a tool to assist you. You can follow this guide on how to use it.

If you're planning to self-grant your application access to one of these APIs via the EMDK like we're going to do in this guide, a profile inside the XML should look something similar like this: 


<characteristic type="Profile">
<parm name="ProfileName" value="IGCurrentSession" />
<parm name="ModifiedDate" value="2022-08-17 10:20:36" />
<parm name="TargetSystemVersion" value="10.4" />

<characteristic type="AccessMgr" version="10.4">
<parm name="emdk_name" value="" />
<parm name="ServiceAccessAction" value="4" />
<parm name="ServiceIdentifier" value="content://com.zebra.mdna.els.provider/currentsession" />
<parm name="CallerPackageName" value="com.zebra.nilac.igplayground" />
<parm name="CallerSignature"
value="MIIC5DCCAcwCAQEwDQYJKoZIhvcNAQEFBQAwNzEWMBQGA1UEAwwNQW5kcm9pZCBEZWJ1ZzEQMA4GA1UECgwHQW5kcm9pZDELMAkGA1UEBhMCVVMwIBcNMjIwNTEyMDg1NDA1WhgPMjA1MjA1MDQwODU0MDVaMDcxFjAUBgNVBAMMDUFuZHJvaWQgRGVidWcxEDAOBgNVBAoMB0FuZHJvaWQxCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqFON7xnLjUCBau4ZKgVlgN0jdP9JfcKE8nev7F5eD/OeLZOR/GCVzJrj29MohR2eonVDWM+kCdBkth8WbsMgc9oLIkdhq1OeOH2JjQRV38X4MQfR/ldz/NoVLPj9oyCNEBEvzCe1z9siHKNWpSqcZj6aimqpyHkBH+2mD9PKyt4a6520J+61E1MOJiS39Ch8pNxJsJ5c9/w1Hb2sURYLe33TPOZfhjcqh5BhNn+qVBoUvabcKuVxh+m0+ltaM1nHbFpKMa+foQVsbQB8wmLiB7F+yE2R0d4UmBqErAM/tQOKp0ZLu3L1jySbRLS1Sf+IbT8ymnirwcvMXC/KzQ/lFQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQCF+JRAC8kPuAJxIxVOxCLwcXS5FvwNwbgEvh8hEbAJwYYelN6weq9EmZurfSzGmxPkhSiqp6F9biTcHHUOKGR9Yty1uZkoRl1/+VLVzGrvPfdFwGoXXoSBPrx3Lj36RysZw0kwwJMD+5ovTzemsiVjm92YrAxFXO8XhXRVHGmncLRNi36Mzm6VdtnhkIKlALFLYvxHEQpghOw2K1Po5XJqFw5twQsv+snoFrjv+8f8MltoqEuVnUhP/NRAF1kUbt1IhgPzx0m5HXAHfl5S06p97UbIFtmvBNSFQyMMwoTXUvWcIuHPImcCDdFcB1g4j//TlznE8vgkpiCrQV/q2zb8" />
</characteristic>
</characteristic>
 

 

Initial requirements

Assuming you already have your project created, let's add the required dependencies before continuing. 

If you're not planning to use the EMDK in your project and prefer to use StageNow to grant your application access to the Identity Guardian APIs, you can freely skip this initial part.


// EMDK Loader
implementation 'com.github.nilac8991:emdk-loader-lib:1.0.8'

// EMDK
implementation 'com.symbol:emdk:11.0.134'
 

The EMDK Loader is a personal library which I've created to cover some logic related on how the EMDK library is initialized and how the profile is processed. I am going to use this library to cover the profile processing part but, if you don't want to use third party libraries in your project please refer here: EMDK Setup on how to add the EMDK library to your project and here: Use of ProfileManager on how to process profile configurations with ProfileManager.

To create an XML configuration containing the profiles that we're going to use for the APIs, you can use the EMDK Profile Manager Wizard Plugin for Android Studio which is going to guide you through all the mentioned steps defined earlier.

You can download the Plugin by checking the EMDK documentation here.

 

Now, if you're planning to use my library, let's initialize it inside our project before its future usage. Otherwise, please refer to the official documentation on how to process a profile mentioned earlier.


EMDKLoader.getInstance().initEMDKManager(this, object : EMDKManagerInitCallBack {
override fun onFailed(message: String) {
Log.e(TAG, "Failed to initialise EMDK Manager")
}

override fun onSuccess() {
Log.i(TAG, "EMDK Manager was successfully initialised")
}
})
 

 

Once the EMDK Manager is initialized, we can process any profile we want. To do this, we'll use the following snippet of code for each Identity Guardian API we plan to grant for our application to use:


ProfileLoader().processProfile(
"IGCurrentSession",
null,
object : ProfileLoaderResultCallback {
override fun onProfileLoadFailed(errorObject: EMDKResults) {
//Nothing to see here..
}

override fun onProfileLoadFailed(message: String) {
Log.e(TAG, "Failed to process profile")
}

override fun onProfileLoaded() {
getCurrentUserSession()
}
})
 

 

Last thing we need to do now is add a required permission in our Android Manifest together with a <queries> tag needed when we'll interrogate Identity Guardian through one of the APIs:


<uses-permission android:name="com.zebra.mdna.els.permission.PROVIDER" />
<queries>
<package android:name="com.zebra.mdna.els" />
</queries>

 

Retrieve current & previous user session

Assuming you've done everything correctly, let's start by retrieving the current session of a user:


private fun getCurrentUserSession() {
var userSession = ""

contentResolver.query(
Uri.parse(AppConstants.CURRENT_SESSION_URI),
null,
null,
null
).use {
if (it == null || it.columnCount == 0) {
Log.e(TAG, "App is not having permission for this API")
acquirePermissionForUserSession()
return
}

while (it.moveToNext()) {
for (i in 0 until it.columnCount) {
userSession = "$userSession\n${it.getColumnName(i)}: ${it.getString(i)}\n"
}
}
}
runOnUiThread {
binding.userSession.text = userSession
}
}

 

 

As we mentioned at the beginning, all the APIs are based on a content provider interface implementation, meaning that we'll always use the ContentResolver object in this case. 

And the Delegation Scope that we're going to call is this one:


const val CURRENT_SESSION_URI =
"content://com.zebra.mdna.els.provider/currentsession"
 

In this case, as soon as we obtain a reference to the cursor object, we immediately check to see if it's null or if it doesn't have any columns. These conditions might occur either because your application doesn't have authorization to use this API, indicating that something went wrong in the previous steps, or because Identity Guardian isn't installed on the device and there are no current active sessions.

Going further we iterate through all the available columns containing data about the user session, in this case we just want to pour all of them into one single string and show it as text inside our application.

At the moment of this writing there are in total 20 different params being mapped in this way which can be queried also individually:

  • id
  • user_id
  • user_role
  • security_types
  • storage_type
  • valid_through
  • barcode_id
  • signin_time
  • signout_time
  • signed_in_state
  • device_model
  • authentication_scheme
  • barcode_created_on
  • barcode_created_on_device_model
  • barcode_created_on_device_serial_no
  • barcode_authenticated_on_device_serial_no
  • sso_provider
  • sso_access_token
  • sso_data

If you want to get the value of a specific column, you'll have to use this syntax:


it.getString(it.getColumnIndex("Column Name"))
 

 

About retrieving a previous user session, it's going to be exactly the same, with the exception of the Uri which we're going to parse. In this case, make sure you have the right access to this one:


const val PREVIOUS_SESSION_URI =
"content://com.zebra.mdna.els.provider/previoussession"
 

 

Listening for incoming Lock/Unlock events

This API could be useful if you're planning to track every time a user successfully logs in with Identity Guardian or if the current user logs out. These are going to be events only, so the response that we're going to get won't contain any information about the user, unlike the previous two APIs. In this case, I would highly advise you to use these APIs within a Service and send notifications whenever there are new events to notify.

 

Assuming you have the right authorization to access this API and you have set up your Service, let's first register a ContentObserver, which we'll use later to listen for these kinds of events:


contentResolver.registerContentObserver(
Uri.parse(AppConstants.STATUS_AUTHENTICATION_URI),
false,
statusContentObserver
)

private val statusContentObserver = object : ContentObserver(Handler(Looper.myLooper()!!)) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
getStatusResponse()
}
}
 

 

Where the Delegation Scope which we're going to parse is this one so make sure you have the right access before using it: 


const val STATUS_AUTHENTICATION_URI =
"content://com.zebra.mdna.els.provider/lockscreenaction/authenticationstatus"

 

Now, let's move forward and talk a little bit about the response, which is going to be a stringified JSON containing 2 parameters:

  • state 
  • lastchangedtimestamp

The state param can be seen as an Enum because it will only have 2 values: "SHOWN" or "HIDDEN" and it will specify whenever a user is logging in or logging out successfully.

 


private fun getStatusResponse() {
val response = contentResolver.call(
Uri.parse(AppConstants.BASE_URI),
AppConstants.LOCKSCREEN_STATUS_ACTION,
AppConstants.LOCKSCREEN_STATUS_STATE_METHOD,
null
)

if (response != null && response.containsKey("RESULT")) {
Log.i(TAG, response.getString("RESULT")!!)
try {
val jsonObject = JSONObject(response.getString("RESULT")!!)

val state = jsonObject.getString("state")
val timestamp = jsonObject.getString("lastchangedtimestamp")

sendNewNotification(
"New Event", """
Type: $state
TimeStamp: $timestamp
""".trimIndent()
)

} catch (e: Exception) {
e.printStackTrace()
}
}
}
 

 

In the function that we're going to call from the ContentObserver, the first thing that we're going to do is to retrieve the response and  In this case, there's no need to grant any sort of permission since this isn't an actual API, but a method we can call when we already have access to this Delegation Scope: content://com.zebra.mdna.els.provider/lockscreenstatus/state


const val BASE_URI = "content://com.zebra.mdna.els.provider/"
const val LOCKSCREEN_STATUS_ACTION = "lockscreenstatus"
const val LOCKSCREEN_STATUS_STATE_METHOD = "state"
 

 

Next, as a precaution, we'll also check if the response object (which in this case will be a Bundle) is not null. We'll also check if the bundle contains a reference to a specific key called "RESULT". This will be the actual stringified JSON that we'll need to parse. Since the JSON is easy to parse, the next part should not pose any real difficulty. We'll put everything into a try-catch block and attempt to parse the string as a JSONObject. Later, we'll retrieve both parameters as mentioned before.

Depending on your app's usage, you can now do whatever you want with this information. You can notify the user, store it inside a database for later use, or even report this information to a backend so that these events are properly tracked by the customer.

 

Invoke a new authentication request and listen for response

Identity Guardian also offers a way for any application to trigger an authentication request. This means that whoever has been authenticated before will automatically be logged out after the successful login of a new user. Once the lockscreen is shown to the user, it will not be possible to go back, and a new user will be forced to authenticate. At the same time, once a new request is triggered, we can listen for the statuses coming from Identity Guardian, for example, if it's successful or if an unexpected issue has occurred.

Keep in mind that this is piloted by a single application and only one application can trigger a new authentication request and have the user logged in. If you try to do the same thing from a different application while a user has already been authenticated, a status BUSY will be returned to the application attempting to launch the request. To summarize, Identity Guardian can send 4 different statuses while a new authentication request has been launched:

  • IN_PROGRESS
  • BUSY
  • SUCCESS
  • ERROR

     

In this case, we'll combine 2 different Delegation Scopes to help us with the authentication request and with the listening of the changes while the user is logging in.

So please make sure you've access to the following before proceeding: 

  • content://com.zebra.mdna.els.provider/lockscreenaction/startauthentication
  • content://com.zebra.mdna.els.provider/lockscreenaction/authenticationstatus

 

To initiate a new authentication request, we must provide two specific parameters that are required:

  • user_verification
  • launchFlag

The user verification param stands for the authentication scheme we want to use for the request. Identity Guardian supports in total 4 different authentication schemes which can be configured from any EMM supporting Managed Configurations and you can check more about this here. And the only accepted values for this parameter are these ones:

  • authenticationScheme1
  • authenticationScheme2
  • authenticationScheme3
  • authenticationScheme4

The launch flag parameter is used for Identity Guardian to understand how to lockscreen should be viewed by the user once the authentication request is triggered. The only accepted values in this case are these ones:

  • blocking (Full immersive mode where NavBar & Status Bar are disabled)
  • unblocking

     

Now that we have all the information we need, we will invoke the ContentResolver object and call the following method, along with the two required parameters, as shown here:


val bundle = Bundle().apply {
putString("user_verification", binding.authenticationSchemeInput.text.toString())
putString("launchflag", binding.flagsInput.text.toString())
}

contentResolver.call(
Uri.parse(AppConstants.BASE_URI),
AppConstants.LOCKSCREEN_ACTION,
AppConstants.START_AUTHENTICATION_METHOD,
bundle
);
 

 


const val BASE_URI = "content://com.zebra.mdna.els.provider/"
const val LOCKSCREEN_ACTION = "lockscreenaction"
const val START_AUTHENTICATION_METHOD = "startauthentication"
 

 

By running this snippet of code under favorable conditions, we'll successfully trigger a new authentication request.

However, we know that this won't always go smoothly. Therefore, we need to monitor the situation. In this case, we will assign the ContentResolver call to a variable and track the status of the response, as shown below:


if (response == null || !response.containsKey("RESULT") || response.getString("RESULT") == "Caller is unauthorized") {
Log.e(TAG, "App is not having permission for this API")
return
} else if (response.containsKey("RESULT") && response.getString("RESULT") == "SUCCESS") {
Toast.makeText(
this,
"Session already in use, please log out first before creating a new request",
Toast.LENGTH_LONG
).show()
} else if (response.containsKey("RESULT") && response.getString("RESULT") == "Error:Cannot initiate as lock type is Device lock") {
Toast.makeText(
this,
"Unable to launch a new authentication request, please log out first",
Toast.LENGTH_LONG
).show()
} else {
Log.w(TAG, "${response.getString("RESULT")}")
}
 

 

In this case, we're checking the most common errors/statuses that could occur during the launch of the authentication request. This doesn't mean that everything is covered, so you'll have to adapt what you're getting from the response based on your application needs.

Finally, if we want to also get the user information as soon as they authenticate, we'll have to add these extra snippets of code:


contentResolver.registerContentObserver(
Uri.parse(AppConstants.STATUS_AUTHENTICATION_URI),
false,
authStatusContentObserver
)

 


const val STATUS_AUTHENTICATION_URI = "content://com.zebra.mdna.els.provider/lockscreenaction/authenticationstatus"
 

 


private val authStatusContentObserver = object : ContentObserver(Handler(Looper.myLooper()!!)) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
getAuthRequestStatus()
}
}
 

 


private fun getAuthRequestStatus() {
var response = ""

contentResolver.query(
Uri.parse(AppConstants.STATUS_AUTHENTICATION_URI),
null,
null,
null
).use {
if (it == null || it.columnCount == 0) {
Log.w(TAG, "Detected a new event but not triggered by our app, ignoring...")
return
}

while (it.moveToNext()) {
for (i in 0 until it.columnCount) {
response = "$response\n${it.getColumnName(i)}: ${it.getString(i)}\n"
}
}
dismissLoadingScreen()
binding.authRequestContainer.visibility = View.GONE
binding.userSession.text = response
}
Log.i(TAG, response)
}
 

 

What we've just done is similar to the Lock/Unlock events. We're registering a ContentObserver for that particular Delegation Scope and will continue listening until the new user authenticates, so we can get their details.

The params returned when querying with the cursor will be quite similar to those for the previous two APIs about user sessions. In particular, the list will look exactly the same, with the exception of one extra param: the application which triggered the authentication request:

  • application_id

 

Log out current user

Triggering a logout action for the current user is fairly simple; it's actually the easiest API to use among all the others.

Like with the other APIs, before launching the method, ensure you have access to this Delegation Scope: content://com.zebra.mdna.els.provider/lockscreenaction/logout

 

All we need to do now is to invoke the ContentResolver object and call this method so we can trigger the log out action.


contentResolver.call(
Uri.parse(AppConstants.BASE_URI),
AppConstants.LOCKSCREEN_ACTION,
AppConstants.LOGOUT_METHOD,
null
);
 

 


const val BASE_URI = "content://com.zebra.mdna.els.provider/"
const val LOCKSCREEN_STATUS_ACTION = "lockscreenstatus"
const val LOGOUT_METHOD = "logout"
 

 

That's it! You'll notice immediately if this works or not because if it does, you're supposed to see Identity Guardian's LockScreen. If it doesn't, please check if you have proper access to the above Delegation Scope.

Also, you can assign the ContentResolver call to a variable and track the status of the response, like this for example:


val response = contentResolver.call(
Uri.parse(AppConstants.BASE_URI),
AppConstants.LOCKSCREEN_ACTION,
AppConstants.LOGOUT_METHOD,
null
);

Log.w(TAG, "LOCK STATE: ${response?.getString("RESULT")}")
 

 

Conclusions

Identity Guardian is a new solution that Zebra has created for its customers. The option to integrate features such as APIs is a significant advantage, opening many opportunities for how Identity Guardian can be integrated within your suite of existing applications. Hopefully, this article helped you understand more about how you can take advantage of these APIs. If you're interested, you can also find the source code of the full sample project here in our ZebraDevs organization on GitHub.

Happy coding!

profile

Daniel Neamtu

Please register or login to post a reply

1 Replies

M Mike Dimmick

The content provider API is hard to use and not type-safe. A wrapper library would be appreciated. An Android Archive is likely to be consumable by almost any framework, and likely easier to target than the raw content provider.

Requiring apps to be whitelisted to use the content provider is compromised immediately by allowing the app to whitelist itself! You may as well not have any security if you permit that.

In the app I work on (M-Netics Retail), we use the device serial number to correlate messages sent to the server to the actual device, in case intervention is required on the device itself using Remote Management tools. I haven't implemented whitelisting that content provider in the app itself, on the expectation that you would close the loophole. However, I'm aware that we have done so in our M-Netics ePOD app.

The serial number provider takes a long time to initialise after a device reboot - in my experience up to 50 seconds. Is there a similar delay in using the Identity Guardian content provider?