
Mifos-Passcode-CMP is a secure and flexible App Lock library built using Kotlin Multiplatform and Jetpack Compose Multiplatform (CMP). It enables developers to easily integrate passcode-based authentication along with biometric authentication (such as fingerprint or face recognition) into cross-platform applications using a shared codebase.
Designed with modularity and security in mind, this library is a foundational part of the Mifos mobile ecosystem and is suitable for any Kotlin Multiplatform project where secure access control is required.
Platform | Passcode | Platform Authenticator |
---|---|---|
Android | ✅ Supported | ✅ Supported |
iOS | ✅ Supported | ✅ Supported |
macOS | ✅ Supported | |
Windows 10+ | ✅ Supported | ✅ Supported |
Linux | ✅ Supported | |
Web | ✅ Supported |
Core library module containing shared and platform-specific implementations:
commonMain/
- Platform-agnostic Platform Authenticator logic and whole passcode logic.
androidMain/
- Biometric Prompt implementation for Platform Authenticator.
iosMain/
- LocalAuthenticator implementation for Platform Authenticator.
desktopMain(jvm)/
windows/
- Windows Hello implementation for Platform Authenticator.
linux/
- LocalAuthenticator implementation for Platform Authenticator.
macOS/
jsMain/
- WebAuthN implementation for using the available FIDO2 or Platform Authenticator.
wasmMain/
- WebAuthN implementation for using the available FIDO2 or Platform Authenticator.
Cross-platform sample implementation of the passcode screen UI and Platform Authenticator:
commonMain/
– Shared Platform Authenticator using Compose Multiplatform.- Logic for using the Passcode implementation.
<platform>Main/
– Platform-specific UI wiring for the Platform Authenticator.
The PasscodeScreen
is a composable function designed to handle passcode authentication or setup workflows in your app. It is powered by a state-preserving utility rememberPasscodeSaver
, which manages the current passcode state and provides utility functions for saving and clearing the passcode.
To use the PasscodeScreen
, you must first set up a PasscodeSaver
instance using rememberPasscodeSaver
.
val passcodeSaver = rememberPasscodeSaver(
currentPasscode = currentPasscode,
isPasscodeSet = isPasscodeAlreadySet,
savePasscode = { passcode -> /* handle saving */ },
clearPasscode = { /* handle clearing */ }
)
Then pass this passcodeSaver
to the PasscodeScreen
:
PasscodeScreen(
passcodeSaver = passcodeSaver,
onForgotButton = { /* handle forgot passcode */ },
onSkipButton = { /* handle skip action */ },
onPasscodeRejected = { /* handle wrong passcode entry */ },
onPasscodeConfirm = { passcode -> /* handle successful confirmation */ }
)
passcodeSaver
– Required. Handles the passcode input and stores the current state.onForgotButton
– Called when the user taps the "Forgot" button.onSkipButton
– Called when the user taps the "Skip" button.onPasscodeRejected
– Optional. Called when the entered passcode is wrong.onPasscodeConfirm
– Called when the user enters the correct passcode or finishes setting a new one.
currentPasscode
– The current passcode (if already set).isPasscodeSet
– Tells the screen whether the user is verifying an existing passcode or creating a new one.savePasscode
– A function that saves the passcode.clearPasscode
– A function that clears the saved passcode.
- If there's already a passcode, the screen asks the user to enter it and checks if it matches.
- If no passcode is set, the screen helps the user create and confirm a new one.
- The
rememberPasscodeSaver
keeps everything in sync and remembers the state even if the screen recomposes.
![]() |
![]() |
![]() |



This module provides a unified and multiplatform way to handle device-based authentication. It uses a PlatformAuthenticator
to interact with platform-specific mechanisms (like Windows Hello or Android BiometricPrompt) and wraps it in a thread-safe PlatformAuthenticationProvider
for easy and safe use in your application.
To use the platform authenticator, first create an instance of PlatformAuthenticator
, and then pass it to PlatformAuthenticationProvider.
On Android
, you must pass a FragmentActivity
or an AppCompatActivity
. On other platforms, this is not required.
// 1. Create the platform-specific authenticator instance
// On Android
val authenticator = PlatformAuthenticator(this)
// On other platforms
val authenticator = PlatformAuthenticator()
// 2. Create the provider instance to interact with
val authProvider = PlatformAuthenticationProvider(authenticator)
This is the main class you should interact with. It acts as a thread-safe facade that simplifies using the PlatformAuthenticator
.
final class PlatformAuthenticationProvider(private val authenticator: PlatformAuthenticator) {
// Checks the current status of the device authenticator.
fun deviceAuthenticatorStatus(): Set<PlatformAuthenticatorStatus>
// Prompts the user to set up a platform authenticator.
fun setupPlatformAuthenticator()
// Registers a user and creates a platform-specific passkey.
suspend fun registerUser(
userName: String = "",
emailId: String = "",
displayName: String = ""
): RegistrationResult
// Verifies the user against their registered credential.
suspend fun onAuthenticatorClick(
appName: String = "",
savedRegistrationData: String? = null
): AuthenticationResult
}
PlatformAuthenticator
(Underlying Engine)
This expect class
contains the core platform-specific logic. It's managed by the PlatformAuthenticationProvider
.
expect class PlatformAuthenticator private constructor() {
constructor(activity: Any? = null)
fun getDeviceAuthenticatorStatus(): Set<PlatformAuthenticatorStatus>
fun setDeviceAuthOption()
suspend fun registerUser(...): RegistrationResult
suspend fun authenticate(...): AuthenticationResult
}
Thread Requirement: The Android BiometricPrompt
API, which this module uses under the hood, requires that it be invoked from the main thread.
Therefore, you must call platformAuthenticationProvider.registerUser(...)
and platformAuthenticationProvider.onAuthenticatorClick(...)
from the Main dispatcher. Using viewModelScope
in Android ViewModels handles this correctly, but it's good practice to be explicit.
// Always launch from the Main thread for these calls
viewModelScope.launch(Dispatchers.Main) {
// ... call registerUser or onAuthenticatorClick
}
The getDeviceAuthenticatorStatus()
function returns a set of the following values:
NOT_AVAILABLE
– Platform authenticator is not supported on the device.NOT_SETUP
– Authenticator is available but not set up yet.DEVICE_CREDENTIAL_SET
– Device credential (PIN, password, etc.) is available.BIOMETRICS_NOT_SET
– Biometrics are supported but not configured.BIOMETRICS_NOT_AVAILABLE
– Biometrics are not available on the device.BIOMETRICS_UNAVAILABLE
– Biometrics are temporarily unavailable.BIOMETRICS_SET
– Biometrics are available and configured.
Here’s how you can integrate PlatformAuthenticationProvider
into your ViewModels, ensuring calls are made on the correct thread.
Registration ViewModel This ViewModel handles the logic for the user registration screen.
class ChooseAuthOptionScreenViewmodel(
private val platformAuthenticationProvider: PlatformAuthenticationProvider,
// other dependencies...
) : ViewModel() {
private val _registrationResult = MutableStateFlow<RegistrationResult?>(null)
val registrationResult = _registrationResult.asStateFlow()
private val _authenticatorStatus = MutableStateFlow(platformAuthenticationProvider.deviceAuthenticatorStatus())
val authenticatorStatus = _authenticatorStatus.asStateFlow()
fun updatePlatformAuthenticatorStatus() {
_authenticatorStatus.value = platformAuthenticationProvider.deviceAuthenticatorStatus()
}
fun registerUser(userID: String = "", userEmail: String = "", displayName: String = "") {
// Explicitly launch on the Main thread
viewModelScope.launch(Dispatchers.Main) {
_registrationResult.value = platformAuthenticationProvider.registerUser(
userID,
userEmail,
displayName
)
}
}
}
Possible return values:
RegistrationResult.Success
- Its parameter contains the registration data that has to saved. The same data is passed as an argument to theauthenticate
functionRegistrationResult.Error
- Its parameter contains a message telling what type of error was received.RegistrationResult.PlatformAuthenticatorNotAvailable
RegistrationResult.PlatformAuthenticatorNotSet
This ViewModel manages the authentication flow, such as on a login screen.
class PlatformAuthenticationScreenViewModel(
private val platformAuthenticationProvider: PlatformAuthenticationProvider,
private val preferenceDataStore: PreferenceDataStore
) : ViewModel() {
private val _authenticationResult = MutableStateFlow<AuthenticationResult?>(null)
val authenticationResult = _authenticationResult.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
fun authenticateUser(appName: String) {
// Explicitly launch on the Main thread
viewModelScope.launch(Dispatchers.Main) {
_isLoading.value = true
val savedData = preferenceDataStore.getRegistrationData()
_authenticationResult.value = platformAuthenticationProvider.onAuthenticatorClick(appName, savedData)
_isLoading.value = false
}
}
fun clearUserRegistrationFromApp() {
preferenceDataStore.clearData(REGISTRATION_DATA)
}
}
Possible return values:
AuthenticationResult.Success
AuthenticationResult.Error
AuthenticationResult.UserNotRegistered
- If the user disables the platform authenticator, or in case of Windows Hello, the passkey is deleted or the Authenticator is disabled. The user should be logged out in this case and registered again.
Prompt the user to set up a device credential or biometric authentication:
authProvider.setupPlatformAuthenticator()
On Android
, it actually redirects users to a screen for setting up a platform authentication method.
On Windows
, it will only show a message saying Set up Windows Hello from settings
. Windows Hello
itself shows a similar message in some cases and doesn't redirect users to the setup screen.
Returned by authProvider.registerUser()
.
sealed interface RegistrationResult {
data class Success(val registrationData: String) : RegistrationResult
data class Error(val message: String) : RegistrationResult
data object PlatformAuthenticatorNotSet : RegistrationResult
data object PlatformAuthenticatorNotAvailable : RegistrationResult
}
Returned by authProvider.onAuthenticatorClick()
.
sealed interface AuthenticationResult {
data object Success : AuthenticationResult
data class Error(val message: String) : AuthenticationResult
data object UserNotRegistered : AuthenticationResult
}