Skip to content

Commit 6923517

Browse files
kennethjadsonpleal
authored andcommitted
[local_auth] Allow device authentication (pin/pattern/passcode) (flutter#2489)
1 parent 7907b2e commit 6923517

File tree

20 files changed

+727
-265
lines changed

20 files changed

+727
-265
lines changed
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
org.gradle.jvmargs=-Xmx1536M
2-

packages/local_auth/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 1.1.0-nullsafety
2+
3+
* Allow pin, passcode, and pattern authentication with `authenticate` method
4+
* **Breaking change**. Parameter names refactored to use the generic `biometric` prefix in place of `fingerprint` in the `AndroidAuthMessages` class
5+
* `fingerprintHint` is now `biometricHint`
6+
* `fingerprintNotRecognized`is now `biometricNotRecognized`
7+
* `fingerprintSuccess`is now `biometricSuccess`
8+
* `fingerprintRequiredTitle` is now `biometricRequiredTitle`
9+
110
## 1.0.0-nullsafety.3
211

312
* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets.

packages/local_auth/README.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ bool canCheckBiometrics =
2323

2424
Currently the following biometric types are implemented:
2525

26-
* BiometricType.face
27-
* BiometricType.fingerprint
26+
- BiometricType.face
27+
- BiometricType.fingerprint
2828

2929
To get a list of enrolled biometrics, call getAvailableBiometrics:
3030

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

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

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

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

58+
The `authenticate()` method uses biometric authentication, but also allows
59+
users to use pin, pattern, or passcode.
60+
5861
```dart
5962
var localAuth = LocalAuthentication();
6063
bool didAuthenticate =
61-
await localAuth.authenticateWithBiometrics(
64+
await localAuth.authenticate(
6265
localizedReason: 'Please authenticate to show account balance');
6366
```
6467

68+
To authenticate using biometric authentication only, set `biometricOnly` to `true`.
69+
70+
```dart
71+
var localAuth = LocalAuthentication();
72+
bool didAuthenticate =
73+
await localAuth.authenticate(
74+
localizedReason: 'Please authenticate to show account balance',
75+
biometricOnly: true);
76+
```
77+
6578
If you don't want to use the default dialogs, call this API with
6679
'useErrorDialogs = false'. In this case, it will throw the error message back
6780
and you need to handle them in your dart code:
6881

6982
```dart
7083
bool didAuthenticate =
71-
await localAuth.authenticateWithBiometrics(
84+
await localAuth.authenticate(
7285
localizedReason: 'Please authenticate to show account balance',
7386
useErrorDialogs: false);
7487
```
@@ -84,7 +97,7 @@ const iosStrings = const IOSAuthMessages(
8497
goToSettingsButton: 'settings',
8598
goToSettingsDescription: 'Please set up your Touch ID.',
8699
lockOut: 'Please reenable your Touch ID');
87-
await localAuth.authenticateWithBiometrics(
100+
await localAuth.authenticate(
88101
localizedReason: 'Please authenticate to show account balance',
89102
useErrorDialogs: false,
90103
iOSAuthStrings: iosStrings);
@@ -112,7 +125,7 @@ import 'package:flutter/services.dart';
112125
import 'package:local_auth/error_codes.dart' as auth_error;
113126
114127
try {
115-
bool didAuthenticate = await local_auth.authenticateWithBiometrics(
128+
bool didAuthenticate = await local_auth.authenticate(
116129
localizedReason: 'Please authenticate to show account balance');
117130
} on PlatformException catch (e) {
118131
if (e.code == auth_error.notAvailable) {
@@ -134,7 +147,6 @@ you need to also add:
134147
to your Info.plist file. Failure to do so results in a dialog that tells the user your
135148
app has not been updated to use TouchID.
136149

137-
138150
## Android Integration
139151

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

197209
## Sticky Auth

packages/local_auth/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ buildscript {
88
}
99

1010
dependencies {
11-
classpath 'com.android.tools.build:gradle:3.3.0'
11+
classpath 'com.android.tools.build:gradle:4.1.1'
1212
}
1313
}
1414

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="io.flutter.plugins.localauth">
3+
<uses-sdk android:targetSdkVersion="29"/>
4+
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
35
</manifest>

packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,16 @@
2929
import java.util.concurrent.Executor;
3030

3131
/**
32-
* Authenticates the user with fingerprint and sends corresponding response back to Flutter.
32+
* Authenticates the user with biometrics and sends corresponding response back to Flutter.
3333
*
3434
* <p>One instance per call is generated to ensure readable separation of executable paths across
3535
* method calls.
3636
*/
3737
@SuppressWarnings("deprecation")
3838
class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback
3939
implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {
40-
4140
/** The callback that handles the result of this authentication process. */
4241
interface AuthCompletionHandler {
43-
4442
/** Called when authentication was successful. */
4543
void onSuccess();
4644

@@ -75,24 +73,32 @@ interface AuthCompletionHandler {
7573
Lifecycle lifecycle,
7674
FragmentActivity activity,
7775
MethodCall call,
78-
AuthCompletionHandler completionHandler) {
76+
AuthCompletionHandler completionHandler,
77+
boolean allowCredentials) {
7978
this.lifecycle = lifecycle;
8079
this.activity = activity;
8180
this.completionHandler = completionHandler;
8281
this.call = call;
8382
this.isAuthSticky = call.argument("stickyAuth");
8483
this.uiThreadExecutor = new UiThreadExecutor();
85-
this.promptInfo =
84+
85+
BiometricPrompt.PromptInfo.Builder promptBuilder =
8686
new BiometricPrompt.PromptInfo.Builder()
8787
.setDescription((String) call.argument("localizedReason"))
8888
.setTitle((String) call.argument("signInTitle"))
89-
.setSubtitle((String) call.argument("fingerprintHint"))
90-
.setNegativeButtonText((String) call.argument("cancelButton"))
89+
.setSubtitle((String) call.argument("biometricHint"))
9190
.setConfirmationRequired((Boolean) call.argument("sensitiveTransaction"))
92-
.build();
91+
.setConfirmationRequired((Boolean) call.argument("sensitiveTransaction"));
92+
93+
if (allowCredentials) {
94+
promptBuilder.setDeviceCredentialAllowed(true);
95+
} else {
96+
promptBuilder.setNegativeButtonText((String) call.argument("cancelButton"));
97+
}
98+
this.promptInfo = promptBuilder.build();
9399
}
94100

95-
/** Start the fingerprint listener. */
101+
/** Start the biometric listener. */
96102
void authenticate() {
97103
if (lifecycle != null) {
98104
lifecycle.addObserver(this);
@@ -103,15 +109,15 @@ void authenticate() {
103109
biometricPrompt.authenticate(promptInfo);
104110
}
105111

106-
/** Cancels the fingerprint authentication. */
112+
/** Cancels the biometric authentication. */
107113
void stopAuthentication() {
108114
if (biometricPrompt != null) {
109115
biometricPrompt.cancelAuthentication();
110116
biometricPrompt = null;
111117
}
112118
}
113119

114-
/** Stops the fingerprint listener. */
120+
/** Stops the biometric listener. */
115121
private void stop() {
116122
if (lifecycle != null) {
117123
lifecycle.removeObserver(this);
@@ -125,21 +131,27 @@ private void stop() {
125131
public void onAuthenticationError(int errorCode, CharSequence errString) {
126132
switch (errorCode) {
127133
case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL:
128-
completionHandler.onError(
129-
"PasscodeNotSet",
130-
"Phone not secured by PIN, pattern or password, or SIM is currently locked.");
131-
break;
134+
if (call.argument("useErrorDialogs")) {
135+
showGoToSettingsDialog(
136+
(String) call.argument("deviceCredentialsRequired"),
137+
(String) call.argument("deviceCredentialsSetupDescription"));
138+
return;
139+
}
140+
completionHandler.onError("NotAvailable", "Security credentials not available.");
132141
case BiometricPrompt.ERROR_NO_SPACE:
133142
case BiometricPrompt.ERROR_NO_BIOMETRICS:
143+
if (promptInfo.isDeviceCredentialAllowed()) return;
134144
if (call.argument("useErrorDialogs")) {
135-
showGoToSettingsDialog();
145+
showGoToSettingsDialog(
146+
(String) call.argument("biometricRequired"),
147+
(String) call.argument("goToSettingDescription"));
136148
return;
137149
}
138150
completionHandler.onError("NotEnrolled", "No Biometrics enrolled on this device.");
139151
break;
140152
case BiometricPrompt.ERROR_HW_UNAVAILABLE:
141153
case BiometricPrompt.ERROR_HW_NOT_PRESENT:
142-
completionHandler.onError("NotAvailable", "Biometrics is not available on this device.");
154+
completionHandler.onError("NotAvailable", "Security credentials not available.");
143155
break;
144156
case BiometricPrompt.ERROR_LOCKOUT:
145157
completionHandler.onError(
@@ -176,7 +188,7 @@ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult resul
176188
public void onAuthenticationFailed() {}
177189

178190
/**
179-
* If the activity is paused, we keep track because fingerprint dialog simply returns "User
191+
* If the activity is paused, we keep track because biometric dialog simply returns "User
180192
* cancelled" when the activity is paused.
181193
*/
182194
@Override
@@ -215,12 +227,12 @@ public void onResume(@NonNull LifecycleOwner owner) {
215227

216228
// Suppress inflateParams lint because dialogs do not need to attach to a parent view.
217229
@SuppressLint("InflateParams")
218-
private void showGoToSettingsDialog() {
230+
private void showGoToSettingsDialog(String title, String descriptionText) {
219231
View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false);
220232
TextView message = (TextView) view.findViewById(R.id.fingerprint_required);
221233
TextView description = (TextView) view.findViewById(R.id.go_to_setting_description);
222-
message.setText((String) call.argument("fingerprintRequired"));
223-
description.setText((String) call.argument("goToSettingDescription"));
234+
message.setText(title);
235+
description.setText(descriptionText);
224236
Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom);
225237
OnClickListener goToSettingHandler =
226238
new OnClickListener() {

0 commit comments

Comments
 (0)