Skip to content

Commit 4e1942e

Browse files
[google_identity_services_web] Set nonce properly in loadWebSdk(). (flutter#8069)
This PR adds logic to `google_identity_services_web/lib/src/js_loader.dart` to cause the `nonce` property to be property set when creating new script elements.
1 parent 4d0673c commit 4e1942e

File tree

5 files changed

+136
-8
lines changed

5 files changed

+136
-8
lines changed

packages/google_identity_services_web/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.3.2
2+
3+
* Adds the `nonce` parameter to `loadWebSdk`.
4+
15
## 0.3.1+5
26

37
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.

packages/google_identity_services_web/lib/src/js_interop/package_web_tweaks.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,15 @@ extension CreateScriptUrlNoArgs on web.TrustedTypePolicy {
3434
String input,
3535
);
3636
}
37+
38+
/// This extension gives web.HTMLScriptElement a nullable getter to the
39+
/// `nonce` property, which needs to be used to check for feature support.
40+
extension NullableNonceGetter on web.HTMLScriptElement {
41+
/// (Nullable) Bindings to HTMLScriptElement.nonce.
42+
///
43+
/// This may be null if the browser doesn't support the Nonce API.
44+
///
45+
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
46+
@JS('nonce')
47+
external String? get nullableNonce;
48+
}

packages/google_identity_services_web/lib/src/js_loader.dart

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,22 @@ const String _url = 'https://accounts.google.com/gsi/client';
1616
// The default TrustedPolicy name that will be used to inject the script.
1717
const String _defaultTrustedPolicyName = 'gis-dart';
1818

19+
// Sentinel value to tell apart when users explicitly set the nonce value to `null`.
20+
const String _undefined = '___undefined___';
21+
1922
/// Loads the GIS SDK for web, using Trusted Types API when available.
23+
///
24+
/// This attempts to use Trusted Types when available, and creates a new policy
25+
/// with the given [trustedTypePolicyName].
26+
///
27+
/// By default, the script will attempt to copy the `nonce` attribute from other
28+
/// scripts in the page. The [nonce] parameter will be used when passed, and
29+
/// not-null. When [nonce] parameter is explicitly `null`, no `nonce`
30+
/// attribute is applied to the script.
2031
Future<void> loadWebSdk({
2132
web.HTMLElement? target,
2233
String trustedTypePolicyName = _defaultTrustedPolicyName,
34+
String? nonce = _undefined,
2335
}) {
2436
final Completer<void> completer = Completer<void>();
2537
onGoogleLibraryLoad = () => completer.complete();
@@ -42,21 +54,51 @@ Future<void> loadWebSdk({
4254
}
4355
}
4456

45-
final web.HTMLScriptElement script =
46-
web.document.createElement('script') as web.HTMLScriptElement
47-
..async = true
48-
..defer = true;
57+
final web.HTMLScriptElement script = web.HTMLScriptElement()
58+
..async = true
59+
..defer = true;
4960
if (trustedUrl != null) {
5061
script.trustedSrc = trustedUrl;
5162
} else {
5263
script.src = _url;
5364
}
5465

66+
if (_getNonce(suppliedNonce: nonce) case final String nonce?) {
67+
script.nonce = nonce;
68+
}
69+
5570
(target ?? web.document.head!).appendChild(script);
5671

5772
return completer.future;
5873
}
5974

75+
/// Computes the actual nonce value to use.
76+
///
77+
/// If [suppliedNonce] has been explicitly passed, returns that.
78+
/// If `suppliedNonce` is null, it attempts to locate the `nonce`
79+
/// attribute from other script in the page.
80+
String? _getNonce({String? suppliedNonce, web.Window? window}) {
81+
if (suppliedNonce != _undefined) {
82+
return suppliedNonce;
83+
}
84+
85+
final web.Window currentWindow = window ?? web.window;
86+
final web.NodeList elements =
87+
currentWindow.document.querySelectorAll('script');
88+
89+
for (int i = 0; i < elements.length; i++) {
90+
if (elements.item(i) case final web.HTMLScriptElement element) {
91+
// Chrome may return an empty string instead of null.
92+
final String nonce =
93+
element.nullableNonce ?? element.getAttribute('nonce') ?? '';
94+
if (nonce.isNotEmpty) {
95+
return nonce;
96+
}
97+
}
98+
}
99+
return null;
100+
}
101+
60102
/// Exception thrown if the Trusted Types feature is supported, enabled, and it
61103
/// has prevented this loader from injecting the JS SDK.
62104
class TrustedTypesException implements Exception {

packages/google_identity_services_web/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: google_identity_services_web
22
description: A Dart JS-interop layer for Google Identity Services. Google's new sign-in SDK for Web that supports multiple types of credentials.
33
repository: https://github.com/flutter/packages/tree/main/packages/google_identity_services_web
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_identiy_services_web%22
5-
version: 0.3.1+5
5+
version: 0.3.2
66

77
environment:
88
sdk: ^3.4.0

packages/google_identity_services_web/test/js_loader_test.dart

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ import 'package:web/web.dart' as web;
2525

2626
void main() {
2727
group('loadWebSdk (no TrustedTypes)', () {
28-
final web.HTMLDivElement target =
29-
web.document.createElement('div') as web.HTMLDivElement;
28+
final web.HTMLDivElement target = web.HTMLDivElement();
29+
30+
tearDown(() {
31+
target.replaceChildren(<JSObject>[].toJS);
32+
});
3033

3134
test('Injects script into desired target', () async {
3235
// This test doesn't simulate the callback that completes the future, and
3336
// the code being tested runs synchronously.
3437
unawaited(loadWebSdk(target: target));
3538

3639
// Target now should have a child that is a script element
37-
final web.Node? injected = target.firstChild;
40+
final web.Node? injected = target.firstElementChild;
3841
expect(injected, isNotNull);
3942
expect(injected, isA<web.HTMLScriptElement>());
4043

@@ -54,6 +57,73 @@ void main() {
5457

5558
await expectLater(loadFuture, completes);
5659
});
60+
61+
group('`nonce` parameter', () {
62+
test('can be set', () async {
63+
const String expectedNonce = 'some-random-nonce';
64+
unawaited(loadWebSdk(target: target, nonce: expectedNonce));
65+
66+
// Target now should have a child that is a script element
67+
final web.HTMLScriptElement script =
68+
target.firstElementChild! as web.HTMLScriptElement;
69+
expect(script.nonce, expectedNonce);
70+
});
71+
72+
test('defaults to a nonce set in other script of the page', () async {
73+
const String expectedNonce = 'another-random-nonce';
74+
final web.HTMLScriptElement otherScript = web.HTMLScriptElement()
75+
..nonce = expectedNonce;
76+
web.document.head?.appendChild(otherScript);
77+
78+
// This test doesn't simulate the callback that completes the future, and
79+
// the code being tested runs synchronously.
80+
unawaited(loadWebSdk(target: target));
81+
82+
// Target now should have a child that is a script element
83+
final web.HTMLScriptElement script =
84+
target.firstElementChild! as web.HTMLScriptElement;
85+
expect(script.nonce, expectedNonce);
86+
87+
otherScript.remove();
88+
});
89+
90+
test('when explicitly set overrides the default', () async {
91+
const String expectedNonce = 'third-random-nonce';
92+
final web.HTMLScriptElement otherScript = web.HTMLScriptElement()
93+
..nonce = 'this-is-the-wrong-nonce';
94+
web.document.head?.appendChild(otherScript);
95+
96+
// This test doesn't simulate the callback that completes the future, and
97+
// the code being tested runs synchronously.
98+
unawaited(loadWebSdk(target: target, nonce: expectedNonce));
99+
100+
// Target now should have a child that is a script element
101+
final web.HTMLScriptElement script =
102+
target.firstElementChild! as web.HTMLScriptElement;
103+
expect(script.nonce, expectedNonce);
104+
105+
otherScript.remove();
106+
});
107+
108+
test('when null disables the feature', () async {
109+
final web.HTMLScriptElement otherScript = web.HTMLScriptElement()
110+
..nonce = 'this-is-the-wrong-nonce';
111+
web.document.head?.appendChild(otherScript);
112+
113+
// This test doesn't simulate the callback that completes the future, and
114+
// the code being tested runs synchronously.
115+
unawaited(loadWebSdk(target: target, nonce: null));
116+
117+
// Target now should have a child that is a script element
118+
final web.HTMLScriptElement script =
119+
target.firstElementChild! as web.HTMLScriptElement;
120+
121+
expect(script.nonce, isEmpty);
122+
expect(script.hasAttribute('nonce'), isFalse);
123+
124+
otherScript.remove();
125+
});
126+
});
57127
});
58128
}
59129

0 commit comments

Comments
 (0)