Authentication Modes and Biometry
Authentication Modes
For a KOBIL Digitanium+ environment the LoginWithTokenEvent takes an AuthenticationMode as parameter. Similarly, for a KOBIL Shift Lite environment, the SetAuthorisationCodeEvent takes this parameter.
The AuthenticationMode can take one of the following three values:
AuthenticationMode.NO
AuthenticationMode.BIOMETRIC
AuthenticationMode.PASSWORD
- when using AuthenticationMode.NO, the MC will save the token data when triggering a LoginWithTokenEvent with full parameters (accessToken and authCode) and it will try to use the saved token data to perform subsequent Logins when triggering a LoginWithTokenEvent without accessToken and authCode.
- when using AuthenticationMode.BIOMETRIC, the MC will add another layer of protection to the saved token data and will need a successful biometric authentication before perform a login.
Note: If opting to use the biometric authentication mode on Android you need to add androidx.biometric:biometric-ktx dependency to your Android app:
dependencies {
...
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha03'
...
}
Note: The authentication mode can be switched at any time, except during an ongoing activation. Starting with MC SDK 14.1 users can temporarily switch from AuthenticationMode.BIOMETRIC to AuthenticationMode.PASSWORD. In all versions prior to 14.1 switching from AuthenticationMode.BIOMETRIC to AuthenticationMode.PASSWORD results in loosing the tokens of the previously enabled biometric login, this means users have to setup biometry anew if they choose to switch back to AuthenticationMode.BIOMETRIC afterwards.
Biometry Error Handling
When using AuthenticationMode.BIOMETRIC for authentication on a KOBIL Digitanium+ or KOBIL Shift Lite environment you will have to handle biometry related results and error codes. The SDK passes down the default codes of the used operating system with a leading 3 digit number. After removing the leading 3 digits you can handle as you would do when directly working with the corresponding OS API. Depending on your needs you can assign the error code to different app handling
- Biometry canceled (by user or operating system): App can decide to retry.
- Biometry failed (detection not possible): App should ask to retry.
- Biometry failed (perhaps an attack): App can decide to retry (but countermeasures against attacks might be needed).
- Biometry currently not available: App should ask the user to setup the biometry on operation system.
- Biometry not available: Supported Biometry technics was not available on this device, app has to disable biometry.
There is a flow in iOS that developers need to handle with a biometry prompt. When the native biometric prompt gets interrupted by a call or putting an application in the background. This makes OfflineLoginEvent or SetAuthorizationCodeEvent gets failed and a developer will receive one of the canceled error codes:
- userCanceled = 128
- userCanceledSE = 2
- systemCanceled = 4
- appCanceled = 9
When the developer receives those errors he should restart the process.
iOS/Swift
Please check the origin source files in the delivered full functional GettingStartedApp as an example implmentation and the Biometry Error Codes iOS to a fully covered error handling according your needs.
private func handleOfflineLoginresult(userIdentifier: KsUserIdentifier, resultEvent: KSMOfflineLoginResultEvent){
switch resultEvent.status {
case .KSMOK:
DispatchQueue.global(qos: .background).async {
self.triggerGetInformation()
}
DispatchQueue.main.async {
InterfaceDelegate.sharedInterface.showTabBar()
}
default:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
let errorCode = resultEvent.errorCode - 802000000
if let biometricError = BiometricError(rawValue: errorCode) {
guard biometricError != .userCanceled,
biometricError != .userCanceledSE,
biometricError != .appCanceled,
biometricError != .systemCanceled else {
BiometryHelper.showDisableBiometryAlert() { [weak self] in
self?.restartLogin()
}
return
}
AlertPresenter().showAlertWith(
title: biometricError.title(),
message: biometricError.errorDescription(),
completion: nil
)
}
self.restartLogin()
}
}
}
...
enum BiometricError: Int {
case authFailed = 25293
case duplicateItem = 25299
case param = 50
case publicKeyInconsistent = 67811
case userCanceled = 128
case userCanceledSE = 2
case systemCanceled = 4
case appCanceled = 9
case biometryLockout = 8
func stringValue() -> String {
return "\(self)"
}
func title() -> String {
return "OSStatus: \(self.rawValue * -1)"
}
func errorDescription() -> String {
switch self {
case .authFailed:
return "AuthFailed (\(self.rawValue)) Authorization and/or authentication failed."
case .param:
return "Param (\(self.rawValue)) One or more parameters passed to the function are not valid."
case .publicKeyInconsistent:
return "PublicKeyInconsistent (\(self.rawValue)) Fingerprint is not configured. Do you want to enroll biometry now, or change authentication mode to password?"
case .userCanceled:
return "UserCanceled (\(self.rawValue)) User canceled the operation."
case .duplicateItem:
return "The item already exists"
case .userCanceledSE:
return "Authentication canceled (\(self.rawValue)) User canceled the operation."
case .systemCanceled:
return "Authentication canceled (\(self.rawValue)) The system canceled authentication."
case .appCanceled:
return "Authentication canceled (\(self.rawValue)) The app canceled authentication."
case .biometryLockout:
return "Biometry lockout (\(self.rawValue)) Biometry is locked because there were too many failed attempts."
}
}
}
...
func setAuthorizationResult(result: KSMSetAuthorisationCodeResultEvent) {
if result.status == .KSMOK {
DispatchQueue.global(qos: .background).async {
self.viewModel?.triggerGetInformation()
}
GlobalConstant.const.logedInUser = KsUserIdentifier(tenantId: GlobalConstant.const.tennatId, userId: "")
DispatchQueue.main.async {
InterfaceDelegate.sharedInterface.showTabBar()
}
} else {
let errorCode = result.errorCode - 802000000
if let biometricError = BiometricError(rawValue: errorCode) {
AlertPresenter().showAlertWith(title: biometricError.title(), message: biometricError.errorDescription(), completion: nil)
} else {
let error = ActivationLoginErrorHelper.getActivationLoginErrorForKSMEventStatusType(status: result.status)
AlertPresenter().showAlertWith(title: "KSMSetAuthorisationCodeResultEvent", message: error!.description, completion: nil)
}
viewModel?.getAstClientData(userIdentifer: userIdentifier, userType: userType)
}
}
Screenshots of this code:
Android/Kotlin
Please check the origin source files in the delivered full functional GettingStartedApp as an example implmentation and the Biometry Error Codes Android to a fully covered error handling according your your needs
/**
Biometry errors will be returned with the result of failed logins
(for example SetAuthorisationCodeResultEvent or OfflineLoginResultEvent with StatusType == FAILED).
Because the error is returned by MC they will be delivered trough a SubSystem of MC.
Biometric errors will be returned as an error of Subsystem 802, meaning that
for example androidx.biometric.BiometricPrompt.ERROR_LOCKOUT (7) will be returned as 802000007.
Because biometric errors will never arrive trough another SubSystem
it is safe to just subtract 802000000 from the received code in order to
get the corresponding value of androidx.biometric.BiometricPrompt.
Example when MC returns 802000007:
802000007 - 802000000 = 7 = androidx.biometric.BiometricPrompt.ERROR_LOCKOUT
*/
// or alternatively we can just take the last two digits of the error code and handle it as androidx.biometric.BiometricPrompt error
val biometryError = eventFrameworkEvent.errorCode.toString().takeLast(2).toInt()
when (biometryError) {
BiometricPrompt.ERROR_LOCKOUT -> {
...
// Too many failed attempts! Biometry locked! You can either retry with Biometry by unlocking it with your device PIN, or temporarily switch to authentication mode password."
...
}
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_CANCELED -> {
...
// Biometry prompt was canceled. Do you want to change authentication mode to password?"
...
}
BiometricPrompt.ERROR_NO_BIOMETRICS -> {
...
// Fingerprint is not configured. Do you want to enroll biometry now, or use authentication mode password?"
...
}
BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> {
...
// Too many failed attempts! Biometry locked! You can either retry with Biometry by unlocking it with your device PIN, or switch to authentication mode password."
...
}
BiometricPrompt.ERROR_VENDOR -> {
...
// Manufacturer specific error. Do you want to change authentication mode to password?",
...
}
}
Android recommendation for handling biometric lockout errors
On some devices when biometry is locked the user has to provide another strong authentication before they are able to use biometry again in any app, or even for unlocking the phone. This might lead to inconsistent behavior or difficulties in handling repeated biometric failures in certain use cases. Our suggestion to avoid any potential issues with ERROR_LOCKOUT cases is to:
- Allow the user to temporarily fall back to AuthMode.Password (IDP login via web view).
- Allow the user to unlock biometry by asking them to provide the device PIN/Pattern.
Example Flow:
- Biometry is locked after 5 failed attempts.
- User can choose to either fall back to AuthMode.Password or unlock the use of Biometry via device PIN/Pattern.
- User chooses to provide their device PIN to unlock the use of Biometry.
- After unlocking via PIN the user can try Biometry again.
Below is a snippet that shows the use of KeyguardManager to ask for device PIN/Pattern
override fun askForPinOrPassword() {
val km = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
if (km.isKeyguardSecure) {
val authIntent = km.createConfirmDeviceCredentialIntent(
"Biometry Locked",
"Too many failed fingerprint/faceId attempts! " +
"Provide your credentials to unlock fingerprint/faceId for further use."
)
startActivityForResult(authIntent, INTENT_AUTHENTICATE_REQUEST_CODE)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == INTENT_AUTHENTICATE_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
// Device PIN success - Biometry can be used again
} else {
...
}
}
}