From 013763dd574e5ef53c8f2f7d27d28b69c979c753 Mon Sep 17 00:00:00 2001 From: Jeroen Weener Date: Thu, 1 Feb 2024 15:44:54 +0100 Subject: [PATCH] [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 https://github.com/flutter/flutter/issues/83556 --- .../webview_flutter/CHANGELOG.md | 3 +- .../webview_flutter_test.dart | 69 ++++++++++++- .../webview_flutter/example/lib/main.dart | 98 +++++++++++++++++++ .../webview_flutter/example/pubspec.yaml | 6 +- .../example/test/main_test.dart | 5 + .../lib/src/navigation_delegate.dart | 9 ++ .../webview_flutter/lib/webview_flutter.dart | 2 + .../webview_flutter/pubspec.yaml | 8 +- .../test/navigation_delegate_test.dart | 12 +++ .../test/webview_flutter_export_test.dart | 41 ++++++++ .../test/webview_flutter_test.dart | 56 ----------- 11 files changed, 244 insertions(+), 65 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart delete mode 100644 packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index bd89d355267..2b10fb2efb1 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 4.5.0 +* Adds support for HTTP basic authentication. See `NavigationDelegate(onReceivedHttpAuthRequest)`. * Updates support matrix in README to indicate that iOS 11 is no longer supported. * Clients on versions of Flutter that still support iOS 11 can continue to use this package with iOS 11, but will not receive any further updates to the iOS implementation. diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index cee1b8b0344..d851aaeabeb 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -32,6 +32,25 @@ Future 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 List? authHeader = + request.headers[HttpHeaders.authorizationHeader]; + if (authHeader != null) { + final String encodedCredential = authHeader.first.split(' ')[1]; + final String credential = + String.fromCharCodes(base64Decode(encodedCredential)); + if (credential == 'user:password') { + request.response.writeln('Authorized'); + } else { + request.response.headers.add( + HttpHeaders.wwwAuthenticateHeader, 'Basic realm="Test realm"'); + request.response.statusCode = HttpStatus.unauthorized; + } + } else { + request.response.headers + .add(HttpHeaders.wwwAuthenticateHeader, 'Basic realm="Test realm"'); + request.response.statusCode = HttpStatus.unauthorized; + } } else { fail('unexpected request: ${request.method} ${request.uri}'); } @@ -41,6 +60,7 @@ Future 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('loadRequest', (WidgetTester tester) async { final Completer pageFinished = Completer(); @@ -52,7 +72,6 @@ Future main() async { unawaited(controller.loadRequest(Uri.parse(primaryUrl))); await tester.pumpWidget(WebViewWidget(controller: controller)); - await pageFinished.future; final String? currentUrl = await controller.currentUrl(); @@ -761,6 +780,54 @@ Future main() async { await expectLater(urlChangeCompleter.future, completion(secondaryUrl)); }); + + testWidgets('can receive HTTP basic auth requests', + (WidgetTester tester) async { + final Completer authRequested = Completer(); + final WebViewController controller = WebViewController(); + + unawaited( + controller.setNavigationDelegate( + NavigationDelegate( + onHttpAuthRequest: (HttpAuthRequest request) => + authRequested.complete(), + ), + ), + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + unawaited(controller.loadRequest(Uri.parse(basicAuthUrl))); + + await expectLater(authRequested.future, completes); + }); + + testWidgets('can authenticate to HTTP basic auth requests', + (WidgetTester tester) async { + final WebViewController controller = WebViewController(); + final Completer pageFinished = Completer(); + + unawaited( + controller.setNavigationDelegate( + NavigationDelegate( + onHttpAuthRequest: (HttpAuthRequest request) => request.onProceed( + const WebViewCredential( + user: 'user', + password: 'password', + ), + ), + onPageFinished: (_) => pageFinished.complete(), + onWebResourceError: (_) => fail('Authentication failed'), + ), + ), + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + unawaited(controller.loadRequest(Uri.parse(basicAuthUrl))); + + await expectLater(pageFinished.future, completes); + }); }); testWidgets('target _blank opens in same window', diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 92e85429e6b..d0a3f965c4e 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -173,6 +173,9 @@ Page resource error: onUrlChange: (UrlChange change) { debugPrint('url change to ${change.url}'); }, + onHttpAuthRequest: (HttpAuthRequest request) { + openDialog(request); + }, ), ) ..addJavaScriptChannel( @@ -226,6 +229,62 @@ Page resource error: child: const Icon(Icons.favorite), ); } + + Future openDialog(HttpAuthRequest httpRequest) async { + final TextEditingController usernameTextController = + TextEditingController(); + final TextEditingController passwordTextController = + TextEditingController(); + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text('${httpRequest.host}: ${httpRequest.realm ?? '-'}'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: const InputDecoration(labelText: 'Username'), + autofocus: true, + controller: usernameTextController, + ), + TextField( + decoration: const InputDecoration(labelText: 'Password'), + controller: passwordTextController, + ), + ], + ), + ), + actions: [ + // Explicitly cancel the request on iOS as the OS does not emit new + // requests when a previous request is pending. + TextButton( + onPressed: () { + httpRequest.onCancel(); + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + httpRequest.onProceed( + WebViewCredential( + user: usernameTextController.text, + password: passwordTextController.text, + ), + ); + Navigator.of(context).pop(); + }, + child: const Text('Authenticate'), + ), + ], + ); + }, + ); + } } enum MenuOptions { @@ -243,6 +302,7 @@ enum MenuOptions { transparentBackground, setCookie, logExample, + basicAuthentication, } class SampleMenu extends StatelessWidget { @@ -288,6 +348,8 @@ class SampleMenu extends StatelessWidget { _onSetCookie(); case MenuOptions.logExample: _onLogExample(); + case MenuOptions.basicAuthentication: + _promptForUrl(context); } }, itemBuilder: (BuildContext context) => >[ @@ -348,6 +410,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.logExample, child: Text('Log example'), ), + const PopupMenuItem( + value: MenuOptions.basicAuthentication, + child: Text('Basic Authentication Example'), + ), ], ); } @@ -501,6 +567,38 @@ class SampleMenu extends StatelessWidget { return webViewController.loadHtmlString(kLogExamplePage); } + + Future _promptForUrl(BuildContext context) { + final TextEditingController urlTextController = TextEditingController(); + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Input URL to visit'), + content: TextField( + decoration: const InputDecoration(labelText: 'URL'), + autofocus: true, + controller: urlTextController, + ), + actions: [ + TextButton( + onPressed: () { + if (urlTextController.text.isNotEmpty) { + final Uri? uri = Uri.tryParse(urlTextController.text); + if (uri != null && uri.scheme.isNotEmpty) { + webViewController.loadRequest(uri); + Navigator.pop(context); + } + } + }, + child: const Text('Visit'), + ), + ], + ); + }, + ); + } } class NavigationControls extends StatelessWidget { diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index efc363539b7..24b41bf5833 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -17,8 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - webview_flutter_android: ^3.12.0 - webview_flutter_wkwebview: ^3.9.0 + webview_flutter_android: ^3.13.0 + webview_flutter_wkwebview: ^3.10.0 dev_dependencies: build_runner: ^2.1.5 @@ -27,7 +27,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - webview_flutter_platform_interface: ^2.3.0 + webview_flutter_platform_interface: ^2.7.0 flutter: uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter/example/test/main_test.dart b/packages/webview_flutter/webview_flutter/example/test/main_test.dart index b1f36d364ab..c4b43428ec9 100644 --- a/packages/webview_flutter/webview_flutter/example/test/main_test.dart +++ b/packages/webview_flutter/webview_flutter/example/test/main_test.dart @@ -116,4 +116,9 @@ class FakeNavigationDelegate extends PlatformNavigationDelegate { @override Future setOnUrlChange(UrlChangeCallback onUrlChange) async {} + + @override + Future setOnHttpAuthRequest( + HttpAuthRequestCallback handler, + ) async {} } diff --git a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart index 9ca6981339d..a490fb1ca8b 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart @@ -39,6 +39,7 @@ class NavigationDelegate { /// /// {@template webview_fluttter.NavigationDelegate.constructor} /// `onUrlChange`: invoked when the underlying web view changes to a new url. + /// `onHttpAuthRequest`: invoked when the web view is requesting authentication. /// {@endtemplate} NavigationDelegate({ FutureOr Function(NavigationRequest request)? @@ -48,6 +49,7 @@ class NavigationDelegate { void Function(int progress)? onProgress, void Function(WebResourceError error)? onWebResourceError, void Function(UrlChange change)? onUrlChange, + void Function(HttpAuthRequest request)? onHttpAuthRequest, }) : this.fromPlatformCreationParams( const PlatformNavigationDelegateCreationParams(), onNavigationRequest: onNavigationRequest, @@ -56,6 +58,7 @@ class NavigationDelegate { onProgress: onProgress, onWebResourceError: onWebResourceError, onUrlChange: onUrlChange, + onHttpAuthRequest: onHttpAuthRequest, ); /// Constructs a [NavigationDelegate] from creation params for a specific @@ -98,6 +101,7 @@ class NavigationDelegate { void Function(int progress)? onProgress, void Function(WebResourceError error)? onWebResourceError, void Function(UrlChange change)? onUrlChange, + void Function(HttpAuthRequest request)? onHttpAuthRequest, }) : this.fromPlatform( PlatformNavigationDelegate(params), onNavigationRequest: onNavigationRequest, @@ -106,6 +110,7 @@ class NavigationDelegate { onProgress: onProgress, onWebResourceError: onWebResourceError, onUrlChange: onUrlChange, + onHttpAuthRequest: onHttpAuthRequest, ); /// Constructs a [NavigationDelegate] from a specific platform implementation. @@ -119,6 +124,7 @@ class NavigationDelegate { this.onProgress, this.onWebResourceError, void Function(UrlChange change)? onUrlChange, + HttpAuthRequestCallback? onHttpAuthRequest, }) { if (onNavigationRequest != null) { platform.setOnNavigationRequest(onNavigationRequest!); @@ -138,6 +144,9 @@ class NavigationDelegate { if (onUrlChange != null) { platform.setOnUrlChange(onUrlChange); } + if (onHttpAuthRequest != null) { + platform.setOnHttpAuthRequest(onHttpAuthRequest); + } } /// Implementation of [PlatformNavigationDelegate] for the current platform. diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index f4c294a4761..3b6495ca96f 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -4,6 +4,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' show + HttpAuthRequest, JavaScriptConsoleMessage, JavaScriptLogLevel, JavaScriptMessage, @@ -24,6 +25,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte WebResourceErrorCallback, WebResourceErrorType, WebViewCookie, + WebViewCredential, WebViewPermissionResourceType, WebViewPlatform; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index e3c8a014eaa..0572df372d5 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 4.4.4 +version: 4.5.0 environment: sdk: ">=3.0.0 <4.0.0" @@ -19,9 +19,9 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_android: ^3.12.0 - webview_flutter_platform_interface: ^2.6.0 - webview_flutter_wkwebview: ^3.9.0 + webview_flutter_android: ^3.13.0 + webview_flutter_platform_interface: ^2.7.0 + webview_flutter_wkwebview: ^3.10.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart index ffd97718bd4..c2bc6f230fa 100644 --- a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart @@ -87,6 +87,18 @@ void main() { verify(delegate.platform.setOnUrlChange(onUrlChange)); }); + + test('onHttpAuthRequest', () { + WebViewPlatform.instance = TestWebViewPlatform(); + + void onHttpAuthRequest(HttpAuthRequest request) {} + + final NavigationDelegate delegate = NavigationDelegate( + onHttpAuthRequest: onHttpAuthRequest, + ); + + verify(delegate.platform.setOnHttpAuthRequest(onHttpAuthRequest)); + }); }); } diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart new file mode 100644 index 00000000000..cf3dd5c89d3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart @@ -0,0 +1,41 @@ +// 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. + +// ignore_for_file: unnecessary_statements + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/webview_flutter.dart' as main_file; + +void main() { + group('webview_flutter', () { + test( + 'ensure webview_flutter.dart exports classes from platform interface', + () { + main_file.HttpAuthRequest; + main_file.JavaScriptConsoleMessage; + main_file.JavaScriptLogLevel; + main_file.JavaScriptMessage; + main_file.JavaScriptMode; + main_file.LoadRequestMethod; + main_file.NavigationDecision; + main_file.NavigationRequest; + main_file.NavigationRequestCallback; + main_file.PageEventCallback; + main_file.PlatformNavigationDelegateCreationParams; + main_file.PlatformWebViewControllerCreationParams; + main_file.PlatformWebViewCookieManagerCreationParams; + main_file.PlatformWebViewPermissionRequest; + main_file.PlatformWebViewWidgetCreationParams; + main_file.ProgressCallback; + main_file.WebViewPermissionResourceType; + main_file.WebResourceError; + main_file.WebResourceErrorCallback; + main_file.WebViewCookie; + main_file.WebViewCredential; + main_file.WebResourceErrorType; + main_file.UrlChange; + }, + ); + }); +} diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart deleted file mode 100644 index 8ceac19f686..00000000000 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -// 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 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter/webview_flutter.dart' as main_file; - -void main() { - group('webview_flutter', () { - test('ensure webview_flutter.dart exports classes from platform interface', - () { - // ignore: unnecessary_statements - main_file.JavaScriptConsoleMessage; - // ignore: unnecessary_statements - main_file.JavaScriptLogLevel; - // ignore: unnecessary_statements - main_file.JavaScriptMessage; - // ignore: unnecessary_statements - main_file.JavaScriptMode; - // ignore: unnecessary_statements - main_file.LoadRequestMethod; - // ignore: unnecessary_statements - main_file.NavigationDecision; - // ignore: unnecessary_statements - main_file.NavigationRequest; - // ignore: unnecessary_statements - main_file.NavigationRequestCallback; - // ignore: unnecessary_statements - main_file.PageEventCallback; - // ignore: unnecessary_statements - main_file.PlatformNavigationDelegateCreationParams; - // ignore: unnecessary_statements - main_file.PlatformWebViewControllerCreationParams; - // ignore: unnecessary_statements - main_file.PlatformWebViewCookieManagerCreationParams; - // ignore: unnecessary_statements - main_file.PlatformWebViewPermissionRequest; - // ignore: unnecessary_statements - main_file.PlatformWebViewWidgetCreationParams; - // ignore: unnecessary_statements - main_file.ProgressCallback; - // ignore: unnecessary_statements - main_file.WebViewPermissionResourceType; - // ignore: unnecessary_statements - main_file.WebResourceError; - // ignore: unnecessary_statements - main_file.WebResourceErrorCallback; - // ignore: unnecessary_statements - main_file.WebViewCookie; - // ignore: unnecessary_statements - main_file.WebResourceErrorType; - // ignore: unnecessary_statements - main_file.UrlChange; - }); - }); -}