Skip to content

Commit

Permalink
[webview_flutter] Support for handling basic authentication requests (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
JeroenWeener committed Feb 1, 2024
1 parent 60a1ffc commit 013763d
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 65 deletions.
3 changes: 2 additions & 1 deletion packages/webview_flutter/webview_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ 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 List<String>? 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}');
}
Expand All @@ -41,6 +60,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('loadRequest', (WidgetTester tester) async {
final Completer<void> pageFinished = Completer<void>();
Expand All @@ -52,7 +72,6 @@ Future<void> main() async {
unawaited(controller.loadRequest(Uri.parse(primaryUrl)));

await tester.pumpWidget(WebViewWidget(controller: controller));

await pageFinished.future;

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

await expectLater(urlChangeCompleter.future, completion(secondaryUrl));
});

testWidgets('can receive HTTP basic auth requests',
(WidgetTester tester) async {
final Completer<void> authRequested = Completer<void>();
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<void> pageFinished = Completer<void>();

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',
Expand Down
98 changes: 98 additions & 0 deletions packages/webview_flutter/webview_flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ Page resource error:
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
onHttpAuthRequest: (HttpAuthRequest request) {
openDialog(request);
},
),
)
..addJavaScriptChannel(
Expand Down Expand Up @@ -226,6 +229,62 @@ Page resource error:
child: const Icon(Icons.favorite),
);
}

Future<void> 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: <Widget>[
TextField(
decoration: const InputDecoration(labelText: 'Username'),
autofocus: true,
controller: usernameTextController,
),
TextField(
decoration: const InputDecoration(labelText: 'Password'),
controller: passwordTextController,
),
],
),
),
actions: <Widget>[
// 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 {
Expand All @@ -243,6 +302,7 @@ enum MenuOptions {
transparentBackground,
setCookie,
logExample,
basicAuthentication,
}

class SampleMenu extends StatelessWidget {
Expand Down Expand Up @@ -288,6 +348,8 @@ class SampleMenu extends StatelessWidget {
_onSetCookie();
case MenuOptions.logExample:
_onLogExample();
case MenuOptions.basicAuthentication:
_promptForUrl(context);
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
Expand Down Expand Up @@ -348,6 +410,10 @@ class SampleMenu extends StatelessWidget {
value: MenuOptions.logExample,
child: Text('Log example'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.basicAuthentication,
child: Text('Basic Authentication Example'),
),
],
);
}
Expand Down Expand Up @@ -501,6 +567,38 @@ class SampleMenu extends StatelessWidget {

return webViewController.loadHtmlString(kLogExamplePage);
}

Future<void> _promptForUrl(BuildContext context) {
final TextEditingController urlTextController = TextEditingController();

return showDialog<String>(
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: <Widget>[
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 {
Expand Down
6 changes: 3 additions & 3 deletions packages/webview_flutter/webview_flutter/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,9 @@ class FakeNavigationDelegate extends PlatformNavigationDelegate {

@override
Future<void> setOnUrlChange(UrlChangeCallback onUrlChange) async {}

@override
Future<void> setOnHttpAuthRequest(
HttpAuthRequestCallback handler,
) async {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NavigationDecision> Function(NavigationRequest request)?
Expand All @@ -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,
Expand All @@ -56,6 +58,7 @@ class NavigationDelegate {
onProgress: onProgress,
onWebResourceError: onWebResourceError,
onUrlChange: onUrlChange,
onHttpAuthRequest: onHttpAuthRequest,
);

/// Constructs a [NavigationDelegate] from creation params for a specific
Expand Down Expand Up @@ -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,
Expand All @@ -106,6 +110,7 @@ class NavigationDelegate {
onProgress: onProgress,
onWebResourceError: onWebResourceError,
onUrlChange: onUrlChange,
onHttpAuthRequest: onHttpAuthRequest,
);

/// Constructs a [NavigationDelegate] from a specific platform implementation.
Expand All @@ -119,6 +124,7 @@ class NavigationDelegate {
this.onProgress,
this.onWebResourceError,
void Function(UrlChange change)? onUrlChange,
HttpAuthRequestCallback? onHttpAuthRequest,
}) {
if (onNavigationRequest != null) {
platform.setOnNavigationRequest(onNavigationRequest!);
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'
show
HttpAuthRequest,
JavaScriptConsoleMessage,
JavaScriptLogLevel,
JavaScriptMessage,
Expand All @@ -24,6 +25,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte
WebResourceErrorCallback,
WebResourceErrorType,
WebViewCookie,
WebViewCredential,
WebViewPermissionResourceType,
WebViewPlatform;

Expand Down
8 changes: 4 additions & 4 deletions packages/webview_flutter/webview_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});
}

Expand Down
Loading

0 comments on commit 013763d

Please sign in to comment.