Skip to content

Commit

Permalink
[google_identity_services_web] Set nonce properly in loadWebSdk(). (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
stereotype441 authored Nov 14, 2024
1 parent 4d0673c commit 4e1942e
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 8 deletions.
4 changes: 4 additions & 0 deletions packages/google_identity_services_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.2

* Adds the `nonce` parameter to `loadWebSdk`.

## 0.3.1+5

* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,15 @@ extension CreateScriptUrlNoArgs on web.TrustedTypePolicy {
String input,
);
}

/// This extension gives web.HTMLScriptElement a nullable getter to the
/// `nonce` property, which needs to be used to check for feature support.
extension NullableNonceGetter on web.HTMLScriptElement {
/// (Nullable) Bindings to HTMLScriptElement.nonce.
///
/// This may be null if the browser doesn't support the Nonce API.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
@JS('nonce')
external String? get nullableNonce;
}
50 changes: 46 additions & 4 deletions packages/google_identity_services_web/lib/src/js_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,22 @@ const String _url = 'https://accounts.google.com/gsi/client';
// The default TrustedPolicy name that will be used to inject the script.
const String _defaultTrustedPolicyName = 'gis-dart';

// Sentinel value to tell apart when users explicitly set the nonce value to `null`.
const String _undefined = '___undefined___';

/// Loads the GIS SDK for web, using Trusted Types API when available.
///
/// This attempts to use Trusted Types when available, and creates a new policy
/// with the given [trustedTypePolicyName].
///
/// By default, the script will attempt to copy the `nonce` attribute from other
/// scripts in the page. The [nonce] parameter will be used when passed, and
/// not-null. When [nonce] parameter is explicitly `null`, no `nonce`
/// attribute is applied to the script.
Future<void> loadWebSdk({
web.HTMLElement? target,
String trustedTypePolicyName = _defaultTrustedPolicyName,
String? nonce = _undefined,
}) {
final Completer<void> completer = Completer<void>();
onGoogleLibraryLoad = () => completer.complete();
Expand All @@ -42,21 +54,51 @@ Future<void> loadWebSdk({
}
}

final web.HTMLScriptElement script =
web.document.createElement('script') as web.HTMLScriptElement
..async = true
..defer = true;
final web.HTMLScriptElement script = web.HTMLScriptElement()
..async = true
..defer = true;
if (trustedUrl != null) {
script.trustedSrc = trustedUrl;
} else {
script.src = _url;
}

if (_getNonce(suppliedNonce: nonce) case final String nonce?) {
script.nonce = nonce;
}

(target ?? web.document.head!).appendChild(script);

return completer.future;
}

/// Computes the actual nonce value to use.
///
/// If [suppliedNonce] has been explicitly passed, returns that.
/// If `suppliedNonce` is null, it attempts to locate the `nonce`
/// attribute from other script in the page.
String? _getNonce({String? suppliedNonce, web.Window? window}) {
if (suppliedNonce != _undefined) {
return suppliedNonce;
}

final web.Window currentWindow = window ?? web.window;
final web.NodeList elements =
currentWindow.document.querySelectorAll('script');

for (int i = 0; i < elements.length; i++) {
if (elements.item(i) case final web.HTMLScriptElement element) {
// Chrome may return an empty string instead of null.
final String nonce =
element.nullableNonce ?? element.getAttribute('nonce') ?? '';
if (nonce.isNotEmpty) {
return nonce;
}
}
}
return null;
}

/// Exception thrown if the Trusted Types feature is supported, enabled, and it
/// has prevented this loader from injecting the JS SDK.
class TrustedTypesException implements Exception {
Expand Down
2 changes: 1 addition & 1 deletion packages/google_identity_services_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: google_identity_services_web
description: A Dart JS-interop layer for Google Identity Services. Google's new sign-in SDK for Web that supports multiple types of credentials.
repository: https://github.com/flutter/packages/tree/main/packages/google_identity_services_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_identiy_services_web%22
version: 0.3.1+5
version: 0.3.2

environment:
sdk: ^3.4.0
Expand Down
76 changes: 73 additions & 3 deletions packages/google_identity_services_web/test/js_loader_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ import 'package:web/web.dart' as web;

void main() {
group('loadWebSdk (no TrustedTypes)', () {
final web.HTMLDivElement target =
web.document.createElement('div') as web.HTMLDivElement;
final web.HTMLDivElement target = web.HTMLDivElement();

tearDown(() {
target.replaceChildren(<JSObject>[].toJS);
});

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

// Target now should have a child that is a script element
final web.Node? injected = target.firstChild;
final web.Node? injected = target.firstElementChild;
expect(injected, isNotNull);
expect(injected, isA<web.HTMLScriptElement>());

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

await expectLater(loadFuture, completes);
});

group('`nonce` parameter', () {
test('can be set', () async {
const String expectedNonce = 'some-random-nonce';
unawaited(loadWebSdk(target: target, nonce: expectedNonce));

// Target now should have a child that is a script element
final web.HTMLScriptElement script =
target.firstElementChild! as web.HTMLScriptElement;
expect(script.nonce, expectedNonce);
});

test('defaults to a nonce set in other script of the page', () async {
const String expectedNonce = 'another-random-nonce';
final web.HTMLScriptElement otherScript = web.HTMLScriptElement()
..nonce = expectedNonce;
web.document.head?.appendChild(otherScript);

// This test doesn't simulate the callback that completes the future, and
// the code being tested runs synchronously.
unawaited(loadWebSdk(target: target));

// Target now should have a child that is a script element
final web.HTMLScriptElement script =
target.firstElementChild! as web.HTMLScriptElement;
expect(script.nonce, expectedNonce);

otherScript.remove();
});

test('when explicitly set overrides the default', () async {
const String expectedNonce = 'third-random-nonce';
final web.HTMLScriptElement otherScript = web.HTMLScriptElement()
..nonce = 'this-is-the-wrong-nonce';
web.document.head?.appendChild(otherScript);

// This test doesn't simulate the callback that completes the future, and
// the code being tested runs synchronously.
unawaited(loadWebSdk(target: target, nonce: expectedNonce));

// Target now should have a child that is a script element
final web.HTMLScriptElement script =
target.firstElementChild! as web.HTMLScriptElement;
expect(script.nonce, expectedNonce);

otherScript.remove();
});

test('when null disables the feature', () async {
final web.HTMLScriptElement otherScript = web.HTMLScriptElement()
..nonce = 'this-is-the-wrong-nonce';
web.document.head?.appendChild(otherScript);

// This test doesn't simulate the callback that completes the future, and
// the code being tested runs synchronously.
unawaited(loadWebSdk(target: target, nonce: null));

// Target now should have a child that is a script element
final web.HTMLScriptElement script =
target.firstElementChild! as web.HTMLScriptElement;

expect(script.nonce, isEmpty);
expect(script.hasAttribute('nonce'), isFalse);

otherScript.remove();
});
});
});
}

Expand Down

0 comments on commit 4e1942e

Please sign in to comment.