The OneLogin MFA SDK is a Android library for registering, displaying,
and using Time Based One Time Passwords (TOTP), which are commonly used
for two factor authentication. A TOTP is commonly referred to below as a
Factor
. The framework supports all factors that adhere to the standard
defined in RFC 6238. In addition,
OneLogin's own factor type, which is normally used in the Protect
application, is
also supported. OneLogin's factor type also extends TOTP to include
additional security and functional aspects (configurable by the OneLogin
administrator), such as rooted device detection and keyguard
requirements.
Table of Contents
To install the library add the following dependency to the build.gradle
file of your module:
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
}
implementation("com.onelogin:onelogin-mfa-android-sdk:1.0.2")
If you choose to utilize our provided QR Scanner, the
camera permission must be added to your AndroidManifest.xml
:
<uses-permission android:name="android.permission.CAMERA"/>
In order to use MFA client, the library needs to be initialized first,
which would normally happen on the onCreate()
method of your
application.
The first step is to create a MfaConfiguration
instance using the
builder:
// Kotlin
OneLoginMfa.initialize(
this,
MfaConfiguration.Builder().isDebug(true).build()
)
// Java
MfaConfiguration mfaConfig = new MfaConfiguration.Builder().isDebug(true).build();
OneLoginMfa.initialize(this, mfaConfig);
Setting isDebug()
to true
will enable Timber logging. Default is
false
.
This initialization only needs to occur one time. After this, an
instance of the MfaClient
can be retrieved by calling
OneLoginMfa.getClient()
. The client is a singleton that can be used to
perform any of the supported MfaClient
operations.
The MfaClient
contains all of the methods which can be used to perform
the supported MFA operations.
Both OneLogin and third party factors are represented by the class
Factor
. The model class is:
data class Factor(
var id: Long, // ID generated by Room
var credentialId: String?, // ID generated by OneLogin
var subdomain: String?, // Subdomain that factor is registered to
var shard: String?,
var username: String?, // Account associated with factor
var seed: String, // Seed used to generate TOTP
var issuer: String?, // Issuer of the factor
var creationDate: Long, // Date factor was added
var allowRoot: Boolean, // Device setting from OneLogin to allow rooted devices, defaults true
var forceLock: Boolean, // Device setting from OneLogin to require keyguard, defaults false
var paired: Boolean, // Device setting from OneLogin to determine if factor is still paired to account
var displayName: String?,
var orderPreference: Int, // Order preference for sorting of factors
var crypto: String, // Defaults to HmacSHA1
var period: Int, // TOTP validity period
var digits: Int, // TOTP digits
// Not yet supported
var allowBackup: Boolean, // Device setting from OneLogin to allow for backup, defaults to true
// Not yet supported
var requireBiometrics: Boolean // Device setting to require biometric verification to view OTP, defaults to false
)
The MfaCallback
interface will be used to provide the results for most
of the operations, this interface has two methods that should be
overridden to process the results:
-
onSuccess(T: Success)
This method will be called when the operation completes successfully and will contain the relevant information of the result. -
onError(T: Error)
This method will be called when there is an error on the operation and will return aThrowable
with the information related to the error.
For all exceptions that occur within the OneLoginMfa library, a
OneLoginMfaException
will be thrown. The custom exception class is as
follows:
data class OneLoginMfaException(
override val message: String?,
override val cause: Throwable? = null,
val code: Int? = null,
val errorDescription: String? = null
) : Exception(message, cause)
All registered factors are encrypted and saved in a Room database provided by the OneLoginMfa library.
There are three methods to register a Factor
. Whichever method you
choose will depend on the type of factor you are going to use. Upon a
successful factor registration attempt, a RegisterFactorSuccess
object
will be returned:
data class RegisterFactorSuccess(
val id: Long // ID generated by Room
)
You can use this id
to perform operations on the factor.
This can be used to register a Factor
using a otpauth://
link
directly. You can either provide the link to the framework or use the
layout resources provided to let your
users enter the code manually or scan a QR-Code containing the link. To
register without user interaction:
// Kotlin
mfaClient.registerFactor(
code,
object : MfaCallback<RegisterFactorSuccess, RegisterFactorError> {
override fun onSuccess(success: RegisterFactorSuccess) {
Timber.d("Successfully added factor")
}
override fun onError(error: RegisterFactorError) {
Timber.d("Error adding factor")
}
})
// Java
mfaClient.registerFactor(code, new MfaCallback<RegisterFactorSuccess, RegisterFactorError>() {
@Override
public void onSuccess(@NonNull RegisterFactorSuccess success) {
Timber.d("Successfully added factor")
}
@Override
public void onError(@NonNull RegisterFactorError error) {
Timber.d("Error adding factor");
}
});
This method can only be used for OneLogin factors. The account must be configured by the administrator to require OTP authentication and users without a MFA device must register before being able to log in. This will only allow the user to register if they do not have any current OneLogin Protect factors already existing, otherwise an exception will be thrown.
The variables subdomain
, username
, and password
are required. To
register:
// Kotlin
mfaClient.registerFactorByWebLogin(subdomain, username, password,
object : MfaCallback<RegisterFactorSuccess, RegisterFactorError> {
override fun onSuccess(success: RegisterFactorSuccess) {
Timber.d("Successfully added OneLogin factor via web login")
}
override fun onError(error: RegisterFactorError) {
Timber.d("Error adding OneLogin factor via web login")
}
})
// Java
mfaClient.registerFactorByWebLogin(subdomain, username, password,
new MfaCallback<RegisterFactorSuccess, RegisterFactorError>() {
@Override
public void onSuccess(@NonNull RegisterFactorSuccess success) {
Timber.d("Successfully added OneLogin factor via web login");
}
@Override
public void onError(@NonNull RegisterFactorError error) {
Timber.d("Error adding OneLogin factor via web login");
}
});
Registering a factor via OIDC is not yet supported.
There are three methods to retrieve registered factors.
Get factor(s):
// Kotlin
mfaClient.getFactors(object : MfaCallback<List<Factor>, Exception> {
override fun onSuccess(success: List<Factor>) {
for (factor in success) {
Timber.d(factor)
}
}
override fun onError(error: Exception) {
Timber.d("Failed to retrieve factors")
}
})
// Java
mfaClient.getFactors(new MfaCallback<List<Factor>, Exception>() {
@Override
public void onSuccess(@NonNull List<Factor> success) {
for (int i = 0; i < success.size(); i++) {
Timber.d(factor);
}
}
@Override
public void onError(@NonNull Exception error) {
Timber.d("Failed to retrieve factors");
}
});
Get factor by ID (this ID is generated by Room):
// Kotlin
mfaClient.getFactorById(id: Long, object : MfaCallback<Factor, Exception> {
override fun onSuccess(success: Factor) {
Timber.d("Retrieved factor: $success")
}
override fun onError(error: Exception) {
Timber.d("Failed to retrieve factor")
}
})
// Java
mfaClient.getFactorById(Long id, new MfaCallback<Factor, Exception>() {
@Override
public void onSuccess(@NonNull Factor success) {
Timber.d("Retrieved factor: $success");
}
@Override
public void onError(@NonNull Exception error) {
Timber.d("Failed to retrieve factor");
}
});
Get factor by credential ID (can only be used for OneLogin factors):
// Kotlin
mfaClient.getFactorByCredentialId(credentialId: String, object : MfaCallback<Factor, Exception> {
override fun onSuccess(success: Factor) {
Timber.d("Retrieved factor: $success")
}
override fun onError(error: Exception) {
Timber.d("Failed to retrieve factor")
}
})
// Java
mfaClient.getFactorByCredentialId(String credentialId, new MfaCallback<Factor, Exception>() {
@Override
public void onSuccess(@NonNull Factor success) {
Timber.d("Retrieved factor: $success");
}
@Override
public void onError(@NonNull Exception error) {
Timber.d("Failed to retrieve factor");
}
});
There are three methods to delete registered factors. Note that calling these methods will not remove your factor within your OneLogin account.
Delete factor(s):
// Kotlin
mfaClient.removeAllFactors(object : MfaCallback<List<Factor>, Exception> {
override fun onSuccess(success: Int) {
Timber.d("Removed all factors")
}
override fun onError(error: Exception) {
Timber.d("Failed to remove all factors")
}
})
// Java
mfaClient.removeAllFactors(new MfaCallback<List<Factor>, Exception>() {
@Override
public void onSuccess(@NonNull List<Factor> success) {
Timber.d("Removed all factors");
}
@Override void onError(@NonNull Exception error)
Timber.d("Failed to remove all factors");
}
});
Delete factor by passing Factor
:
// Kotlin
mfaClient.removeFactor(factor: Factor, object : MfaCallback<Int, Exception> {
override fun onSuccess(success: Factor) {
Timber.d("Removed factor")
}
override fun onError(error: Exception) {
Timber.d("Failed to remove factor")
}
})
// Java
mfaClient.removeFactor(factor, new MfaCallback<Integer, Exception>() {
@Override
public void onSuccess(Integer integer) {
Timber.d("Removed factor");
}
@Override
public void onError(Exception e) {
Timber.d("Failed to remove factor");
}
});
Delete factor by credential ID (can only be used for OneLogin factors):
// Kotlin
mfaClient.removeFactorByCredentialId(credentialId: String, object : MfaCallback<Int, Exception> {
override fun onSuccess(success: Factor) {
Timber.d("Removed factor")
}
override fun onError(error: Exception) {
Timber.d("Failed to remove factor")
}
})
// Java
mfaClient.removeFactor(String credentialId, new MfaCallback<Integer, Exception>() {
@Override
public void onSuccess(Integer integer) {
Timber.d("Removed factor");
}
@Override
public void onError(Exception e) {
Timber.d("Failed to remove factor");
}
});
This method should be called if you have any OneLogin factors registered in your app. This is necessary to retrieve any updated device settings or notification of a removed factor from your OneLogin account. If any settings need to be updated, the method will handle updating it within the local database. If a factor has been removed from your OneLogin account, this method will delete the factor from the local database. Ideally, this should be called before retrieving your factors for display.
To refresh factor(s):
// Kotlin
mfaClient.refreshFactors(object : MfaCallback<RefreshFactorsSuccess, Exception> {
override fun onSuccess(success: RefreshFactorsSuccess) {
Timber.d("Refreshed factors")
}
override fun onError(error: Exception) {
Timber.d("Failed to refresh factors")
}
})
// Java
mfaClient.refreshFactors(new MfaCallback<RefreshFactorsSuccess, Exception>() {
@Override
public void onSuccess(@NonNull RefreshFactorsSuccess success) {
Timber.d("Refreshed factors");
}
@Override
public void onError(@NonNull Exception error) {
Timber.d("Failed to refresh factors");
}
});
The RefreshFactorsSuccess
object that gets returned consists of:
data class RefreshFactorsSuccess(
var unpairedCount: Int = 0, // number of factors removed from OneLogin account
var unpairedFactors: ArrayList<Factor> = arrayListOf(), // list of unpaired Factors
var updatedCount: Int = 0, // number of factors that had updated device settings
var updatedFactors: ArrayList<Factor> = arrayListOf(), // list of affected factors with updated device settings
)
To retrieve a OTP from a registered factor, you can display it to the user with the provided OTP View or retrieve it yourself. Retrieve a factor's OTP by:
// Kotlin
val otpCode = myFactor.getOtp()
// Java
String otpCode = myFactor.getOtp();
We've added customizable layout resources that are currently existing in
our Protect application to allow for easier development. Using these
layouts is not required to call the provided methods within the
MfaClient
.
This is a custom view that handles displaying the OTP code for a factor. It also will update based on the device settings that are retrieved from your OneLogin account. A user can also tap on the code to copy it directly to their clip board.
To add the Otp
View to your layout:
<com.onelogin.mfa.view.Otp
android:id="@+id/my_otp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:code_color="@color/primary_blue"
app:code_size="50sp"
app:enable_tap_to_copy="true"
app:enable_show_toast_on_copy="true" />
Then in your activity/fragment:
// Kotlin
val otpCodeView: Otp by lazy { findViewById(R.id.my_otp) }
otpCodeView.setFactor(myFactor)
// Java
Otp otpCodeView = findViewById(R.id.my_otp);
otpCodeView.setFactor(myFactor);
These are the custom attributes available for the Otp
View:
Attribute | Format | Default Value | Purpose |
---|---|---|---|
code |
string | "" | Set code without using setFactor(factor: Factor) |
code_color |
color | Default text color | Set color for OTP text |
code_size |
dimension | Default text size | Set size for OTP text |
enable_tap_to_copy |
boolean | true | Allow user to tap on the OTP text & copy to clipboard |
enable_show_toast_on_copy |
boolean | true | Show toast message when user taps on OTP to copy code |
enable_warning_icon |
boolean | true | Show warning icon when OTP can't be displayed because of OneLogin device settings |
warning_icon_size |
dimension | 18dp | Set size for warning icon |
message_warning_size |
dimension | Default text size | Set size warning message |
message_warning_keyguard_required |
string | "Screen lock required" | Set text for keyguard warning message |
message_warning_unrooted_device_required |
string | "Un-rooted device required" | Set text for un-rooted device message |
This is a custom view that allows you to add a QR code scanner to scan
OneLogin and third party QR codes to register a factor. It is required
that you override onRequestPermissionsResult
for the camera to be able
to open and setScanListener
to retrieve the code.
To add the QRScan
View to your layout:
<com.onelogin.mfa.view.QrScan
android:id="@+id/my_qr_scanner"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:enable_border="true"
app:enable_accept_symbol="true"/>
Then in your activity/fragment:
// Kotlin
val qrScannerView: QrScan by lazy { requireView().findViewById(R.id.my_qr_scanner) }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
qrScannerView.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
qrScannerView.setScanListener(object : OnCodeEntryListener {
override fun onOneLoginCode(code: String) {
Timber.d("Found OneLogin code. Ready to register")
}
override fun onThirdPartyCode(code: String) {
Timber.d("Found 3rd party code. Ready to register")
}
})
// Java
QrScan qrScanner = findViewById(R.id.my_qr_scanner);
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
qrScanner.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
qrScanner.setScanListener(new OnCodeEntryListener() {
@Override
public void onOneLoginCode(@NonNull String code) {
Timber.d("Found OneLogin code. Ready to register");
}
@Override
public void onThirdPartyCode(@NonNull String code) {
Timber.d("Found 3rd party code. Ready to register");
}
});
These are the custom attributes available for the QrScan
View:
Attribute | Format | Default Value | Purpose |
---|---|---|---|
enable_border |
boolean | true | Set to true to show a green square border on the QR scanner |
enable_accept_symbol |
boolean | true | Set to true to show a green check mark when QR code is found |
This is a custom view that allows you to manually enter the code
provided by your OneLogin account. The code is currently in the format
of XX-XXXXXXX
.
To add the ManualEntry
View to your layout:
<com.onelogin.mfa.view.ManualEntry
android:id="@+id/my_manual_entry_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:factor_code_size="30sp"
app:factor_code_color="@color/primary_blue"
app:factor_code_hint="Enter Activation Code"
app:enable_nine_digit_format="true" />
Then in your activity/fragment:
// Kotlin
val manualEntryView: ManualEntry by lazy { requireView().findViewById(R.id.my_manual_entry_input) }
manualEntryView.setEntryCompleteListener(object: OnCodeEntryListener {
override fun onOneLoginCode(code: String) {
Timber.d("Valid OneLogin code. Ready to register.")
}
override fun onThirdPartyCode(code: String) {
Timber.d("Valid 3rd party code. Ready to register.")
}
})
// Java
ManualEntry manualEntry = findViewById(R.id.my_manual_entry_input);
manualEntry.setEntryCompleteListener(new OnCodeEntryListener() {
@Override
public void onOneLoginCode(@NonNull String code) {
Timber.d("Valid OneLogin code. Ready to register.");
}
@Override
public void onThirdPartyCode(@NonNull String code) {
Timber.d("Valid 3rd party code. Ready to register.");
}
});
These are the custom attributes available for the ManualEntry
View:
Attribute | Format | Default Value | Purpose |
---|---|---|---|
factor_code_color |
color | Default text color | Set color of code text input |
factor_code_size |
dimension | Default text size | Set size of code text |
factor_code_hint |
string | "" | Set text for code entry hint |
factor_code_hint_color |
color | Default text color | Set color for code entry hint |
This is a custom view that allows you to add a countdown dial that coincides with the expiration of your OTP code.
To add CountdownDial
to your layout:
<com.onelogin.mfa.view.CountdownDial
android:id="@+id/my_countdown"
android:layout_width="25dp"
android:layout_height="25dp"
app:countdown_paint_color="@color/primary_orange_variant"
app:countdown_background_color="@color/primary_grey"
app:countdown_center_color="?android:colorBackground"/>
Then in your activity/fragment:
// Kotlin
val countdownDialView: CountdownDial = findViewById(R.id.my_countdown)
countdownDialView.setProgress(myFactor)
// Java
CountdownDial countdownDial = findViewById(R.id.my_countdown);
setProgress(myFactor, 100);
These are the custom attributes available for the CountdownDial
View:
Attribute | Format | Default Value | Purpose |
---|---|---|---|
factor_code_color |
color | Default text color | Set color of code text input |
factor_code_size |
dimension | Default text size | Set size of code text |
factor_code_hint |
string | "" | Set text for code entry hint |
factor_code_hint_color |
color | Default text color | Set color for code entry hint |
Currently, a Kotlin demo application is available to test. Clone the
project and run appkotlin
in Android Studio.