Skip to content

Commit bb1277e

Browse files
bparrishMinesFMorschel
authored andcommitted
[webview_flutter] Adds support to respond to recoverable SSL certificate errors (flutter#9150)
**tl;dr** This adds the `NavigationDelegate.onSslAuthError` method for recoverable SSL certificate errors. Fixes flutter/flutter#36925 **Important Note**: This is a more niche feature that should only be used for logging errors or used in a development environment. See docs for [WebViewClient.onReceviedSslError](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%20android.webkit.SslErrorHandler,%20android.net.http.SslError)). Therefore, I included multiple comments throughout that highly recommends calling `cancel()` and ensured that we call or set `cancel`/`performDefaultHandling` when the method is not set with unit tests. The respective platform callback methods are: Android: [WebViewClient.onReceviedSslError](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%20android.webkit.SslErrorHandler,%20android.net.http.SslError)) iOS/macOS: [WKNavigationDelegate(_:didReceive:completionHandler:)](https://developer.apple.com/documentation/webkit/wknavigationdelegate/webview(_:didreceive:completionhandler:)) The Android method is only called for recoverable SSL errors while the iOS/macOS method is called for all authentication related callbacks. So for iOS/macOS, the callback is only implemented when: 1. `URLAuthenticationChallenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust` 2. [SecTrustEvaluateWithError()](https://developer.apple.com/documentation/security/sectrustevaluatewitherror(_:_:)) returns false when passed `URLAuthenticationChallenge.protectionSpace.serverTrust`. 3. [SecTrustGetTrustResult()](https://developer.apple.com/documentation/security/sectrustgettrustresult(_:_:)) returns [SecTrustResultType.recoverableTrustFailure](https://developer.apple.com/documentation/security/sectrustresulttype/recoverabletrustfailure) when passed `URLAuthenticationChallenge.protectionSpace.serverTrust`. This implementation only really provides the error description, certificate data, and the ability to proceed or cancel. Providing access to verify or manipulate a certificate seem to be outside the scope of a WebView plugin since [Android](https://developer.android.com/reference/javax/security/cert/package-summary) and [iOS/macOS](https://developer.apple.com/documentation/security) have an extensive separate library for them. Providing the encoded data for the `X509Certificate` should be enough to use the certificate with another library. A few platform differences to note: 1. Android provides a `String getUrl()` for the error while iOS/macOS provides a [URLProtectionSpace](https://developer.apple.com/documentation/foundation/urlprotectionspace) that provides `host`, `port`, `protocol`, and `proxyType`. I didn't think I would be able to create a full Dart `Uri` for iOS/macOS from these reliably, so each platform provides these values separately. 2. A certificate can have multiple errors, but only Android provides access to them. However, both platforms say that they provide access to the "primary" error, so I just provided that. A platform specific value for Android could be added later. Side note: Android provides a [WebViewClient.onReceivedClientCertRequest](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedClientCertRequest(android.webkit.WebView,%20android.webkit.ClientCertRequest)) to read all certificate requests that don't get a recoverable error. A separate plugin method could be made to receive these. Side Side note: Old PR: flutter/plugins#2285 Fixes flutter/flutter#74609 Old quote about this feature from flutter/plugins#2285 (comment): > By default, Flutter plugins should expect or enforce a secure environment, therefore we can not process this PR as-is. However, I can see a use case, especially during local development when it's just may not be feasible to have a properly signed https connection. ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 2a3ee54 commit bb1277e

File tree

9 files changed

+166
-12
lines changed

9 files changed

+166
-12
lines changed

packages/webview_flutter/webview_flutter/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 4.13.0
2+
3+
* Adds support to respond to recoverable SSL certificate errors. See `NavigationDelegate(onSSlAuthError)`.
4+
15
## 4.12.0
26

37
* Adds support to set whether to draw the scrollbar. See

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: ^4.5.0
21-
webview_flutter_wkwebview: ^3.21.0
20+
webview_flutter_android: ^4.7.0
21+
webview_flutter_wkwebview: ^3.22.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.12.0
30+
webview_flutter_platform_interface: ^2.13.0
3131

3232
flutter:
3333
uses-material-design: true

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

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ class NavigationDelegate {
3838
/// Constructs a [NavigationDelegate].
3939
///
4040
/// {@template webview_fluttter.NavigationDelegate.constructor}
41-
/// `onUrlChange`: invoked when the underlying web view changes to a new url.
42-
/// `onHttpAuthRequest`: invoked when the web view is requesting authentication.
41+
/// **`onUrlChange`:** invoked when the underlying web view changes to a new url.
42+
/// **`onHttpAuthRequest`:** invoked when the web view is requesting authentication.
43+
/// **`onSslAuthError`:** Invoked when the web view receives a recoverable SSL
44+
/// error for a certificate. The host application must call either
45+
/// [SslAuthError.cancel] or [SslAuthError.proceed].
4346
/// {@endtemplate}
4447
NavigationDelegate({
4548
FutureOr<NavigationDecision> Function(NavigationRequest request)?
@@ -51,6 +54,7 @@ class NavigationDelegate {
5154
void Function(UrlChange change)? onUrlChange,
5255
void Function(HttpAuthRequest request)? onHttpAuthRequest,
5356
void Function(HttpResponseError error)? onHttpError,
57+
void Function(SslAuthError request)? onSslAuthError,
5458
}) : this.fromPlatformCreationParams(
5559
const PlatformNavigationDelegateCreationParams(),
5660
onNavigationRequest: onNavigationRequest,
@@ -61,6 +65,7 @@ class NavigationDelegate {
6165
onUrlChange: onUrlChange,
6266
onHttpAuthRequest: onHttpAuthRequest,
6367
onHttpError: onHttpError,
68+
onSslAuthError: onSslAuthError,
6469
);
6570

6671
/// Constructs a [NavigationDelegate] from creation params for a specific
@@ -105,6 +110,7 @@ class NavigationDelegate {
105110
void Function(UrlChange change)? onUrlChange,
106111
void Function(HttpAuthRequest request)? onHttpAuthRequest,
107112
void Function(HttpResponseError error)? onHttpError,
113+
void Function(SslAuthError request)? onSslAuthError,
108114
}) : this.fromPlatform(
109115
PlatformNavigationDelegate(params),
110116
onNavigationRequest: onNavigationRequest,
@@ -115,6 +121,7 @@ class NavigationDelegate {
115121
onUrlChange: onUrlChange,
116122
onHttpAuthRequest: onHttpAuthRequest,
117123
onHttpError: onHttpError,
124+
onSslAuthError: onSslAuthError,
118125
);
119126

120127
/// Constructs a [NavigationDelegate] from a specific platform implementation.
@@ -130,6 +137,7 @@ class NavigationDelegate {
130137
void Function(UrlChange change)? onUrlChange,
131138
HttpAuthRequestCallback? onHttpAuthRequest,
132139
void Function(HttpResponseError error)? onHttpError,
140+
void Function(SslAuthError request)? onSslAuthError,
133141
}) {
134142
if (onNavigationRequest != null) {
135143
platform.setOnNavigationRequest(onNavigationRequest!);
@@ -155,6 +163,13 @@ class NavigationDelegate {
155163
if (onHttpError != null) {
156164
platform.setOnHttpError(onHttpError);
157165
}
166+
if (onSslAuthError != null) {
167+
platform.setOnSSlAuthError(
168+
(PlatformSslAuthError error) {
169+
onSslAuthError(SslAuthError._fromPlatform(error));
170+
},
171+
);
172+
}
158173
}
159174

160175
/// Implementation of [PlatformNavigationDelegate] for the current platform.
@@ -184,3 +199,54 @@ class NavigationDelegate {
184199
/// Invoked when a resource loading error occurred.
185200
final WebResourceErrorCallback? onWebResourceError;
186201
}
202+
203+
/// Represents an SSL error with the associated certificate.
204+
///
205+
/// The host application must call [cancel] or, contrary to secure web
206+
/// communication standards, [proceed] to provide the web view's response to the
207+
/// error. [proceed] should generally only be used in test environments, as
208+
/// using it in production can expose users to security and privacy risks.
209+
///
210+
/// ## Platform-Specific Features
211+
/// This class contains an underlying implementation provided by the current
212+
/// platform. Once a platform implementation is imported, the examples below
213+
/// can be followed to use features provided by a platform's implementation.
214+
///
215+
/// Below is an example of accessing the platform-specific implementation for
216+
/// iOS and Android:
217+
///
218+
/// ```dart
219+
/// final SslAuthError error = ...;
220+
///
221+
/// if (WebViewPlatform.instance is WebKitWebViewPlatform) {
222+
/// final WebKitSslAuthError webKitError =
223+
/// error.platform as WebKitSslAuthError;
224+
/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) {
225+
/// final AndroidSslAuthError androidError =
226+
/// error.platform as AndroidSslAuthError;
227+
/// }
228+
/// ```
229+
class SslAuthError {
230+
SslAuthError._fromPlatform(this.platform);
231+
232+
/// An implementation of [PlatformSslAuthError] for the current platform.
233+
final PlatformSslAuthError platform;
234+
235+
/// The certificate associated with this error.
236+
X509Certificate? get certificate => platform.certificate;
237+
238+
/// Instructs the WebView that encountered the SSL certificate error to
239+
/// terminate communication with the server.
240+
///
241+
/// The host application must call this method to prevent a resource from
242+
/// loading when an SSL certificate is invalid.
243+
Future<void> cancel() => platform.cancel();
244+
245+
/// Instructs the WebView that encountered the SSL certificate error to ignore
246+
/// the error and continue communicating with the server.
247+
///
248+
/// **Warning:** Calling [proceed] in a production environment is strongly
249+
/// discouraged, as an invalid SSL certificate means that the connection is
250+
/// not secure, so proceeding can expose users to security and privacy risks.
251+
Future<void> proceed() => platform.proceed();
252+
}

packages/webview_flutter/webview_flutter/lib/webview_flutter.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte
3636
WebViewCredential,
3737
WebViewOverScrollMode,
3838
WebViewPermissionResourceType,
39-
WebViewPlatform;
39+
WebViewPlatform,
40+
X509Certificate;
4041

4142
export 'src/navigation_delegate.dart';
4243
export 'src/webview_controller.dart';

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 backed by the system webview.
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.12.0
5+
version: 4.13.0
66

77
environment:
88
sdk: ^3.6.0
@@ -21,9 +21,9 @@ flutter:
2121
dependencies:
2222
flutter:
2323
sdk: flutter
24-
webview_flutter_android: ^4.5.0
25-
webview_flutter_platform_interface: ^2.12.0
26-
webview_flutter_wkwebview: ^3.21.0
24+
webview_flutter_android: ^4.7.0
25+
webview_flutter_platform_interface: ^2.13.0
26+
webview_flutter_wkwebview: ^3.22.0
2727

2828
dev_dependencies:
2929
build_runner: ^2.1.5

packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte
1111

1212
import 'navigation_delegate_test.mocks.dart';
1313

14-
@GenerateMocks(<Type>[WebViewPlatform, PlatformNavigationDelegate])
14+
@GenerateMocks(<Type>[
15+
WebViewPlatform,
16+
PlatformNavigationDelegate,
17+
PlatformSslAuthError,
18+
])
1519
void main() {
1620
group('NavigationDelegate', () {
1721
test('onNavigationRequest', () async {
@@ -111,6 +115,28 @@ void main() {
111115

112116
verify(delegate.platform.setOnHttpError(onHttpError));
113117
});
118+
119+
test('onSslAuthError', () async {
120+
WebViewPlatform.instance = TestWebViewPlatform();
121+
122+
final NavigationDelegate delegate = NavigationDelegate(
123+
onSslAuthError: expectAsync1((SslAuthError error) {
124+
error.proceed();
125+
}),
126+
);
127+
128+
final void Function(PlatformSslAuthError) callback = verify(
129+
(delegate.platform as MockPlatformNavigationDelegate)
130+
.setOnSSlAuthError(captureAny))
131+
.captured
132+
.single as void Function(PlatformSslAuthError);
133+
134+
final MockPlatformSslAuthError mockPlatformError =
135+
MockPlatformSslAuthError();
136+
callback(mockPlatformError);
137+
138+
verify(mockPlatformError.proceed());
139+
});
114140
});
115141
}
116142

packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
import 'dart:async' as _i8;
77

88
import 'package:mockito/mockito.dart' as _i1;
9+
import 'package:mockito/src/dummies.dart' as _i10;
910
import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart'
1011
as _i3;
12+
import 'package:webview_flutter_platform_interface/src/platform_ssl_auth_error.dart'
13+
as _i9;
1114
import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart'
1215
as _i4;
1316
import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart'
@@ -211,4 +214,47 @@ class MockPlatformNavigationDelegate extends _i1.Mock
211214
returnValue: _i8.Future<void>.value(),
212215
returnValueForMissingStub: _i8.Future<void>.value(),
213216
) as _i8.Future<void>);
217+
218+
@override
219+
_i8.Future<void> setOnSSlAuthError(
220+
_i3.SslAuthErrorCallback? onSslAuthError,
221+
) =>
222+
(super.noSuchMethod(
223+
Invocation.method(#setOnSSlAuthError, [onSslAuthError]),
224+
returnValue: _i8.Future<void>.value(),
225+
returnValueForMissingStub: _i8.Future<void>.value(),
226+
) as _i8.Future<void>);
227+
}
228+
229+
/// A class which mocks [PlatformSslAuthError].
230+
///
231+
/// See the documentation for Mockito's code generation for more information.
232+
class MockPlatformSslAuthError extends _i1.Mock
233+
implements _i9.PlatformSslAuthError {
234+
MockPlatformSslAuthError() {
235+
_i1.throwOnMissingStub(this);
236+
}
237+
238+
@override
239+
String get description => (super.noSuchMethod(
240+
Invocation.getter(#description),
241+
returnValue: _i10.dummyValue<String>(
242+
this,
243+
Invocation.getter(#description),
244+
),
245+
) as String);
246+
247+
@override
248+
_i8.Future<void> proceed() => (super.noSuchMethod(
249+
Invocation.method(#proceed, []),
250+
returnValue: _i8.Future<void>.value(),
251+
returnValueForMissingStub: _i8.Future<void>.value(),
252+
) as _i8.Future<void>);
253+
254+
@override
255+
_i8.Future<void> cancel() => (super.noSuchMethod(
256+
Invocation.method(#cancel, []),
257+
returnValue: _i8.Future<void>.value(),
258+
returnValueForMissingStub: _i8.Future<void>.value(),
259+
) as _i8.Future<void>);
214260
}

packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,4 +458,14 @@ class MockPlatformNavigationDelegate extends _i1.Mock
458458
returnValue: _i5.Future<void>.value(),
459459
returnValueForMissingStub: _i5.Future<void>.value(),
460460
) as _i5.Future<void>);
461+
462+
@override
463+
_i5.Future<void> setOnSSlAuthError(
464+
_i6.SslAuthErrorCallback? onSslAuthError,
465+
) =>
466+
(super.noSuchMethod(
467+
Invocation.method(#setOnSSlAuthError, [onSslAuthError]),
468+
returnValue: _i5.Future<void>.value(),
469+
returnValueForMissingStub: _i5.Future<void>.value(),
470+
) as _i5.Future<void>);
461471
}

packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ void main() {
3030
main_file.PlatformWebViewPermissionRequest;
3131
main_file.PlatformWebViewWidgetCreationParams;
3232
main_file.ProgressCallback;
33+
main_file.UrlChange;
3334
main_file.WebViewOverScrollMode;
3435
main_file.WebViewPermissionResourceType;
3536
main_file.WebResourceError;
@@ -39,7 +40,7 @@ void main() {
3940
main_file.WebResourceErrorType;
4041
main_file.WebResourceRequest;
4142
main_file.WebResourceResponse;
42-
main_file.UrlChange;
43+
main_file.X509Certificate;
4344
},
4445
);
4546
});

0 commit comments

Comments
 (0)