Codename One ships first-class biometric authentication (Touch ID, Face ID, Android BiometricPrompt) under the com.codename1.security package. A single entry point exposes the platform prompt, a typed enum reports the modalities currently enrolled, and a typed error enum lets callers react to failures without string matching.
A sibling SecureStorage API stores secret strings (auth tokens, refresh tokens, encryption keys) behind biometric authentication using the iOS keychain or the Android Keystore.
Quick start: Authenticate
Biometrics b = Biometrics.getInstance();
if (!b.canAuthenticate()) {
// Fall back to password
return;
}
b.authenticate("Unlock your account").onResult((success, err) -> {
if (err != null) {
BiometricError code = ((BiometricException) err).getError();
switch (code) {
case USER_CANCELED: /* user dismissed the prompt */ break;
case LOCKED_OUT: /* too many bad attempts */ break;
case NOT_ENROLLED: /* prompt the user to enrol in Settings */ break;
default: /* generic failure */ break;
}
} else {
// Authenticated. Continue with the gated action.
}
});
Always gate the call on canAuthenticate() and never on isSupported() alone. isSupported() only checks for hardware, while canAuthenticate() also requires at least one enrolled credential and that the device isn’t in a locked-out state.
Quick start: Secure storage
Entries are bound to the current set of enrolled biometrics. If the user enrols a new fingerprint or face after writing, the next get fails with BiometricError.KEY_REVOKED and the application must re-prompt for the original value and write it again.
Platform behaviour
| Platform | Implementation | Notes |
|---|---|---|
iOS |
| Add the |
Android API 29+ |
| Face / iris / fingerprint per |
Android API 23-28 |
| Fingerprint only. Goes through the support library rather than |
JavaSE simulator |
| Toggle hardware availability, per-modality enrolment, and the next-call outcome. |
All other ports | Non-supporting fallback |
|
Application code never needs a platform if. The API itself returns the right thing on each port. On platforms without biometric hardware, the fallback reports NOT_AVAILABLE immediately.
Build hints
The Codename One Maven plugin and the build daemon both automatically inject the platform configuration when the app’s bytecode references com.codename1.security:
iOS:
LocalAuthentication.frameworkis linked into the generated Xcode project.Android:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />(andUSE_FINGERPRINTwithmaxSdkVersion=28for the legacy path) is injected intoAndroidManifest.xml.
Apps that never touch the API pay nothing: no extra framework, no extra permission.
One thing the plugin won’t inject: the iOS Face ID usage description. Apple rejects placeholder text, so you must supply the build hint yourself:
ios.NSFaceIDUsageDescription=Authenticate to securely access your account
The JavaSE simulator automatically sets this hint to a default the first time biometrics is invoked, so projects developed in the simulator have a working default; you should overwrite the text before shipping.
To share keychain entries with iOS App Extensions, set both the build hint and the runtime access group:
ios.keychainAccessGroup=TEAMID123.group.com.example.app
SecureStorage.getInstance().setKeychainAccessGroup("TEAMID123.group.com.example.app");
The Team ID prefix is required; omitting it produces errSecMissingEntitlement.
Configuring the prompt
Use AuthenticationOptions to control prompt copy and security policy:
Biometrics.getInstance().authenticate(new AuthenticationOptions()
.setReason("Authorize transfer") // iOS localizedReason; Android title fallback
.setTitle("Confirm payment") // Android only
.setSubtitle("Stripe charge $25.00") // Android only
.setNegativeButtonText("Cancel") // Android only
.setBiometricOnly(true) // reject PIN / passcode fallback
.setSensitiveTransaction(true) // request class-3 ("strong") biometric (Android 30+)
);
Unrecognized options are ignored, so callers can set the union without platform-checking.
Typed error handling
BiometricException.getError() returns a BiometricError:
| Code | Meaning |
|---|---|
| No biometric hardware or the platform doesn’t support it |
| Hardware present but the user hasn’t enrolled |
| Too many failed attempts; temporary |
| User must unlock with passcode before biometrics can be used again |
| Device has no PIN / pattern configured (can’t use biometrics) |
| User dismissed the prompt |
| OS dismissed the prompt (app backgrounded, system pre-empted) |
| Prompt completed but the user wasn’t recognized |
|
|
| Anything not covered above |
Cancelling an in-flight prompt
Call Biometrics.getInstance().stopAuthentication() to dismiss an active prompt; the pending AsyncResource then completes with BiometricError.USER_CANCELED. The method returns true if a prompt was cancelled, false if nothing was pending.
Listing enrolled biometrics
getAvailableBiometrics() returns a List<BiometricType> to drive UI affordances (icon, prompt copy):
FINGERPRINT: iOS Touch ID; Android fingerprint sensor.FACE: iOS Face ID; Android face recognition (API 29+).IRIS: a handful of Samsung devices; never on iOS.STRONG: Android class-3 authenticator tier (false-acceptance < 1/100,000); only Android API 30+.WEAK: Android class-2 authenticator tier; only Android API 30+. Can’t unlock Keystore-bound keys, so weak-only devices can’t useSecureStorage.
Never use this list to decide whether to call authenticate(); always use canAuthenticate() for that decision.
Simulator workflow
The JavaSE simulator’s Simulate → Biometric Simulation submenu lets you exercise every code path without a device:
Hardware Available: toggles
isSupported()andcanAuthenticate().Face ID Enrolled / Touch ID Enrolled / Iris Enrolled: populate
getAvailableBiometrics().Next authenticate() Outcome: radio button that picks the outcome of the next prompt:
SUCCEED,FAIL,CANCEL,LOCKED_OUT,PERMANENTLY_LOCKED_OUT,NOT_ENROLLED,PASSCODE_NOT_SET. The selection is sticky; change it any time.
When authenticate() is invoked, a small modal Swing dialog mirrors the platform prompt so the developer can see the trigger fire and click through. State persists across simulator restarts via java.util.prefs.
SecureStorage round-trips against java.util.prefs in the simulator, so you can test the full set → get → KEY_REVOKED flow end-to-end.
Security notes
The Android backing for authenticate() doesn’t simply forward the OS success callback; it binds the prompt to a single-use AES probe cipher in the Android Keystore (with setUserAuthenticationRequired(true) and, on API 24+, setInvalidatedByBiometricEnrollment(true)). On success the cipher performs a real doFinal operation. A hooked or spoofed success callback fails because the Keystore refuses the operation. This guards against the bypass class documented by CodeQL’s java/android/insecure-local-authentication rule.