Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
[local_auth] Allow device authentication (pin/pattern/passcode) (#2489)
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethj committed Jan 15, 2021
1 parent cc234f6 commit eccc3cd
Show file tree
Hide file tree
Showing 20 changed files with 727 additions and 265 deletions.
1 change: 0 additions & 1 deletion packages/integration_test/android/gradle.properties
@@ -1,2 +1 @@
org.gradle.jvmargs=-Xmx1536M

9 changes: 9 additions & 0 deletions packages/local_auth/CHANGELOG.md
@@ -1,3 +1,12 @@
## 1.1.0-nullsafety

* Allow pin, passcode, and pattern authentication with `authenticate` method
* **Breaking change**. Parameter names refactored to use the generic `biometric` prefix in place of `fingerprint` in the `AndroidAuthMessages` class
* `fingerprintHint` is now `biometricHint`
* `fingerprintNotRecognized`is now `biometricNotRecognized`
* `fingerprintSuccess`is now `biometricSuccess`
* `fingerprintRequiredTitle` is now `biometricRequiredTitle`

## 1.0.0-nullsafety.3

* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets.
Expand Down
36 changes: 24 additions & 12 deletions packages/local_auth/README.md
Expand Up @@ -23,8 +23,8 @@ bool canCheckBiometrics =

Currently the following biometric types are implemented:

* BiometricType.face
* BiometricType.fingerprint
- BiometricType.face
- BiometricType.fingerprint

To get a list of enrolled biometrics, call getAvailableBiometrics:

Expand All @@ -44,31 +44,44 @@ if (Platform.isIOS) {
We have default dialogs with an 'OK' button to show authentication error
messages for the following 2 cases:

1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on
iOS or PIN/pattern on Android.
2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any
fingerprints on the device.
1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on
iOS or PIN/pattern on Android.
2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any
fingerprints on the device.

Which means, if there's no fingerprint on the user's device, a dialog with
instructions will pop up to let the user set up fingerprint. If the user clicks
'OK' button, it will return 'false'.

Use the exported APIs to trigger local authentication with default dialogs:

The `authenticate()` method uses biometric authentication, but also allows
users to use pin, pattern, or passcode.

```dart
var localAuth = LocalAuthentication();
bool didAuthenticate =
await localAuth.authenticateWithBiometrics(
await localAuth.authenticate(
localizedReason: 'Please authenticate to show account balance');
```

To authenticate using biometric authentication only, set `biometricOnly` to `true`.

```dart
var localAuth = LocalAuthentication();
bool didAuthenticate =
await localAuth.authenticate(
localizedReason: 'Please authenticate to show account balance',
biometricOnly: true);
```

If you don't want to use the default dialogs, call this API with
'useErrorDialogs = false'. In this case, it will throw the error message back
and you need to handle them in your dart code:

```dart
bool didAuthenticate =
await localAuth.authenticateWithBiometrics(
await localAuth.authenticate(
localizedReason: 'Please authenticate to show account balance',
useErrorDialogs: false);
```
Expand All @@ -84,7 +97,7 @@ const iosStrings = const IOSAuthMessages(
goToSettingsButton: 'settings',
goToSettingsDescription: 'Please set up your Touch ID.',
lockOut: 'Please reenable your Touch ID');
await localAuth.authenticateWithBiometrics(
await localAuth.authenticate(
localizedReason: 'Please authenticate to show account balance',
useErrorDialogs: false,
iOSAuthStrings: iosStrings);
Expand Down Expand Up @@ -112,7 +125,7 @@ import 'package:flutter/services.dart';
import 'package:local_auth/error_codes.dart' as auth_error;
try {
bool didAuthenticate = await local_auth.authenticateWithBiometrics(
bool didAuthenticate = await local_auth.authenticate(
localizedReason: 'Please authenticate to show account balance');
} on PlatformException catch (e) {
if (e.code == auth_error.notAvailable) {
Expand All @@ -134,7 +147,6 @@ you need to also add:
to your Info.plist file. Failure to do so results in a dialog that tells the user your
app has not been updated to use TouchID.


## Android Integration

Note that local_auth plugin requires the use of a FragmentActivity as
Expand Down Expand Up @@ -191,7 +203,7 @@ Update your project's `AndroidManifest.xml` file to include the
On Android, you can check only for existence of fingerprint hardware prior
to API 29 (Android Q). Therefore, if you would like to support other biometrics
types (such as face scanning) and you want to support SDKs lower than Q,
*do not* call `getAvailableBiometrics`. Simply call `authenticateWithBiometrics`.
_do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`.
This will return an error if there was no hardware available.

## Sticky Auth
Expand Down
2 changes: 1 addition & 1 deletion packages/local_auth/android/build.gradle
Expand Up @@ -8,7 +8,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:4.1.1'
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/local_auth/android/src/main/AndroidManifest.xml
@@ -1,3 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.flutter.plugins.localauth">
<uses-sdk android:targetSdkVersion="29"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
</manifest>
Expand Up @@ -29,18 +29,16 @@
import java.util.concurrent.Executor;

/**
* Authenticates the user with fingerprint and sends corresponding response back to Flutter.
* Authenticates the user with biometrics and sends corresponding response back to Flutter.
*
* <p>One instance per call is generated to ensure readable separation of executable paths across
* method calls.
*/
@SuppressWarnings("deprecation")
class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback
implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {

/** The callback that handles the result of this authentication process. */
interface AuthCompletionHandler {

/** Called when authentication was successful. */
void onSuccess();

Expand Down Expand Up @@ -75,24 +73,32 @@ interface AuthCompletionHandler {
Lifecycle lifecycle,
FragmentActivity activity,
MethodCall call,
AuthCompletionHandler completionHandler) {
AuthCompletionHandler completionHandler,
boolean allowCredentials) {
this.lifecycle = lifecycle;
this.activity = activity;
this.completionHandler = completionHandler;
this.call = call;
this.isAuthSticky = call.argument("stickyAuth");
this.uiThreadExecutor = new UiThreadExecutor();
this.promptInfo =

BiometricPrompt.PromptInfo.Builder promptBuilder =
new BiometricPrompt.PromptInfo.Builder()
.setDescription((String) call.argument("localizedReason"))
.setTitle((String) call.argument("signInTitle"))
.setSubtitle((String) call.argument("fingerprintHint"))
.setNegativeButtonText((String) call.argument("cancelButton"))
.setSubtitle((String) call.argument("biometricHint"))
.setConfirmationRequired((Boolean) call.argument("sensitiveTransaction"))
.build();
.setConfirmationRequired((Boolean) call.argument("sensitiveTransaction"));

if (allowCredentials) {
promptBuilder.setDeviceCredentialAllowed(true);
} else {
promptBuilder.setNegativeButtonText((String) call.argument("cancelButton"));
}
this.promptInfo = promptBuilder.build();
}

/** Start the fingerprint listener. */
/** Start the biometric listener. */
void authenticate() {
if (lifecycle != null) {
lifecycle.addObserver(this);
Expand All @@ -103,15 +109,15 @@ void authenticate() {
biometricPrompt.authenticate(promptInfo);
}

/** Cancels the fingerprint authentication. */
/** Cancels the biometric authentication. */
void stopAuthentication() {
if (biometricPrompt != null) {
biometricPrompt.cancelAuthentication();
biometricPrompt = null;
}
}

/** Stops the fingerprint listener. */
/** Stops the biometric listener. */
private void stop() {
if (lifecycle != null) {
lifecycle.removeObserver(this);
Expand All @@ -125,21 +131,27 @@ private void stop() {
public void onAuthenticationError(int errorCode, CharSequence errString) {
switch (errorCode) {
case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL:
completionHandler.onError(
"PasscodeNotSet",
"Phone not secured by PIN, pattern or password, or SIM is currently locked.");
break;
if (call.argument("useErrorDialogs")) {
showGoToSettingsDialog(
(String) call.argument("deviceCredentialsRequired"),
(String) call.argument("deviceCredentialsSetupDescription"));
return;
}
completionHandler.onError("NotAvailable", "Security credentials not available.");
case BiometricPrompt.ERROR_NO_SPACE:
case BiometricPrompt.ERROR_NO_BIOMETRICS:
if (promptInfo.isDeviceCredentialAllowed()) return;
if (call.argument("useErrorDialogs")) {
showGoToSettingsDialog();
showGoToSettingsDialog(
(String) call.argument("biometricRequired"),
(String) call.argument("goToSettingDescription"));
return;
}
completionHandler.onError("NotEnrolled", "No Biometrics enrolled on this device.");
break;
case BiometricPrompt.ERROR_HW_UNAVAILABLE:
case BiometricPrompt.ERROR_HW_NOT_PRESENT:
completionHandler.onError("NotAvailable", "Biometrics is not available on this device.");
completionHandler.onError("NotAvailable", "Security credentials not available.");
break;
case BiometricPrompt.ERROR_LOCKOUT:
completionHandler.onError(
Expand Down Expand Up @@ -176,7 +188,7 @@ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult resul
public void onAuthenticationFailed() {}

/**
* If the activity is paused, we keep track because fingerprint dialog simply returns "User
* If the activity is paused, we keep track because biometric dialog simply returns "User
* cancelled" when the activity is paused.
*/
@Override
Expand Down Expand Up @@ -215,12 +227,12 @@ public void onResume(@NonNull LifecycleOwner owner) {

// Suppress inflateParams lint because dialogs do not need to attach to a parent view.
@SuppressLint("InflateParams")
private void showGoToSettingsDialog() {
private void showGoToSettingsDialog(String title, String descriptionText) {
View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false);
TextView message = (TextView) view.findViewById(R.id.fingerprint_required);
TextView description = (TextView) view.findViewById(R.id.go_to_setting_description);
message.setText((String) call.argument("fingerprintRequired"));
description.setText((String) call.argument("goToSettingDescription"));
message.setText(title);
description.setText(descriptionText);
Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom);
OnClickListener goToSettingHandler =
new OnClickListener() {
Expand Down

0 comments on commit eccc3cd

Please sign in to comment.