Skip to content

Commit 3273017

Browse files
authored
[webview_flutter] Support for handling basic authentication requests (iOS) (flutter#5455)
Adds the iOS implementation for basic http authentication. This PR is part of a series of PRs that aim to close flutter#83556. The PR that contains all changes can be found at flutter/packages#4140.
1 parent e2b5334 commit 3273017

38 files changed

+2366
-132
lines changed

packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 3.10.0
22

3+
* Adds support for `PlatformNavigationDelegate.setOnHttpAuthRequest`.
34
* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0.
45

56
## 3.9.4

packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ Future<void> main() async {
3434
request.response.writeln('${request.headers}');
3535
} else if (request.uri.path == '/favicon.ico') {
3636
request.response.statusCode = HttpStatus.notFound;
37+
} else if (request.uri.path == '/http-basic-authentication') {
38+
final bool isAuthenticating = request.headers['Authorization'] != null;
39+
if (isAuthenticating) {
40+
request.response.writeln('Authorized');
41+
} else {
42+
request.response.headers
43+
.add('WWW-Authenticate', 'Basic realm="Test realm"');
44+
request.response.statusCode = HttpStatus.unauthorized;
45+
}
3746
} else {
3847
fail('unexpected request: ${request.method} ${request.uri}');
3948
}
@@ -43,6 +52,7 @@ Future<void> main() async {
4352
final String primaryUrl = '$prefixUrl/hello.txt';
4453
final String secondaryUrl = '$prefixUrl/secondary.txt';
4554
final String headersUrl = '$prefixUrl/headers';
55+
final String basicAuthUrl = '$prefixUrl/http-basic-authentication';
4656

4757
testWidgets(
4858
'withWeakReferenceTo allows encapsulating class to be garbage collected',
@@ -1127,6 +1137,82 @@ Future<void> main() async {
11271137
});
11281138
});
11291139

1140+
testWidgets('can receive HTTP basic auth requests',
1141+
(WidgetTester tester) async {
1142+
final Completer<void> authRequested = Completer<void>();
1143+
final PlatformWebViewController controller = PlatformWebViewController(
1144+
const PlatformWebViewControllerCreationParams(),
1145+
);
1146+
1147+
final PlatformNavigationDelegate navigationDelegate =
1148+
PlatformNavigationDelegate(
1149+
const PlatformNavigationDelegateCreationParams(),
1150+
);
1151+
await navigationDelegate.setOnHttpAuthRequest(
1152+
(HttpAuthRequest request) => authRequested.complete());
1153+
await controller.setPlatformNavigationDelegate(navigationDelegate);
1154+
1155+
// Clear cache so that the auth request is always received and we don't get
1156+
// a cached response.
1157+
await controller.clearCache();
1158+
1159+
await tester.pumpWidget(
1160+
Builder(
1161+
builder: (BuildContext context) {
1162+
return PlatformWebViewWidget(
1163+
WebKitWebViewWidgetCreationParams(controller: controller),
1164+
).build(context);
1165+
},
1166+
),
1167+
);
1168+
1169+
await controller.loadRequest(
1170+
LoadRequestParams(uri: Uri.parse(basicAuthUrl)),
1171+
);
1172+
1173+
await expectLater(authRequested.future, completes);
1174+
});
1175+
1176+
testWidgets('can reply to HTTP basic auth requests',
1177+
(WidgetTester tester) async {
1178+
final Completer<void> pageFinished = Completer<void>();
1179+
final PlatformWebViewController controller = PlatformWebViewController(
1180+
const PlatformWebViewControllerCreationParams(),
1181+
);
1182+
1183+
final PlatformNavigationDelegate navigationDelegate =
1184+
PlatformNavigationDelegate(
1185+
const PlatformNavigationDelegateCreationParams(),
1186+
);
1187+
await navigationDelegate.setOnPageFinished((_) => pageFinished.complete());
1188+
await navigationDelegate.setOnHttpAuthRequest(
1189+
(HttpAuthRequest request) => request.onProceed(
1190+
const WebViewCredential(user: 'user', password: 'password'),
1191+
),
1192+
);
1193+
await controller.setPlatformNavigationDelegate(navigationDelegate);
1194+
1195+
// Clear cache so that the auth request is always received and we don't get
1196+
// a cached response.
1197+
await controller.clearCache();
1198+
1199+
await tester.pumpWidget(
1200+
Builder(
1201+
builder: (BuildContext context) {
1202+
return PlatformWebViewWidget(
1203+
WebKitWebViewWidgetCreationParams(controller: controller),
1204+
).build(context);
1205+
},
1206+
),
1207+
);
1208+
1209+
await controller.loadRequest(
1210+
LoadRequestParams(uri: Uri.parse(basicAuthUrl)),
1211+
);
1212+
1213+
await expectLater(pageFinished.future, completes);
1214+
});
1215+
11301216
testWidgets('launches with gestureNavigationEnabled on iOS',
11311217
(WidgetTester tester) async {
11321218
final WebKitWebViewController controller = WebKitWebViewController(

packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
1212
8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */; };
1313
8F4FF94B29AC223F000A6586 /* FWFURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF94A29AC223F000A6586 /* FWFURLTests.m */; };
14+
8F562F902A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */; };
15+
8F562F922A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */; };
16+
8F562F942A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */; };
1417
8F78EAAA2A02CB9100C2E520 /* FWFErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */; };
1518
8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; };
1619
8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; };
@@ -81,6 +84,9 @@
8184
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
8285
8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewFlutterWKWebViewExternalAPITests.m; sourceTree = "<group>"; };
8386
8F4FF94A29AC223F000A6586 /* FWFURLTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLTests.m; sourceTree = "<group>"; };
87+
8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLCredentialHostApiTests.m; sourceTree = "<group>"; };
88+
8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLProtectionSpaceHostApiTests.m; sourceTree = "<group>"; };
89+
8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLAuthenticationChallengeHostApiTests.m; sourceTree = "<group>"; };
8490
8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFErrorTests.m; sourceTree = "<group>"; };
8591
8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = "<group>"; };
8692
8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = "<group>"; };
@@ -167,6 +173,9 @@
167173
8FB79B902820BAC700C101D3 /* FWFUIViewHostApiTests.m */,
168174
8FB79B962821985200C101D3 /* FWFObjectHostApiTests.m */,
169175
8F4FF94A29AC223F000A6586 /* FWFURLTests.m */,
176+
8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */,
177+
8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */,
178+
8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */,
170179
8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */,
171180
);
172181
path = RunnerTests;
@@ -318,7 +327,7 @@
318327
isa = PBXProject;
319328
attributes = {
320329
DefaultBuildSystemTypeForWorkspace = Original;
321-
LastUpgradeCheck = 1300;
330+
LastUpgradeCheck = 1430;
322331
ORGANIZATIONNAME = "The Flutter Authors";
323332
TargetAttributes = {
324333
68BDCAE823C3F7CB00D9C032 = {
@@ -327,7 +336,6 @@
327336
};
328337
97C146ED1CF9000F007C117D = {
329338
CreatedOnToolsVersion = 7.3.1;
330-
DevelopmentTeam = 7624MWN53C;
331339
};
332340
F7151F73266057800028CB91 = {
333341
CreatedOnToolsVersion = 12.5;
@@ -474,12 +482,15 @@
474482
8FB79B852820A3A400C101D3 /* FWFUIDelegateHostApiTests.m in Sources */,
475483
8FB79B972821985200C101D3 /* FWFObjectHostApiTests.m in Sources */,
476484
8FB79B672820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m in Sources */,
485+
8F562F942A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m in Sources */,
477486
8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */,
478487
8FB79B73282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m in Sources */,
479488
8FB79B7928209D1300C101D3 /* FWFUserContentControllerHostApiTests.m in Sources */,
489+
8F562F902A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m in Sources */,
480490
8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */,
481491
8FB79B6B28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m in Sources */,
482492
8FB79B8F2820BAB300C101D3 /* FWFScrollViewHostApiTests.m in Sources */,
493+
8F562F922A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m in Sources */,
483494
8FB79B912820BAC700C101D3 /* FWFUIViewHostApiTests.m in Sources */,
484495
8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */,
485496
8FB79B6D2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m in Sources */,
@@ -689,6 +700,7 @@
689700
buildSettings = {
690701
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
691702
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
703+
DEVELOPMENT_TEAM = 7624MWN53C;
692704
ENABLE_BITCODE = NO;
693705
FRAMEWORK_SEARCH_PATHS = (
694706
"$(inherited)",
@@ -715,6 +727,7 @@
715727
buildSettings = {
716728
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
717729
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
730+
DEVELOPMENT_TEAM = 7624MWN53C;
718731
ENABLE_BITCODE = NO;
719732
FRAMEWORK_SEARCH_PATHS = (
720733
"$(inherited)",

packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1300"
3+
LastUpgradeVersion = "1430"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFNavigationDelegateHostApiTests.m

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,58 @@ - (void)testWebViewWebContentProcessDidTerminate {
213213
webViewIdentifier:1
214214
completion:OCMOCK_ANY]);
215215
}
216+
217+
- (void)testDidReceiveAuthenticationChallenge {
218+
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
219+
220+
FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager
221+
identifier:0];
222+
FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI =
223+
[self mockFlutterApiWithManager:instanceManager];
224+
225+
OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI);
226+
227+
WKWebView *mockWebView = OCMClassMock([WKWebView class]);
228+
[instanceManager addDartCreatedInstance:mockWebView withIdentifier:1];
229+
230+
NSURLAuthenticationChallenge *mockChallenge = OCMClassMock([NSURLAuthenticationChallenge class]);
231+
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host"
232+
port:0
233+
protocol:nil
234+
realm:@"realm"
235+
authenticationMethod:nil];
236+
OCMStub([mockChallenge protectionSpace]).andReturn(protectionSpace);
237+
[instanceManager addDartCreatedInstance:mockChallenge withIdentifier:2];
238+
239+
NSURLCredential *credential = [NSURLCredential credentialWithUser:@"user"
240+
password:@"password"
241+
persistence:NSURLCredentialPersistenceNone];
242+
[instanceManager addDartCreatedInstance:credential withIdentifier:5];
243+
244+
OCMStub([mockFlutterAPI
245+
didReceiveAuthenticationChallengeForDelegateWithIdentifier:0
246+
webViewIdentifier:1
247+
challengeIdentifier:2
248+
completion:
249+
([OCMArg
250+
invokeBlockWithArgs:
251+
[FWFAuthenticationChallengeResponse
252+
makeWithDisposition:
253+
FWFNSUrlSessionAuthChallengeDispositionCancelAuthenticationChallenge
254+
credentialIdentifier:@(5)],
255+
[NSNull null], nil])]);
256+
257+
NSURLSessionAuthChallengeDisposition __block callbackDisposition = -1;
258+
NSURLCredential *__block callbackCredential;
259+
[mockDelegate webView:mockWebView
260+
didReceiveAuthenticationChallenge:mockChallenge
261+
completionHandler:^(NSURLSessionAuthChallengeDisposition disposition,
262+
NSURLCredential *credential) {
263+
callbackDisposition = disposition;
264+
callbackCredential = credential;
265+
}];
266+
267+
XCTAssertEqual(callbackDisposition, NSURLSessionAuthChallengeCancelAuthenticationChallenge);
268+
XCTAssertEqualObjects(callbackCredential, credential);
269+
}
216270
@end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import Flutter;
6+
@import XCTest;
7+
@import webview_flutter_wkwebview;
8+
9+
#import <OCMock/OCMock.h>
10+
11+
@interface FWFURLAuthenticationChallengeHostApiTests : XCTestCase
12+
13+
@end
14+
15+
@implementation FWFURLAuthenticationChallengeHostApiTests
16+
- (void)testFlutterApiCreate {
17+
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
18+
FWFURLAuthenticationChallengeFlutterApiImpl *flutterApi =
19+
[[FWFURLAuthenticationChallengeFlutterApiImpl alloc]
20+
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
21+
instanceManager:instanceManager];
22+
23+
flutterApi.api = OCMClassMock([FWFNSUrlAuthenticationChallengeFlutterApi class]);
24+
25+
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host"
26+
port:0
27+
protocol:nil
28+
realm:@"realm"
29+
authenticationMethod:nil];
30+
31+
NSURLAuthenticationChallenge *mockChallenge = OCMClassMock([NSURLAuthenticationChallenge class]);
32+
OCMStub([mockChallenge protectionSpace]).andReturn(protectionSpace);
33+
34+
[flutterApi createWithInstance:mockChallenge
35+
protectionSpace:protectionSpace
36+
completion:^(FlutterError *error){
37+
38+
}];
39+
40+
long identifier = [instanceManager identifierWithStrongReferenceForInstance:mockChallenge];
41+
long protectionSpaceIdentifier =
42+
[instanceManager identifierWithStrongReferenceForInstance:protectionSpace];
43+
OCMVerify([flutterApi.api createWithIdentifier:identifier
44+
protectionSpaceIdentifier:protectionSpaceIdentifier
45+
completion:OCMOCK_ANY]);
46+
}
47+
@end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import Flutter;
6+
@import XCTest;
7+
@import webview_flutter_wkwebview;
8+
9+
#import <OCMock/OCMock.h>
10+
11+
@interface FWFURLCredentialHostApiTests : XCTestCase
12+
@end
13+
14+
@implementation FWFURLCredentialHostApiTests
15+
- (void)testHostApiCreate {
16+
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
17+
18+
FWFURLCredentialHostApiImpl *hostApi = [[FWFURLCredentialHostApiImpl alloc]
19+
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
20+
instanceManager:instanceManager];
21+
22+
FlutterError *error;
23+
[hostApi createWithUserWithIdentifier:0
24+
user:@"user"
25+
password:@"password"
26+
persistence:FWFNSUrlCredentialPersistencePermanent
27+
error:&error];
28+
XCTAssertNil(error);
29+
30+
NSURLCredential *credential = (NSURLCredential *)[instanceManager instanceForIdentifier:0];
31+
XCTAssertEqualObjects(credential.user, @"user");
32+
XCTAssertEqualObjects(credential.password, @"password");
33+
XCTAssertEqual(credential.persistence, NSURLCredentialPersistencePermanent);
34+
}
35+
@end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import Flutter;
6+
@import XCTest;
7+
@import webview_flutter_wkwebview;
8+
9+
#import <OCMock/OCMock.h>
10+
11+
@interface FWFURLProtectionSpaceHostApiTests : XCTestCase
12+
@end
13+
14+
@implementation FWFURLProtectionSpaceHostApiTests
15+
- (void)testFlutterApiCreate {
16+
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
17+
FWFURLProtectionSpaceFlutterApiImpl *flutterApi = [[FWFURLProtectionSpaceFlutterApiImpl alloc]
18+
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
19+
instanceManager:instanceManager];
20+
21+
flutterApi.api = OCMClassMock([FWFNSUrlProtectionSpaceFlutterApi class]);
22+
23+
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host"
24+
port:0
25+
protocol:nil
26+
realm:@"realm"
27+
authenticationMethod:nil];
28+
[flutterApi createWithInstance:protectionSpace
29+
host:@"host"
30+
realm:@"realm"
31+
authenticationMethod:@"method"
32+
completion:^(FlutterError *error){
33+
34+
}];
35+
36+
long identifier = [instanceManager identifierWithStrongReferenceForInstance:protectionSpace];
37+
OCMVerify([flutterApi.api createWithIdentifier:identifier
38+
host:@"host"
39+
realm:@"realm"
40+
authenticationMethod:@"method"
41+
completion:OCMOCK_ANY]);
42+
}
43+
@end

0 commit comments

Comments
 (0)