-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[local_auth] Fix failed biometric authentication not throwing error #6821
Changes from 6 commits
684790a
89ba69c
823843d
3da994d
714907d
abb8e68
b7bbcab
cc7e32f
0bcffeb
5a545bd
1a053b2
2a64876
2583cc4
40a00cf
0ac9aee
699a612
b483520
737bbd1
f62195a
56d4969
99e34ca
c70c8bc
42682f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,7 +124,7 @@ - (void)testFailedAuthWithBiometrics { | |
void (^reply)(BOOL, NSError *); | ||
[invocation getArgument:&reply atIndex:4]; | ||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ | ||
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); | ||
reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); | ||
}); | ||
}; | ||
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) | ||
|
@@ -136,17 +136,112 @@ - (void)testFailedAuthWithBiometrics { | |
@"localizedReason" : reason, | ||
}]; | ||
|
||
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; | ||
[plugin handleMethodCall:call | ||
result:^(id _Nullable result) { | ||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); | ||
[expectation fulfill]; | ||
}]; | ||
[self waitForExpectationsWithTimeout:kTimeout handler:nil]; | ||
} | ||
|
||
- (void)testFailedWithKnownErrorCode { | ||
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; | ||
id mockAuthContext = OCMClassMock([LAContext class]); | ||
plugin.authContextOverrides = @[ mockAuthContext ]; | ||
|
||
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" | ||
arguments:@{ | ||
@"success" : @NO, | ||
@"error" : [NSError errorWithDomain:@"error" | ||
code:LAErrorPasscodeNotSet | ||
userInfo:nil], | ||
@"stickyAuth" : @NO, | ||
}]; | ||
|
||
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; | ||
[plugin handleMethodCall:call | ||
result:^(id _Nullable result) { | ||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); | ||
[expectation fulfill]; | ||
}]; | ||
[self waitForExpectationsWithTimeout:kTimeout handler:nil]; | ||
} | ||
|
||
- (void)testFailedWithUnknownErrorCode { | ||
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; | ||
id mockAuthContext = OCMClassMock([LAContext class]); | ||
plugin.authContextOverrides = @[ mockAuthContext ]; | ||
|
||
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" | ||
arguments:@{ | ||
@"success" : @NO, | ||
@"error" : [NSError errorWithDomain:@"error" | ||
code:-9999 | ||
userInfo:nil], | ||
@"stickyAuth" : @NO, | ||
}]; | ||
|
||
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; | ||
[plugin handleMethodCall:call | ||
result:^(id _Nullable result) { | ||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); | ||
[expectation fulfill]; | ||
}]; | ||
[self waitForExpectationsWithTimeout:kTimeout handler:nil]; | ||
} | ||
|
||
- (void)testHandleAuthReplyFailedWithSystemCancel { | ||
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; | ||
id mockAuthContext = OCMClassMock([LAContext class]); | ||
plugin.authContextOverrides = @[ mockAuthContext ]; | ||
|
||
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" | ||
arguments:@{ | ||
@"success" : @NO, | ||
@"error" : [NSError errorWithDomain:@"error" | ||
code:LAErrorSystemCancel | ||
userInfo:nil], | ||
@"stickyAuth" : @NO, | ||
}]; | ||
|
||
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)testHandleAuthReplySucceeded { | ||
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; | ||
id mockAuthContext = OCMClassMock([LAContext class]); | ||
plugin.authContextOverrides = @[ mockAuthContext ]; | ||
|
||
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"handleAuthReplyWithSuccess" | ||
arguments:@{ | ||
@"success" : @YES | ||
}]; | ||
|
||
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]); | ||
XCTAssertTrue([result boolValue]); | ||
[expectation fulfill]; | ||
}]; | ||
[self waitForExpectationsWithTimeout:kTimeout handler:nil]; | ||
} | ||
|
||
|
||
- (void)testFailedAuthWithoutBiometrics { | ||
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; | ||
id mockAuthContext = OCMClassMock([LAContext class]); | ||
|
@@ -163,7 +258,7 @@ - (void)testFailedAuthWithoutBiometrics { | |
void (^reply)(BOOL, NSError *); | ||
[invocation getArgument:&reply atIndex:4]; | ||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ | ||
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); | ||
reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); | ||
}); | ||
}; | ||
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) | ||
|
@@ -179,8 +274,7 @@ - (void)testFailedAuthWithoutBiometrics { | |
[plugin handleMethodCall:call | ||
result:^(id _Nullable result) { | ||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isKindOfClass:[NSNumber class]]); | ||
XCTAssertFalse([result boolValue]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean to change these result blocks? |
||
XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); | ||
[expectation fulfill]; | ||
}]; | ||
[self waitForExpectationsWithTimeout:kTimeout handler:nil]; | ||
|
@@ -203,7 +297,7 @@ - (void)testLocalizedFallbackTitle { | |
void (^reply)(BOOL, NSError *); | ||
[invocation getArgument:&reply atIndex:4]; | ||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ | ||
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); | ||
reply(NO, [NSError errorWithDomain:@"error" code:LAErrorUserFallback userInfo:nil]); | ||
}); | ||
}; | ||
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) | ||
|
@@ -220,10 +314,9 @@ - (void)testLocalizedFallbackTitle { | |
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; | ||
[plugin handleMethodCall:call | ||
result:^(id _Nullable result) { | ||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isKindOfClass:[NSNumber class]]); | ||
OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); | ||
XCTAssertFalse([result boolValue]); | ||
Comment on lines
-223
to
-226
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. |
||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); | ||
[expectation fulfill]; | ||
}]; | ||
[self waitForExpectationsWithTimeout:kTimeout handler:nil]; | ||
|
@@ -245,7 +338,7 @@ - (void)testSkippedLocalizedFallbackTitle { | |
void (^reply)(BOOL, NSError *); | ||
[invocation getArgument:&reply atIndex:4]; | ||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ | ||
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); | ||
reply(NO, [NSError errorWithDomain:@"error" code:LAErrorUserFallback userInfo:nil]); | ||
}); | ||
}; | ||
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) | ||
|
@@ -260,10 +353,9 @@ - (void)testSkippedLocalizedFallbackTitle { | |
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; | ||
[plugin handleMethodCall:call | ||
result:^(id _Nullable result) { | ||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isKindOfClass:[NSNumber class]]); | ||
OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); | ||
XCTAssertFalse([result boolValue]); | ||
Comment on lines
-263
to
-266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I changed it because in these tests, they were originally using a random errorCode of 99 which is not an enum value, so it was falling through the switch case and returning result(No) instead of throwing an error. I changed them to use appropriate error codes instead cuz I thought it would be more accurate to throw an error, but what do you think? Should I just leave it as it was before? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the second and third copy of this, the fact that it's simulating an error and asserting things about the error is a copypasta issue in these tests that I missed in review; there's no reason for a test about localized fallback title handling to be using the error path, or asserting anything other than the title part and the main thread. They were just created by copying and pasting from the closest test in the file it looks like. We should just update the dummy reply to a success path, and remove the irrelevant assertions. For the first, if we intentionally have a codepath that returns NO instead of throwing an error, we should ideally test both that and the error path. The random error code test simulates what would happen if Apple added a new error that we didn't handle yet, for instance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
+1, there should be a test for a new enum that we haven't handled yet. |
||
XCTAssertTrue([NSThread isMainThread]); | ||
XCTAssertTrue([result isMemberOfClass:[FlutterError class]]); | ||
[expectation fulfill]; | ||
}]; | ||
[self waitForExpectationsWithTimeout:kTimeout handler:nil]; | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -41,6 +41,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result | |||||
[self deviceSupportsBiometrics:result]; | ||||||
} else if ([@"isDeviceSupported" isEqualToString:call.method]) { | ||||||
result(@YES); | ||||||
} else if ([@"handleAuthReplyWithSuccess" isEqualToString:call.method]) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, why was this added? These should correspond to the messages coming from the dart API, like: plugins/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart Lines 78 to 79 in 2eb6165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I added it because thats how the other tests were testing methods - Ill test it another way then, good to know its just for dart API messages |
||||||
bool success = [call.arguments[@"success"] boolValue]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BOOL |
||||||
NSError* error = call.arguments[@"error"]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be!
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you double check by setting a break point here and inspecting the type of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
[self handleAuthReplyWithSuccess:success error:error flutterArguments:call.arguments flutterResult:result]; | ||||||
} else { | ||||||
result(FlutterMethodNotImplemented); | ||||||
} | ||||||
|
@@ -216,41 +220,35 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success | |||||
result(@YES); | ||||||
} else { | ||||||
switch (error.code) { | ||||||
case LAErrorSystemCancel: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could put this back to where it was at the bottom to avoid dirtying the git history and just add the |
||||||
if ([arguments[@"stickyAuth"] boolValue]) { | ||||||
self->_lastCallArgs = arguments; | ||||||
self->_lastResult = result; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think result(@yES) should only get returned if it's successful, so if the system cancels it should return no error and exit silently There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you explain what is I believe the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from what I can tell, stickyauth makes it so that when you leave then return to the app, the authentication screen persists. I didn't add it btw, I just moved the cases around As for the result, that makes sense - should it return result(@no) instead? Since otherwise it would imply the authentication was successful There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked into the doc. It's intended behavior that we don't call the |
||||||
} else { | ||||||
result(@NO); | ||||||
} | ||||||
return; | ||||||
case LAErrorPasscodeNotSet: | ||||||
#pragma clang diagnostic push | ||||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations" | ||||||
// TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when | ||||||
// iOS 10 support is dropped. The values are the same, only the names have changed. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is ios 10 dropped? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes (?), I was under the impression minimum ios version is 11.0 now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm, i thought it was ios 9 but i could remember it wrongly. Can you double check other plugins? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the flutter docs it says minimum version is 11 https://docs.flutter.dev/deployment/ios#:~:text=The%20minimum%20iOS%20version%20that,to%20the%20highest%20required%20version. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's the current flutter version (people could be using older flutter) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. quick search and found this - we are still at ios 9 here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huan is right, this version of the plugin should continue to work on iOS 9 until the minimum version is bumped to 11. The tests are passing because PRs only run on Flutter master, which is auto-migrating the example app to iOS 11 so there's no issue. I think that would even happen in stable too, which only gets run on post-submit. Trying to think how to force the app to run against the iOS 9 SDK in CI. I guess we just haven't run into issues related to this yet... cc @stuartmorgan There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a problem given the auto-migrate logic. I would suggest someone just do a mass-update of all the iOS implementation packages to Flutter 3.3 and iOS 11 now (or maybe 3.3 first, and then iOS 11 as a second pass if that's non-trivial due to dead code removal). That's the approach I've adopted for our old-Flutter-version support: when we update the N-2 stable version in CI, I mass-update all plugins to at least that minimum version, because we're no longer getting any automated testing that we aren't accidentally breaking people using older versions. (That kind of update doesn't cause any churn for users, because people still using old Flutter versions will just not be given the updates they can't use due to the resolver constraint on the Flutter version.) |
||||||
case LAErrorTouchIDNotAvailable: | ||||||
case LAErrorTouchIDNotEnrolled: | ||||||
case LAErrorTouchIDLockout: | ||||||
#pragma clang diagnostic pop | ||||||
case LAErrorAuthenticationFailed: | ||||||
case LAErrorBiometryNotAvailable: | ||||||
case LAErrorBiometryNotEnrolled: | ||||||
case LAErrorBiometryLockout: | ||||||
case LAErrorUserFallback: | ||||||
default: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when will this default be called? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I added the default so it would catch any unknown error codes and throw an error - the original issue was happening because an error code was thrown that wasnt listed and it was failing silently. Should I change my approach? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
can you inspect the error code and explicitly list here? The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the error code was
So I already added it to the list There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, we should handle all enums and remove the |
||||||
[self handleErrors:error flutterArguments:arguments withFlutterResult:result]; | ||||||
return; | ||||||
case LAErrorSystemCancel: | ||||||
if ([arguments[@"stickyAuth"] boolValue]) { | ||||||
self->_lastCallArgs = arguments; | ||||||
self->_lastResult = result; | ||||||
return; | ||||||
} | ||||||
} | ||||||
result(@NO); | ||||||
} | ||||||
} | ||||||
|
||||||
|
||||||
- (void)handleErrors:(NSError *)authError | ||||||
flutterArguments:(NSDictionary *)arguments | ||||||
withFlutterResult:(FlutterResult)result { | ||||||
NSString *errorCode = @"NotAvailable"; | ||||||
switch (authError.code) { | ||||||
case LAErrorPasscodeNotSet: | ||||||
#pragma clang diagnostic push | ||||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations" | ||||||
// TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when | ||||||
// iOS 10 support is dropped. The values are the same, only the names have changed. | ||||||
case LAErrorTouchIDNotEnrolled: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be put back too then? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, missed that thanks! |
||||||
#pragma clang diagnostic pop | ||||||
case LAErrorBiometryNotEnrolled: | ||||||
if ([arguments[@"useErrorDialogs"] boolValue]) { | ||||||
[self alertMessage:arguments[@"goToSettingDescriptionIOS"] | ||||||
firstButton:arguments[@"okButton"] | ||||||
|
@@ -260,12 +258,7 @@ - (void)handleErrors:(NSError *)authError | |||||
} | ||||||
errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; | ||||||
break; | ||||||
#pragma clang diagnostic push | ||||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations" | ||||||
// TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when | ||||||
// iOS 10 support is dropped. The values are the same, only the names have changed. | ||||||
case LAErrorTouchIDLockout: | ||||||
#pragma clang diagnostic pop | ||||||
case LAErrorBiometryLockout: | ||||||
[self alertMessage:arguments[@"lockOut"] | ||||||
firstButton:arguments[@"okButton"] | ||||||
flutterResult:result | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump this.