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

[local_auth] Fix deviceSupportsBiometrics returning false on iOS when present biometric hardware is not enrolled. #5329

Merged
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
5 changes: 5 additions & 0 deletions packages/local_auth/local_auth_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.0.4

* Fixes `deviceSupportsBiometrics` to return true when biometric hardware
is available but not enrolled.

## 1.0.3

* Adopts `Object.hash`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 50;
objects = {

/* Begin PBXBuildFile section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,209 @@ - (void)testSkippedLocalizedFallbackTitle {
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testDeviceSupportsBiometrics_withEnrolledHardware {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 {
if (@available(iOS 11, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call =
[FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}

- (void)testDeviceSupportsBiometrics_withNoBiometricHardware {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertFalse([result boolValue]);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testGetEnrolledBiometrics_withFaceID_iOS11 {
if (@available(iOS 11, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"face");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}

- (void)testGetEnrolledBiometrics_withTouchID_iOS11 {
if (@available(iOS 11, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"fingerprint");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}

- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 {
if (@available(iOS 11, *)) {
return;
}
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"fingerprint");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 {
if (@available(iOS 11, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 0);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}
@end
26 changes: 13 additions & 13 deletions packages/local_auth/local_auth_ios/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
_SupportState _supportState = _SupportState.unknown;
bool? _canCheckBiometrics;
List<BiometricType>? _availableBiometrics;
List<BiometricType>? _enrolledBiometrics;
String _authorized = 'Not Authorized';
bool _isAuthenticating = false;

Expand All @@ -40,38 +40,38 @@ class _MyAppState extends State<MyApp> {
}

Future<void> _checkBiometrics() async {
late bool canCheckBiometrics;
late bool deviceSupportsBiometrics;
try {
canCheckBiometrics =
(await LocalAuthPlatform.instance.getEnrolledBiometrics()).isNotEmpty;
deviceSupportsBiometrics =
await LocalAuthPlatform.instance.deviceSupportsBiometrics();
} on PlatformException catch (e) {
canCheckBiometrics = false;
deviceSupportsBiometrics = false;
print(e);
}
if (!mounted) {
return;
}

setState(() {
_canCheckBiometrics = canCheckBiometrics;
_canCheckBiometrics = deviceSupportsBiometrics;
});
}

Future<void> _getEnrolledBiometrics() async {
late List<BiometricType> availableBiometrics;
late List<BiometricType> enrolledBiometrics;
try {
availableBiometrics =
enrolledBiometrics =
await LocalAuthPlatform.instance.getEnrolledBiometrics();
} on PlatformException catch (e) {
availableBiometrics = <BiometricType>[];
enrolledBiometrics = <BiometricType>[];
print(e);
}
if (!mounted) {
return;
}

setState(() {
_availableBiometrics = availableBiometrics;
_enrolledBiometrics = enrolledBiometrics;
});
}

Expand Down Expand Up @@ -173,15 +173,15 @@ class _MyAppState extends State<MyApp> {
else
const Text('This device is not supported'),
const Divider(height: 100),
Text('Can check biometrics: $_canCheckBiometrics\n'),
Text('Device supports biometrics: $_canCheckBiometrics\n'),
ElevatedButton(
child: const Text('Check biometrics'),
onPressed: _checkBiometrics,
),
const Divider(height: 100),
Text('Available biometrics: $_availableBiometrics\n'),
Text('Enrolled biometrics: $_enrolledBiometrics\n'),
ElevatedButton(
child: const Text('Get available biometrics'),
child: const Text('Get enrolled biometrics'),
onPressed: _getEnrolledBiometrics,
),
const Divider(height: 100),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
} else {
[self authenticate:call.arguments withFlutterResult:result];
}
} else if ([@"getAvailableBiometrics" isEqualToString:call.method]) {
[self getAvailableBiometrics:result];
} else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) {
[self getEnrolledBiometrics:result];
} else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) {
[self deviceSupportsBiometrics:result];
} else if ([@"isDeviceSupported" isEqualToString:call.method]) {
result(@YES);
} else {
Expand Down Expand Up @@ -93,14 +95,41 @@ - (void)alertMessage:(NSString *)message
completion:nil];
}

- (void)getAvailableBiometrics:(FlutterResult)result {
- (void)deviceSupportsBiometrics:(FlutterResult)result {
LAContext *context = self.createAuthContext;
NSError *authError = nil;
// Check if authentication with biometrics is possible.
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
error:&authError]) {
if (authError == nil) {
result(@YES);
return;
}
}
// If not, check if it is because no biometrics are enrolled (but still present).
if (authError != nil) {
if (@available(iOS 11, *)) {
if (authError.code == LAErrorBiometryNotEnrolled) {
result(@YES);
return;
}
} else if (authError.code == LAErrorTouchIDNotEnrolled) {
result(@YES);
return;
}
}

result(@NO);
}

- (void)getEnrolledBiometrics:(FlutterResult)result {
LAContext *context = self.createAuthContext;
NSError *authError = nil;
NSMutableArray<NSString *> *biometrics = [[NSMutableArray<NSString *> alloc] init];
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
error:&authError]) {
if (authError == nil) {
if (@available(iOS 11.0.1, *)) {
if (@available(iOS 11, *)) {
if (context.biometryType == LABiometryTypeFaceID) {
[biometrics addObject:@"face"];
} else if (context.biometryType == LABiometryTypeTouchID) {
Expand All @@ -110,8 +139,6 @@ - (void)getAvailableBiometrics:(FlutterResult)result {
[biometrics addObject:@"fingerprint"];
}
}
} else if (authError.code == LAErrorTouchIDNotEnrolled) {
[biometrics addObject:@"undefined"];
}
result(biometrics);
}
Expand Down
7 changes: 3 additions & 4 deletions packages/local_auth/local_auth_ios/lib/local_auth_ios.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ class LocalAuthIOS extends LocalAuthPlatform {

@override
Future<bool> deviceSupportsBiometrics() async {
return (await getEnrolledBiometrics()).isNotEmpty;
return (await _channel.invokeMethod<bool>('deviceSupportsBiometrics')) ??
false;
}

@override
Future<List<BiometricType>> getEnrolledBiometrics() async {
final List<String> result = (await _channel.invokeListMethod<String>(
'getAvailableBiometrics',
'getEnrolledBiometrics',
)) ??
<String>[];
final List<BiometricType> biometrics = <BiometricType>[];
Expand All @@ -70,8 +71,6 @@ class LocalAuthIOS extends LocalAuthPlatform {
case 'iris':
biometrics.add(BiometricType.iris);
break;
case 'undefined':
break;
}
}
return biometrics;
Expand Down
Loading