Skip to content

[google_identity_services_web] Set nonce properly in loadWebSdk(). #8069

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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