Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/local_auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.1

* Added ability to stop authentication (For Android).

## 0.6.0+3

* Remove AndroidX warnings.
Expand Down
10 changes: 10 additions & 0 deletions packages/local_auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ await localAuth.authenticateWithBiometrics(

```

If needed, you can manually stop authentication for android:

```dart

void _cancelAuthentication() {
localAuth.stopAuthentication();
}

```

### Exceptions

There are 6 types of exceptions: PasscodeNotSet, NotEnrolled, NotAvailable, OtherOperatingSystem, LockedOut and PermanentlyLockedOut.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface AuthCompletionHandler {
private final boolean isAuthSticky;
private final UiThreadExecutor uiThreadExecutor;
private boolean activityPaused = false;
private BiometricPrompt biometricPrompt;

public AuthenticationHelper(
FragmentActivity activity, MethodCall call, AuthCompletionHandler completionHandler) {
Expand All @@ -84,7 +85,16 @@ public AuthenticationHelper(
/** Start the fingerprint listener. */
public void authenticate() {
activity.getApplication().registerActivityLifecycleCallbacks(this);
new BiometricPrompt(activity, uiThreadExecutor, this).authenticate(promptInfo);
biometricPrompt = new BiometricPrompt(activity, uiThreadExecutor, this);
biometricPrompt.authenticate(promptInfo);
}

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

/** Stops the fingerprint listener. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
public class LocalAuthPlugin implements MethodCallHandler {
private final Registrar registrar;
private final AtomicBoolean authInProgress = new AtomicBoolean(false);
private AuthenticationHelper authenticationHelper;

/** Plugin registration. */
public static void registerWith(Registrar registrar) {
Expand All @@ -37,7 +38,7 @@ private LocalAuthPlugin(Registrar registrar) {
@Override
public void onMethodCall(MethodCall call, final Result result) {
if (call.method.equals("authenticateWithBiometrics")) {
if (!authInProgress.compareAndSet(false, true)) {
if (authInProgress.get()) {
// Apps should not invoke another authentication request while one is in progress,
// so we classify this as an error condition. If we ever find a legitimate use case for
// this, we can try to cancel the ongoing auth and start a new one but for now, not worth
Expand All @@ -59,7 +60,8 @@ public void onMethodCall(MethodCall call, final Result result) {
null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bug here for authInProgress. We might set it to true and never actually call authenticate.

Could you chage line 41 to: if (authInProgress.get())
then add to line 62: authInProgress.set(true);.

Since this method always runs in the UIThread, we technically don't need to use AtomicBoolean. You can also optionally change all of this to a normal boolean primitive.

Copy link
Contributor Author

@karan-rawal karan-rawal Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved. Not changing it to boolean primitive. Since, I don't want things to break. :)

return;
}
AuthenticationHelper authenticationHelper =
authInProgress.set(true);
authenticationHelper =
new AuthenticationHelper(
(FragmentActivity) activity,
call,
Expand Down Expand Up @@ -112,8 +114,27 @@ public void onError(String code, String error) {
} catch (Exception e) {
result.error("no_biometrics_available", e.getMessage(), null);
}
} else if (call.method.equals(("stopAuthentication"))) {
stopAuthentication(result);
} else {
result.notImplemented();
}
}

/*
Stops the authentication if in progress.
*/
private void stopAuthentication(Result result) {
try {
if (authenticationHelper != null && authInProgress.get()) {
authenticationHelper.stopAuthentication();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this means .authenticate method will never return? Will one of the AuthCompletionHandler methods be called when you stop authentication?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mehmetf

  1. The .authenticate will return false when we stop authentication.
  2. Yes, the onFailure method will be called.

authenticationHelper = null;
result.success(true);
return;
}
result.success(false);
} catch (Exception e) {
result.success(false);
}
}
}
21 changes: 18 additions & 3 deletions packages/local_auth/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class _MyAppState extends State<MyApp> {
bool _canCheckBiometrics;
List<BiometricType> _availableBiometrics;
String _authorized = 'Not Authorized';
bool _isAuthenticating = false;

Future<void> _checkBiometrics() async {
bool canCheckBiometrics;
Expand Down Expand Up @@ -53,20 +54,33 @@ class _MyAppState extends State<MyApp> {
Future<void> _authenticate() async {
bool authenticated = false;
try {
setState(() {
_isAuthenticating = true;
_authorized = 'Authenticating';
});
authenticated = await auth.authenticateWithBiometrics(
localizedReason: 'Scan your fingerprint to authenticate',
useErrorDialogs: true,
stickyAuth: true);
setState(() {
_isAuthenticating = false;
_authorized = 'Authenticating';
});
} on PlatformException catch (e) {
print(e);
}
if (!mounted) return;

final String message = authenticated ? 'Authorized' : 'Not Authorized';
setState(() {
_authorized = authenticated ? 'Authorized' : 'Not Authorized';
_authorized = message;
});
}

void _cancelAuthentication() {
auth.stopAuthentication();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
Expand All @@ -91,8 +105,9 @@ class _MyAppState extends State<MyApp> {
),
Text('Current State: $_authorized\n'),
RaisedButton(
child: const Text('Authenticate'),
onPressed: _authenticate,
child: Text(_isAuthenticating ? 'Cancel' : 'Authenticate'),
onPressed:
_isAuthenticating ? _cancelAuthentication : _authenticate,
)
])),
));
Expand Down
12 changes: 12 additions & 0 deletions packages/local_auth/lib/local_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ class LocalAuthentication {
'authenticateWithBiometrics', args);
}

/// Returns true if auth was cancelled successfully.
/// This api only works for Android.
/// Returns false if there was some error or no auth in progress.
///
/// Returns [Future] bool true or false:
Future<bool> stopAuthentication() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, how would the Dart app decide to call this?

Copy link
Contributor Author

@karan-rawal karan-rawal Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For devices with in-display fingerprint sensor(assuming that authentication is in progress), if user clicks on any other button on the screen, the fingerprint scanning will still be in progress. Hence, we needed an API to stop the authentication in such scenarios.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the response!

If user clicks on any other button on the screen

This is the part that I don't get. Why would I allow user to click anything in my app if there's authentication in progress? The user is not authenticated so should not be interacting with the app at all. I was assuming that on-screen auth will show exactly the same dialog. The only difference would be where you touch.

Could you please point me to a video or an article of some sort which shows cancelAuthentication in action?

Copy link
Contributor Author

@karan-rawal karan-rawal Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mehmetf For in-display fingerprint scanners, we don't get any popup. We just get the fingerprint scanner icon.

http://tiny.cc/e345ez

Assuming current behaviour of the plugin
In the link, you can see that we only get the fingerprint scanner icon and no popup. The user can still type the passcode, and click done. When user clicks done, and we navigate to another screen the authentication is still going on. The plugin doesn't provide any mechanism to cancel this authentication.

So this PR should solve the above problem.

I hope I was clear in explaining. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting.

Something is broken in this flow though.

  • The image shows FP and a PIN and user can choose to authenticate either way.
  • If both of these are not appearing due to this plugin then who is displaying the PIN entry method? That seems wrong. So, I assume both of these are appearing due to the plugin calling biometric.authenticate.
  • So, assuming biometric API is showing the PIN entry, if user enters PIN instead of FP, why would biometric.authenticate not return "success"? Why would we still expect a fingerprint scan?

I am OK adding this API because it is supported on Android. However the use case you are describing seems very very wrong to me. The app should not be responsible for cancelling authentication if the user already authenticated.

If you agree with me, could you take some time to research how the Android API is supposed to be used? In particular why would PIN entry not run success/failure callbacks? (https://github.com/flutter/plugins/blob/master/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java#L142)

Copy link
Contributor Author

@karan-rawal karan-rawal Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mehmetf Thanks for your quick response.

  1. The screenshot is not from my app but explains the exact scenario we need in our app. The screenshot is from oneplus's app locker screen.
  2. The PIN entry is not due to the plugin. It's the app's screen having PIN entry, and biometric.authenticate just overlays the fingerprint icon on top of it for scanning (The user can still interact with other part of the screen though).
  3. Because the biometric API is not responsible for showing the PIN entry, the biometric.authenticate doesn't know anything about it. There's no relation between them. :)

I'm not sure if the use case is wrong, but I have seen this flow in a lot of apps.
Just to add, the biometric API does show fingerprint dialog for physical fingerprint sensors. But it just shows fingerprint icon overlay for in-screen fingerprint sensors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see!

OK that makes more sense. So basically you app is offering two auth methods simultaneously and you want to cancel one if the user chooses the other one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mehmetf Exactly. :)

if (_platform.isAndroid) {
return _channel.invokeMethod<bool>('stopAuthentication');
}
return Future<bool>.sync(() => true);
}

/// Returns true if device is capable of checking biometrics
///
/// Returns a [Future] bool true or false:
Expand Down
4 changes: 2 additions & 2 deletions packages/local_auth/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS device authentication sensors
such as Fingerprint Reader and Touch ID.
author: Flutter Team <[email protected]>
homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth
version: 0.6.0+3
version: 0.6.1

flutter:
plugin:
Expand All @@ -16,7 +16,7 @@ dependencies:
sdk: flutter
meta: ^1.0.5
intl: ">=0.15.1 <0.17.0"
platform: ^2.0.0
platform: ^2.0.0

dev_dependencies:
flutter_test:
Expand Down