Skip to content

Commit 013763d

Browse files
authored
[webview_flutter] Support for handling basic authentication requests (#5727)
## Description This pull request exposes the Android and iOS HTTP Basic Authentication feature to users of the `webview_flutter` plugin. It is the final PR in a sequence of PRs. Previous PRs are #5362, #5454 and #5455. Issues fixed by PR: Closes flutter/flutter#83556
1 parent 60a1ffc commit 013763d

File tree

11 files changed

+244
-65
lines changed

11 files changed

+244
-65
lines changed

packages/webview_flutter/webview_flutter/CHANGELOG.md

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

3+
* Adds support for HTTP basic authentication. See `NavigationDelegate(onReceivedHttpAuthRequest)`.
34
* Updates support matrix in README to indicate that iOS 11 is no longer supported.
45
* Clients on versions of Flutter that still support iOS 11 can continue to use this
56
package with iOS 11, but will not receive any further updates to the iOS implementation.

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ Future<void> main() async {
3232
request.response.writeln('${request.headers}');
3333
} else if (request.uri.path == '/favicon.ico') {
3434
request.response.statusCode = HttpStatus.notFound;
35+
} else if (request.uri.path == '/http-basic-authentication') {
36+
final List<String>? authHeader =
37+
request.headers[HttpHeaders.authorizationHeader];
38+
if (authHeader != null) {
39+
final String encodedCredential = authHeader.first.split(' ')[1];
40+
final String credential =
41+
String.fromCharCodes(base64Decode(encodedCredential));
42+
if (credential == 'user:password') {
43+
request.response.writeln('Authorized');
44+
} else {
45+
request.response.headers.add(
46+
HttpHeaders.wwwAuthenticateHeader, 'Basic realm="Test realm"');
47+
request.response.statusCode = HttpStatus.unauthorized;
48+
}
49+
} else {
50+
request.response.headers
51+
.add(HttpHeaders.wwwAuthenticateHeader, 'Basic realm="Test realm"');
52+
request.response.statusCode = HttpStatus.unauthorized;
53+
}
3554
} else {
3655
fail('unexpected request: ${request.method} ${request.uri}');
3756
}
@@ -41,6 +60,7 @@ Future<void> main() async {
4160
final String primaryUrl = '$prefixUrl/hello.txt';
4261
final String secondaryUrl = '$prefixUrl/secondary.txt';
4362
final String headersUrl = '$prefixUrl/headers';
63+
final String basicAuthUrl = '$prefixUrl/http-basic-authentication';
4464

4565
testWidgets('loadRequest', (WidgetTester tester) async {
4666
final Completer<void> pageFinished = Completer<void>();
@@ -52,7 +72,6 @@ Future<void> main() async {
5272
unawaited(controller.loadRequest(Uri.parse(primaryUrl)));
5373

5474
await tester.pumpWidget(WebViewWidget(controller: controller));
55-
5675
await pageFinished.future;
5776

5877
final String? currentUrl = await controller.currentUrl();
@@ -761,6 +780,54 @@ Future<void> main() async {
761780

762781
await expectLater(urlChangeCompleter.future, completion(secondaryUrl));
763782
});
783+
784+
testWidgets('can receive HTTP basic auth requests',
785+
(WidgetTester tester) async {
786+
final Completer<void> authRequested = Completer<void>();
787+
final WebViewController controller = WebViewController();
788+
789+
unawaited(
790+
controller.setNavigationDelegate(
791+
NavigationDelegate(
792+
onHttpAuthRequest: (HttpAuthRequest request) =>
793+
authRequested.complete(),
794+
),
795+
),
796+
);
797+
798+
await tester.pumpWidget(WebViewWidget(controller: controller));
799+
800+
unawaited(controller.loadRequest(Uri.parse(basicAuthUrl)));
801+
802+
await expectLater(authRequested.future, completes);
803+
});
804+
805+
testWidgets('can authenticate to HTTP basic auth requests',
806+
(WidgetTester tester) async {
807+
final WebViewController controller = WebViewController();
808+
final Completer<void> pageFinished = Completer<void>();
809+
810+
unawaited(
811+
controller.setNavigationDelegate(
812+
NavigationDelegate(
813+
onHttpAuthRequest: (HttpAuthRequest request) => request.onProceed(
814+
const WebViewCredential(
815+
user: 'user',
816+
password: 'password',
817+
),
818+
),
819+
onPageFinished: (_) => pageFinished.complete(),
820+
onWebResourceError: (_) => fail('Authentication failed'),
821+
),
822+
),
823+
);
824+
825+
await tester.pumpWidget(WebViewWidget(controller: controller));
826+
827+
unawaited(controller.loadRequest(Uri.parse(basicAuthUrl)));
828+
829+
await expectLater(pageFinished.future, completes);
830+
});
764831
});
765832

766833
testWidgets('target _blank opens in same window',

packages/webview_flutter/webview_flutter/example/lib/main.dart

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ Page resource error:
173173
onUrlChange: (UrlChange change) {
174174
debugPrint('url change to ${change.url}');
175175
},
176+
onHttpAuthRequest: (HttpAuthRequest request) {
177+
openDialog(request);
178+
},
176179
),
177180
)
178181
..addJavaScriptChannel(
@@ -226,6 +229,62 @@ Page resource error:
226229
child: const Icon(Icons.favorite),
227230
);
228231
}
232+
233+
Future<void> openDialog(HttpAuthRequest httpRequest) async {
234+
final TextEditingController usernameTextController =
235+
TextEditingController();
236+
final TextEditingController passwordTextController =
237+
TextEditingController();
238+
239+
return showDialog(
240+
context: context,
241+
barrierDismissible: false,
242+
builder: (BuildContext context) {
243+
return AlertDialog(
244+
title: Text('${httpRequest.host}: ${httpRequest.realm ?? '-'}'),
245+
content: SingleChildScrollView(
246+
child: Column(
247+
mainAxisSize: MainAxisSize.min,
248+
children: <Widget>[
249+
TextField(
250+
decoration: const InputDecoration(labelText: 'Username'),
251+
autofocus: true,
252+
controller: usernameTextController,
253+
),
254+
TextField(
255+
decoration: const InputDecoration(labelText: 'Password'),
256+
controller: passwordTextController,
257+
),
258+
],
259+
),
260+
),
261+
actions: <Widget>[
262+
// Explicitly cancel the request on iOS as the OS does not emit new
263+
// requests when a previous request is pending.
264+
TextButton(
265+
onPressed: () {
266+
httpRequest.onCancel();
267+
Navigator.of(context).pop();
268+
},
269+
child: const Text('Cancel'),
270+
),
271+
TextButton(
272+
onPressed: () {
273+
httpRequest.onProceed(
274+
WebViewCredential(
275+
user: usernameTextController.text,
276+
password: passwordTextController.text,
277+
),
278+
);
279+
Navigator.of(context).pop();
280+
},
281+
child: const Text('Authenticate'),
282+
),
283+
],
284+
);
285+
},
286+
);
287+
}
229288
}
230289

231290
enum MenuOptions {
@@ -243,6 +302,7 @@ enum MenuOptions {
243302
transparentBackground,
244303
setCookie,
245304
logExample,
305+
basicAuthentication,
246306
}
247307

248308
class SampleMenu extends StatelessWidget {
@@ -288,6 +348,8 @@ class SampleMenu extends StatelessWidget {
288348
_onSetCookie();
289349
case MenuOptions.logExample:
290350
_onLogExample();
351+
case MenuOptions.basicAuthentication:
352+
_promptForUrl(context);
291353
}
292354
},
293355
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
@@ -348,6 +410,10 @@ class SampleMenu extends StatelessWidget {
348410
value: MenuOptions.logExample,
349411
child: Text('Log example'),
350412
),
413+
const PopupMenuItem<MenuOptions>(
414+
value: MenuOptions.basicAuthentication,
415+
child: Text('Basic Authentication Example'),
416+
),
351417
],
352418
);
353419
}
@@ -501,6 +567,38 @@ class SampleMenu extends StatelessWidget {
501567

502568
return webViewController.loadHtmlString(kLogExamplePage);
503569
}
570+
571+
Future<void> _promptForUrl(BuildContext context) {
572+
final TextEditingController urlTextController = TextEditingController();
573+
574+
return showDialog<String>(
575+
context: context,
576+
builder: (BuildContext context) {
577+
return AlertDialog(
578+
title: const Text('Input URL to visit'),
579+
content: TextField(
580+
decoration: const InputDecoration(labelText: 'URL'),
581+
autofocus: true,
582+
controller: urlTextController,
583+
),
584+
actions: <Widget>[
585+
TextButton(
586+
onPressed: () {
587+
if (urlTextController.text.isNotEmpty) {
588+
final Uri? uri = Uri.tryParse(urlTextController.text);
589+
if (uri != null && uri.scheme.isNotEmpty) {
590+
webViewController.loadRequest(uri);
591+
Navigator.pop(context);
592+
}
593+
}
594+
},
595+
child: const Text('Visit'),
596+
),
597+
],
598+
);
599+
},
600+
);
601+
}
504602
}
505603

506604
class NavigationControls extends StatelessWidget {

packages/webview_flutter/webview_flutter/example/pubspec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ dependencies:
1717
# The example app is bundled with the plugin so we use a path dependency on
1818
# the parent directory to use the current plugin's version.
1919
path: ../
20-
webview_flutter_android: ^3.12.0
21-
webview_flutter_wkwebview: ^3.9.0
20+
webview_flutter_android: ^3.13.0
21+
webview_flutter_wkwebview: ^3.10.0
2222

2323
dev_dependencies:
2424
build_runner: ^2.1.5
@@ -27,7 +27,7 @@ dev_dependencies:
2727
sdk: flutter
2828
integration_test:
2929
sdk: flutter
30-
webview_flutter_platform_interface: ^2.3.0
30+
webview_flutter_platform_interface: ^2.7.0
3131

3232
flutter:
3333
uses-material-design: true

packages/webview_flutter/webview_flutter/example/test/main_test.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,9 @@ class FakeNavigationDelegate extends PlatformNavigationDelegate {
116116

117117
@override
118118
Future<void> setOnUrlChange(UrlChangeCallback onUrlChange) async {}
119+
120+
@override
121+
Future<void> setOnHttpAuthRequest(
122+
HttpAuthRequestCallback handler,
123+
) async {}
119124
}

packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class NavigationDelegate {
3939
///
4040
/// {@template webview_fluttter.NavigationDelegate.constructor}
4141
/// `onUrlChange`: invoked when the underlying web view changes to a new url.
42+
/// `onHttpAuthRequest`: invoked when the web view is requesting authentication.
4243
/// {@endtemplate}
4344
NavigationDelegate({
4445
FutureOr<NavigationDecision> Function(NavigationRequest request)?
@@ -48,6 +49,7 @@ class NavigationDelegate {
4849
void Function(int progress)? onProgress,
4950
void Function(WebResourceError error)? onWebResourceError,
5051
void Function(UrlChange change)? onUrlChange,
52+
void Function(HttpAuthRequest request)? onHttpAuthRequest,
5153
}) : this.fromPlatformCreationParams(
5254
const PlatformNavigationDelegateCreationParams(),
5355
onNavigationRequest: onNavigationRequest,
@@ -56,6 +58,7 @@ class NavigationDelegate {
5658
onProgress: onProgress,
5759
onWebResourceError: onWebResourceError,
5860
onUrlChange: onUrlChange,
61+
onHttpAuthRequest: onHttpAuthRequest,
5962
);
6063

6164
/// Constructs a [NavigationDelegate] from creation params for a specific
@@ -98,6 +101,7 @@ class NavigationDelegate {
98101
void Function(int progress)? onProgress,
99102
void Function(WebResourceError error)? onWebResourceError,
100103
void Function(UrlChange change)? onUrlChange,
104+
void Function(HttpAuthRequest request)? onHttpAuthRequest,
101105
}) : this.fromPlatform(
102106
PlatformNavigationDelegate(params),
103107
onNavigationRequest: onNavigationRequest,
@@ -106,6 +110,7 @@ class NavigationDelegate {
106110
onProgress: onProgress,
107111
onWebResourceError: onWebResourceError,
108112
onUrlChange: onUrlChange,
113+
onHttpAuthRequest: onHttpAuthRequest,
109114
);
110115

111116
/// Constructs a [NavigationDelegate] from a specific platform implementation.
@@ -119,6 +124,7 @@ class NavigationDelegate {
119124
this.onProgress,
120125
this.onWebResourceError,
121126
void Function(UrlChange change)? onUrlChange,
127+
HttpAuthRequestCallback? onHttpAuthRequest,
122128
}) {
123129
if (onNavigationRequest != null) {
124130
platform.setOnNavigationRequest(onNavigationRequest!);
@@ -138,6 +144,9 @@ class NavigationDelegate {
138144
if (onUrlChange != null) {
139145
platform.setOnUrlChange(onUrlChange);
140146
}
147+
if (onHttpAuthRequest != null) {
148+
platform.setOnHttpAuthRequest(onHttpAuthRequest);
149+
}
141150
}
142151

143152
/// Implementation of [PlatformNavigationDelegate] for the current platform.

packages/webview_flutter/webview_flutter/lib/webview_flutter.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'
66
show
7+
HttpAuthRequest,
78
JavaScriptConsoleMessage,
89
JavaScriptLogLevel,
910
JavaScriptMessage,
@@ -24,6 +25,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte
2425
WebResourceErrorCallback,
2526
WebResourceErrorType,
2627
WebViewCookie,
28+
WebViewCredential,
2729
WebViewPermissionResourceType,
2830
WebViewPlatform;
2931

packages/webview_flutter/webview_flutter/pubspec.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: webview_flutter
22
description: A Flutter plugin that provides a WebView widget on Android and iOS.
33
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
5-
version: 4.4.4
5+
version: 4.5.0
66

77
environment:
88
sdk: ">=3.0.0 <4.0.0"
@@ -19,9 +19,9 @@ flutter:
1919
dependencies:
2020
flutter:
2121
sdk: flutter
22-
webview_flutter_android: ^3.12.0
23-
webview_flutter_platform_interface: ^2.6.0
24-
webview_flutter_wkwebview: ^3.9.0
22+
webview_flutter_android: ^3.13.0
23+
webview_flutter_platform_interface: ^2.7.0
24+
webview_flutter_wkwebview: ^3.10.0
2525

2626
dev_dependencies:
2727
build_runner: ^2.1.5

packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ void main() {
8787

8888
verify(delegate.platform.setOnUrlChange(onUrlChange));
8989
});
90+
91+
test('onHttpAuthRequest', () {
92+
WebViewPlatform.instance = TestWebViewPlatform();
93+
94+
void onHttpAuthRequest(HttpAuthRequest request) {}
95+
96+
final NavigationDelegate delegate = NavigationDelegate(
97+
onHttpAuthRequest: onHttpAuthRequest,
98+
);
99+
100+
verify(delegate.platform.setOnHttpAuthRequest(onHttpAuthRequest));
101+
});
90102
});
91103
}
92104

0 commit comments

Comments
 (0)