Skip to content

Commit

Permalink
[webview_flutter] Support for handling basic authentication requests …
Browse files Browse the repository at this point in the history
…(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.
  • Loading branch information
JeroenWeener authored Dec 19, 2023
1 parent e2b5334 commit 3273017
Show file tree
Hide file tree
Showing 38 changed files with 2,366 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 3.10.0

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

## 3.9.4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ Future<void> main() async {
request.response.writeln('${request.headers}');
} else if (request.uri.path == '/favicon.ico') {
request.response.statusCode = HttpStatus.notFound;
} else if (request.uri.path == '/http-basic-authentication') {
final bool isAuthenticating = request.headers['Authorization'] != null;
if (isAuthenticating) {
request.response.writeln('Authorized');
} else {
request.response.headers
.add('WWW-Authenticate', 'Basic realm="Test realm"');
request.response.statusCode = HttpStatus.unauthorized;
}
} else {
fail('unexpected request: ${request.method} ${request.uri}');
}
Expand All @@ -43,6 +52,7 @@ Future<void> main() async {
final String primaryUrl = '$prefixUrl/hello.txt';
final String secondaryUrl = '$prefixUrl/secondary.txt';
final String headersUrl = '$prefixUrl/headers';
final String basicAuthUrl = '$prefixUrl/http-basic-authentication';

testWidgets(
'withWeakReferenceTo allows encapsulating class to be garbage collected',
Expand Down Expand Up @@ -1127,6 +1137,82 @@ Future<void> main() async {
});
});

testWidgets('can receive HTTP basic auth requests',
(WidgetTester tester) async {
final Completer<void> authRequested = Completer<void>();
final PlatformWebViewController controller = PlatformWebViewController(
const PlatformWebViewControllerCreationParams(),
);

final PlatformNavigationDelegate navigationDelegate =
PlatformNavigationDelegate(
const PlatformNavigationDelegateCreationParams(),
);
await navigationDelegate.setOnHttpAuthRequest(
(HttpAuthRequest request) => authRequested.complete());
await controller.setPlatformNavigationDelegate(navigationDelegate);

// Clear cache so that the auth request is always received and we don't get
// a cached response.
await controller.clearCache();

await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
return PlatformWebViewWidget(
WebKitWebViewWidgetCreationParams(controller: controller),
).build(context);
},
),
);

await controller.loadRequest(
LoadRequestParams(uri: Uri.parse(basicAuthUrl)),
);

await expectLater(authRequested.future, completes);
});

testWidgets('can reply to HTTP basic auth requests',
(WidgetTester tester) async {
final Completer<void> pageFinished = Completer<void>();
final PlatformWebViewController controller = PlatformWebViewController(
const PlatformWebViewControllerCreationParams(),
);

final PlatformNavigationDelegate navigationDelegate =
PlatformNavigationDelegate(
const PlatformNavigationDelegateCreationParams(),
);
await navigationDelegate.setOnPageFinished((_) => pageFinished.complete());
await navigationDelegate.setOnHttpAuthRequest(
(HttpAuthRequest request) => request.onProceed(
const WebViewCredential(user: 'user', password: 'password'),
),
);
await controller.setPlatformNavigationDelegate(navigationDelegate);

// Clear cache so that the auth request is always received and we don't get
// a cached response.
await controller.clearCache();

await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
return PlatformWebViewWidget(
WebKitWebViewWidgetCreationParams(controller: controller),
).build(context);
},
),
);

await controller.loadRequest(
LoadRequestParams(uri: Uri.parse(basicAuthUrl)),
);

await expectLater(pageFinished.future, completes);
});

testWidgets('launches with gestureNavigationEnabled on iOS',
(WidgetTester tester) async {
final WebKitWebViewController controller = WebKitWebViewController(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */; };
8F4FF94B29AC223F000A6586 /* FWFURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF94A29AC223F000A6586 /* FWFURLTests.m */; };
8F562F902A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */; };
8F562F922A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */; };
8F562F942A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */; };
8F78EAAA2A02CB9100C2E520 /* FWFErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */; };
8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; };
8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; };
Expand Down Expand Up @@ -81,6 +84,9 @@
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewFlutterWKWebViewExternalAPITests.m; sourceTree = "<group>"; };
8F4FF94A29AC223F000A6586 /* FWFURLTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLTests.m; sourceTree = "<group>"; };
8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLCredentialHostApiTests.m; sourceTree = "<group>"; };
8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLProtectionSpaceHostApiTests.m; sourceTree = "<group>"; };
8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLAuthenticationChallengeHostApiTests.m; sourceTree = "<group>"; };
8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFErrorTests.m; sourceTree = "<group>"; };
8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = "<group>"; };
8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -167,6 +173,9 @@
8FB79B902820BAC700C101D3 /* FWFUIViewHostApiTests.m */,
8FB79B962821985200C101D3 /* FWFObjectHostApiTests.m */,
8F4FF94A29AC223F000A6586 /* FWFURLTests.m */,
8F562F8F2A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m */,
8F562F912A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m */,
8F562F932A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m */,
8F78EAA92A02CB9100C2E520 /* FWFErrorTests.m */,
);
path = RunnerTests;
Expand Down Expand Up @@ -318,7 +327,7 @@
isa = PBXProject;
attributes = {
DefaultBuildSystemTypeForWorkspace = Original;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
68BDCAE823C3F7CB00D9C032 = {
Expand All @@ -327,7 +336,6 @@
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
DevelopmentTeam = 7624MWN53C;
};
F7151F73266057800028CB91 = {
CreatedOnToolsVersion = 12.5;
Expand Down Expand Up @@ -474,12 +482,15 @@
8FB79B852820A3A400C101D3 /* FWFUIDelegateHostApiTests.m in Sources */,
8FB79B972821985200C101D3 /* FWFObjectHostApiTests.m in Sources */,
8FB79B672820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m in Sources */,
8F562F942A56C07B00C2BED6 /* FWFURLAuthenticationChallengeHostApiTests.m in Sources */,
8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */,
8FB79B73282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m in Sources */,
8FB79B7928209D1300C101D3 /* FWFUserContentControllerHostApiTests.m in Sources */,
8F562F902A56C02D00C2BED6 /* FWFURLCredentialHostApiTests.m in Sources */,
8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */,
8FB79B6B28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m in Sources */,
8FB79B8F2820BAB300C101D3 /* FWFScrollViewHostApiTests.m in Sources */,
8F562F922A56C04F00C2BED6 /* FWFURLProtectionSpaceHostApiTests.m in Sources */,
8FB79B912820BAC700C101D3 /* FWFUIViewHostApiTests.m in Sources */,
8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */,
8FB79B6D2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m in Sources */,
Expand Down Expand Up @@ -689,6 +700,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 7624MWN53C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -715,6 +727,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 7624MWN53C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,58 @@ - (void)testWebViewWebContentProcessDidTerminate {
webViewIdentifier:1
completion:OCMOCK_ANY]);
}

- (void)testDidReceiveAuthenticationChallenge {
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];

FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager
identifier:0];
FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI =
[self mockFlutterApiWithManager:instanceManager];

OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI);

WKWebView *mockWebView = OCMClassMock([WKWebView class]);
[instanceManager addDartCreatedInstance:mockWebView withIdentifier:1];

NSURLAuthenticationChallenge *mockChallenge = OCMClassMock([NSURLAuthenticationChallenge class]);
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host"
port:0
protocol:nil
realm:@"realm"
authenticationMethod:nil];
OCMStub([mockChallenge protectionSpace]).andReturn(protectionSpace);
[instanceManager addDartCreatedInstance:mockChallenge withIdentifier:2];

NSURLCredential *credential = [NSURLCredential credentialWithUser:@"user"
password:@"password"
persistence:NSURLCredentialPersistenceNone];
[instanceManager addDartCreatedInstance:credential withIdentifier:5];

OCMStub([mockFlutterAPI
didReceiveAuthenticationChallengeForDelegateWithIdentifier:0
webViewIdentifier:1
challengeIdentifier:2
completion:
([OCMArg
invokeBlockWithArgs:
[FWFAuthenticationChallengeResponse
makeWithDisposition:
FWFNSUrlSessionAuthChallengeDispositionCancelAuthenticationChallenge
credentialIdentifier:@(5)],
[NSNull null], nil])]);

NSURLSessionAuthChallengeDisposition __block callbackDisposition = -1;
NSURLCredential *__block callbackCredential;
[mockDelegate webView:mockWebView
didReceiveAuthenticationChallenge:mockChallenge
completionHandler:^(NSURLSessionAuthChallengeDisposition disposition,
NSURLCredential *credential) {
callbackDisposition = disposition;
callbackCredential = credential;
}];

XCTAssertEqual(callbackDisposition, NSURLSessionAuthChallengeCancelAuthenticationChallenge);
XCTAssertEqualObjects(callbackCredential, credential);
}
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@import Flutter;
@import XCTest;
@import webview_flutter_wkwebview;

#import <OCMock/OCMock.h>

@interface FWFURLAuthenticationChallengeHostApiTests : XCTestCase

@end

@implementation FWFURLAuthenticationChallengeHostApiTests
- (void)testFlutterApiCreate {
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
FWFURLAuthenticationChallengeFlutterApiImpl *flutterApi =
[[FWFURLAuthenticationChallengeFlutterApiImpl alloc]
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
instanceManager:instanceManager];

flutterApi.api = OCMClassMock([FWFNSUrlAuthenticationChallengeFlutterApi class]);

NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host"
port:0
protocol:nil
realm:@"realm"
authenticationMethod:nil];

NSURLAuthenticationChallenge *mockChallenge = OCMClassMock([NSURLAuthenticationChallenge class]);
OCMStub([mockChallenge protectionSpace]).andReturn(protectionSpace);

[flutterApi createWithInstance:mockChallenge
protectionSpace:protectionSpace
completion:^(FlutterError *error){

}];

long identifier = [instanceManager identifierWithStrongReferenceForInstance:mockChallenge];
long protectionSpaceIdentifier =
[instanceManager identifierWithStrongReferenceForInstance:protectionSpace];
OCMVerify([flutterApi.api createWithIdentifier:identifier
protectionSpaceIdentifier:protectionSpaceIdentifier
completion:OCMOCK_ANY]);
}
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@import Flutter;
@import XCTest;
@import webview_flutter_wkwebview;

#import <OCMock/OCMock.h>

@interface FWFURLCredentialHostApiTests : XCTestCase
@end

@implementation FWFURLCredentialHostApiTests
- (void)testHostApiCreate {
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];

FWFURLCredentialHostApiImpl *hostApi = [[FWFURLCredentialHostApiImpl alloc]
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
instanceManager:instanceManager];

FlutterError *error;
[hostApi createWithUserWithIdentifier:0
user:@"user"
password:@"password"
persistence:FWFNSUrlCredentialPersistencePermanent
error:&error];
XCTAssertNil(error);

NSURLCredential *credential = (NSURLCredential *)[instanceManager instanceForIdentifier:0];
XCTAssertEqualObjects(credential.user, @"user");
XCTAssertEqualObjects(credential.password, @"password");
XCTAssertEqual(credential.persistence, NSURLCredentialPersistencePermanent);
}
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@import Flutter;
@import XCTest;
@import webview_flutter_wkwebview;

#import <OCMock/OCMock.h>

@interface FWFURLProtectionSpaceHostApiTests : XCTestCase
@end

@implementation FWFURLProtectionSpaceHostApiTests
- (void)testFlutterApiCreate {
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
FWFURLProtectionSpaceFlutterApiImpl *flutterApi = [[FWFURLProtectionSpaceFlutterApiImpl alloc]
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
instanceManager:instanceManager];

flutterApi.api = OCMClassMock([FWFNSUrlProtectionSpaceFlutterApi class]);

NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"host"
port:0
protocol:nil
realm:@"realm"
authenticationMethod:nil];
[flutterApi createWithInstance:protectionSpace
host:@"host"
realm:@"realm"
authenticationMethod:@"method"
completion:^(FlutterError *error){

}];

long identifier = [instanceManager identifierWithStrongReferenceForInstance:protectionSpace];
OCMVerify([flutterApi.api createWithIdentifier:identifier
host:@"host"
realm:@"realm"
authenticationMethod:@"method"
completion:OCMOCK_ANY]);
}
@end
Loading

0 comments on commit 3273017

Please sign in to comment.