From ab6e26822f6b55759fc5c3c3ba96d024ccd606b8 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 14 Mar 2023 21:00:49 -0700 Subject: [PATCH 1/5] [google_sign_in] Updates plugin to separate AuthN and AuthZ. * Exposes new 'canAccessScopes' method. * Updates example to handle isSignedIn and isAuthorized separately. * On the web, uses the new Sign In Button API. * (Implemented through conditional imports) * Add comments to the app so it's easier to follow. * Document changes in the README. --- .../google_sign_in/CHANGELOG.md | 6 +- .../google_sign_in/google_sign_in/README.md | 78 ++++++++++++++- .../google_sign_in/example/lib/main.dart | 98 +++++++++++++++---- .../example/lib/src/sign_in_button.dart | 7 ++ .../lib/src/sign_in_button/mobile.dart | 15 +++ .../example/lib/src/sign_in_button/stub.dart | 15 +++ .../example/lib/src/sign_in_button/web.dart | 15 +++ .../google_sign_in/example/pubspec.yaml | 13 +++ .../google_sign_in/example/web/index.html | 2 +- .../google_sign_in/lib/google_sign_in.dart | 69 ++++++++++--- .../google_sign_in/pubspec.yaml | 10 +- .../google_sign_in/regen_mocks.sh | 10 ++ .../test/google_sign_in_test.dart | 65 +++++++++++- .../test/google_sign_in_test.mocks.dart | 13 +++ 14 files changed, 376 insertions(+), 40 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart create mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart create mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart create mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart create mode 100755 packages/google_sign_in/google_sign_in/regen_mocks.sh diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index d37374ac3589..e7c287c7ed84 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,5 +1,9 @@ -## NEXT +## 6.1.0 +* Exposes the new method `canAccessScopes`. +* Updates example app to separate Authentication from Authorization for those + platforms where scopes are not automatically granted upon signIn (like the web). + * Updates README with information about these changes. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index ac5baeae96c0..316da3fb68bb 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -96,7 +96,15 @@ be an option. ### Web integration -For web integration details, see the +The new SDK used by the web has fully separated Authentication from Authorization, +so `signIn` and `signInSilently` no longer authorize Oauth `scopes`. + +Flutter Apps must be able to detect what scopes have been granted by their users, +and if the grants are still valid. + +Read below about **Working with scopes, and incremental authorization** for +general information about changes that may be needed on an app, and for more +specific web integration details, see the [`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). ## Usage @@ -139,6 +147,74 @@ Future _handleSignIn() async { } ``` +In the web, you should use the **Google Sign In button** (and not the `signIn` method) +to guarantee that your user authentication contains a valid `idToken`. + +For more details, take a look at the +[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). + +## Working with scopes, and incremental authorization. + +### Checking if scopes have been granted + +Users may (or may *not*) grant all the scopes that your application requests at +Sign In. In fact, in the web, no scopes are granted by signIn or silentSignIn anymore. + +Your app must be able to: + +* Detect if the authenticated user has authorized the scopes your app needs. +* Detect if the scopes that were granted a few minutes ago are still valid. + +There's a new method that allows your app to check this: + +```dart +final bool isAuthorized = await _googleSignIn.canAccessScopes(scopes); +``` + +### Requesting more scopes when needed + +If your app determines that the user hasn't granted the scopes it requires, it +should initiate an Authorization request **from an user interaction** (like a +button press). + +```dart +Future _handleAuthorizeScopes() async { + final bool isAuthorized = await _googleSignIn.requestScopes(scopes); + if (isAuthorized) { + // Do things that only authorized users can do! + _handleGetContact(_currentUser!); + } +} +``` + +The `requestScopes` returns a `boolean` value that is `true` if the user has +granted all the requested scopes or `false` otherwise. + +Once your app determines that the current user `isAuthorized` to access the +services for which you need `scopes`, it can proceed normally. + +### Authorization expiration + +In the web, **the `accessToken` is no longer refreshed**. It expires after 3600 +seconds (one hour), so your app needs to be able to handle failed REST requests, +and update its UI to prompt the user for a new Authorization round. + +This can be done by combining the error responses from your REST requests with +the `canAccessScopes` and `requestScopes` methods described above. + +For more details, take a look at the +[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). + +### My app didn't need any of this, what gives!? + +The new web SDK implicitly grant access to `email`, `profile` and `openid` when +users complete the sign-in process (either via the One Tap UX or the Google Sign +In button). + +If your app only needs an `idToken`, or only requests permissions to some of the +[OpenID Connect scopes](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect), +you might not need to implement any of the scope handling above. + ## Example Find the example wiring in the diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 523ead71262a..b1400de07f2e 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: public_member_api_docs, avoid_print +// ignore_for_file: avoid_print import 'dart:async'; import 'dart:convert' show json; @@ -11,13 +11,18 @@ import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:http/http.dart' as http; +import 'src/sign_in_button.dart'; + +/// The scopes required by this application. +const List scopes = [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', +]; + GoogleSignIn _googleSignIn = GoogleSignIn( // Optional clientId - // clientId: '479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com', - scopes: [ - 'email', - 'https://www.googleapis.com/auth/contacts.readonly', - ], + // clientId: 'your-client_id.apps.googleusercontent.com', + scopes: scopes, ); void main() { @@ -29,31 +34,53 @@ void main() { ); } +/// The SignInDemo app. class SignInDemo extends StatefulWidget { + /// const SignInDemo({super.key}); @override - State createState() => SignInDemoState(); + State createState() => _SignInDemoState(); } -class SignInDemoState extends State { +class _SignInDemoState extends State { GoogleSignInAccount? _currentUser; + bool _isAuthorized = false; // has granted permissions? String _contactText = ''; @override void initState() { super.initState(); - _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) { + + _googleSignIn.onCurrentUserChanged + .listen((GoogleSignInAccount? account) async { + // Check if the account can access scopes... + bool isAuthorized = false; + if (account != null) { + isAuthorized = await _googleSignIn.canAccessScopes(scopes); + } + setState(() { _currentUser = account; + _isAuthorized = isAuthorized; }); - if (_currentUser != null) { - _handleGetContact(_currentUser!); + + // Now that we know that the user can access the required scopes, the app + // can call the REST API. + if (isAuthorized) { + _handleGetContact(account!); } }); + + // In the web, _googleSignIn.signInSilently() triggers the One Tap UX. + // + // It is recommended by Google Identity Services to render both the One Tap UX + // and the Google Sign In button together to "reduce friction and improve + // sign-in rates" ([docs](https://developers.google.com/identity/gsi/web/guides/display-button#html)). _googleSignIn.signInSilently(); } + // Calls the People API REST endpoint for the signed-in user to retrieve information. Future _handleGetContact(GoogleSignInAccount user) async { setState(() { _contactText = 'Loading contact info...'; @@ -103,6 +130,10 @@ class SignInDemoState extends State { return null; } + // This is the on-click handler for the Sign In button that is rendered by Flutter. + // + // On the web, the on-click handler of the Sign In button is owned by the JS + // SDK, so this method can be considered mobile only. Future _handleSignIn() async { try { await _googleSignIn.signIn(); @@ -111,11 +142,28 @@ class SignInDemoState extends State { } } + // Prompts the user to authorize `scopes`. + // + // This action is **required** in platforms that don't perform Authentication + // and Authorization at the same time (like the web). + // + // On the web, this must be called from an user interaction (button click). + Future _handleAuthorizeScopes() async { + final bool isAuthorized = await _googleSignIn.requestScopes(scopes); + setState(() { + _isAuthorized = isAuthorized; + }); + if (isAuthorized) { + _handleGetContact(_currentUser!); + } + } + Future _handleSignOut() => _googleSignIn.disconnect(); Widget _buildBody() { final GoogleSignInAccount? user = _currentUser; if (user != null) { + // The user is Authenticated return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ @@ -127,25 +175,39 @@ class SignInDemoState extends State { subtitle: Text(user.email), ), const Text('Signed in successfully.'), - Text(_contactText), + if (_isAuthorized) ...[ + // The user has Authorized all required scopes + Text(_contactText), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + ], + if (!_isAuthorized) ...[ + // The user has NOT Authorized all required scopes. + // (Mobile users may never see this button!) + const Text('Additional permissions needed to read your contacts.'), + ElevatedButton( + onPressed: _handleAuthorizeScopes, + child: const Text('REQUEST PERMISSIONS'), + ), + ], ElevatedButton( onPressed: _handleSignOut, child: const Text('SIGN OUT'), ), - ElevatedButton( - child: const Text('REFRESH'), - onPressed: () => _handleGetContact(user), - ), ], ); } else { + // The user is NOT Authenticated return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ const Text('You are not currently signed in.'), - ElevatedButton( + // This method is used to separate mobile from web code with conditional exports. + // See: src/sign_in_button.dart + buildSignInButton( onPressed: _handleSignIn, - child: const Text('SIGN IN'), ), ], ); diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart new file mode 100644 index 000000000000..c0a339663126 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'sign_in_button/stub.dart' + if (dart.library.js_util) 'sign_in_button/web.dart' + if (dart.library.io) 'sign_in_button/mobile.dart'; diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart new file mode 100644 index 000000000000..8d929d7ef835 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'stub.dart'; + +/// Renders a SIGN IN button that calls `handleSignIn` onclick. +Widget buildSignInButton({HandleSignInFn? onPressed}) { + return ElevatedButton( + onPressed: onPressed, + child: const Text('SIGN IN'), + ); +} diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart new file mode 100644 index 000000000000..85a54f0ac27e --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// The type of the onClick callback for the (mobile) Sign In Button. +typedef HandleSignInFn = Future Function(); + +/// Renders a SIGN IN button that (maybe) calls the `handleSignIn` onclick. +Widget buildSignInButton({HandleSignInFn? onPressed}) { + return Container(); +} diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart new file mode 100644 index 000000000000..4189fc6cd724 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/google_sign_in_web.dart' as web; + +import 'stub.dart'; + +/// Renders a web-only SIGN IN button. +Widget buildSignInButton({HandleSignInFn? onPressed}) { + return (GoogleSignInPlatform.instance as web.GoogleSignInPlugin) + .renderButton(); +} diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index f46a4df7eb81..cc90258cae02 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -16,8 +16,21 @@ 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: ../ + google_sign_in_platform_interface: ^2.2.0 + google_sign_in_web: ^0.11.0 http: ^0.13.0 +dependency_overrides: + google_identity_services_web: + git: + url: https://github.com/ditman/flutter-packages.git + ref: gis-web-fix-render-button-api + path: packages/google_identity_services_web + google_sign_in_platform_interface: + path: ../../google_sign_in_platform_interface + google_sign_in_web: + path: ../../google_sign_in_web + dev_dependencies: espresso: ^0.2.0 flutter_driver: diff --git a/packages/google_sign_in/google_sign_in/example/web/index.html b/packages/google_sign_in/google_sign_in/example/web/index.html index 5710c936c2ed..6bd23335f2c1 100644 --- a/packages/google_sign_in/google_sign_in/example/web/index.html +++ b/packages/google_sign_in/google_sign_in/example/web/index.html @@ -5,7 +5,7 @@ - + Google Sign-in Example diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 8e908dc479ed..ff94949b6cf4 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -189,7 +189,13 @@ class GoogleSignIn { this.clientId, this.serverClientId, this.forceCodeForRefreshToken = false, - }); + }) { + // Start initializing. + // Async methods in the plugin will await for this to be done. + if (kIsWeb) { + unawaited(_ensureInitialized()); + } + } /// Factory for creating default sign in user experience. factory GoogleSignIn.standard({ @@ -261,11 +267,9 @@ class GoogleSignIn { StreamController.broadcast(); /// Subscribe to this stream to be notified when the current user changes. - Stream get onCurrentUserChanged => - _currentUserController.stream; - - // Future that completes when we've finished calling `init` on the native side - Future? _initialization; + Stream get onCurrentUserChanged { + return _currentUserController.stream; + } Future _callMethod( Future Function() method) async { @@ -278,6 +282,7 @@ class GoogleSignIn { : null); } + // Sets the current user, and propagates it through the _currentUserController. GoogleSignInAccount? _setCurrentUser(GoogleSignInAccount? currentUser) { if (currentUser != _currentUser) { _currentUser = currentUser; @@ -286,20 +291,36 @@ class GoogleSignIn { return _currentUser; } - Future _ensureInitialized() { - return _initialization ??= - GoogleSignInPlatform.instance.initWithParams(SignInInitParameters( + // Future that completes when we've finished calling `init` on the native side. + Future? _initialization; + + // Performs initialization, guarding it with the _initialization future. + Future _ensureInitialized() async { + return _initialization ??= _doInitialization() + ..catchError((dynamic _) { + // Invalidate initialization if it errors out. + _initialization = null; + }); + } + + // Actually performs the initialization. + // + // This method calls initWithParams, and then, if the plugin instance has a + // userDataEvents Stream, connects it to the [_setCurrentUser] method. + Future _doInitialization() async { + await GoogleSignInPlatform.instance.initWithParams(SignInInitParameters( signInOption: signInOption, scopes: scopes, hostedDomain: hostedDomain, clientId: clientId, serverClientId: serverClientId, forceCodeForRefreshToken: forceCodeForRefreshToken, - )) - ..catchError((dynamic _) { - // Invalidate initialization if it errors out. - _initialization = null; - }); + )); + + GoogleSignInPlatform.instance.userDataEvents + ?.map((GoogleSignInUserData? userData) { + return userData != null ? GoogleSignInAccount._(this, userData) : null; + }).forEach(_setCurrentUser); } /// The most recently scheduled method call. @@ -424,4 +445,24 @@ class GoogleSignIn { await _ensureInitialized(); return GoogleSignInPlatform.instance.requestScopes(scopes); } + + /// Checks if the [_currentUser] can access all the given [scopes]. + /// + /// Optionally, an [accessToken] can be passed to perform this check. This + /// may be useful when an application holds on to a cached, potentially + /// long-lived [accessToken]. + Future canAccessScopes( + List scopes, { + String? accessToken, + }) async { + await _ensureInitialized(); + + final String? token = + accessToken ?? (await _currentUser?.authentication)?.accessToken; + + return GoogleSignInPlatform.instance.canAccessScopes( + scopes, + accessToken: token, + ); + } } diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 3fcabdf0ffdf..930f0e3b27ea 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.0.2 +version: 6.1.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -27,6 +27,12 @@ dependencies: google_sign_in_platform_interface: ^2.2.0 google_sign_in_web: ^0.11.0 +dependency_overrides: + google_sign_in_platform_interface: + path: ../google_sign_in_platform_interface + google_sign_in_web: + path: ../google_sign_in_web + dev_dependencies: build_runner: ^2.1.10 flutter_driver: @@ -43,5 +49,3 @@ false_secrets: - /example/android/app/google-services.json - /example/ios/Runner/GoogleService-Info.plist - /example/ios/RunnerTests/GoogleSignInTests.m - - /example/lib/main.dart - - /example/web/index.html diff --git a/packages/google_sign_in/google_sign_in/regen_mocks.sh b/packages/google_sign_in/google_sign_in/regen_mocks.sh new file mode 100755 index 000000000000..78bcdc0f9e28 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/regen_mocks.sh @@ -0,0 +1,10 @@ +#!/usr/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +flutter pub get + +echo "(Re)generating mocks." + +flutter pub run build_runner build --delete-conflicting-outputs diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 2296f2d79887..86b83cf4a09b 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -31,6 +31,7 @@ void main() { setUp(() { mockPlatform = MockGoogleSignInPlatform(); when(mockPlatform.isMock).thenReturn(true); + when(mockPlatform.userDataEvents).thenReturn(null); when(mockPlatform.signInSilently()) .thenAnswer((Invocation _) async => kDefaultUser); when(mockPlatform.signIn()) @@ -260,10 +261,13 @@ void main() { }); test('can sign in after init failed before', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - + // Web eagerly `initWithParams` when GoogleSignIn is created, so make sure + // the initWithParams is throwy ASAP. when(mockPlatform.initWithParams(any)) .thenThrow(Exception('First init fails')); + + final GoogleSignIn googleSignIn = GoogleSignIn(); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); when(mockPlatform.initWithParams(any)) @@ -327,6 +331,63 @@ void main() { verify(mockPlatform.requestScopes(['testScope'])); }); + test('canAccessScopes forwards calls to platform', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.canAccessScopes( + any, + accessToken: anyNamed('accessToken'), + )).thenAnswer((Invocation _) async => true); + + await googleSignIn.signIn(); + final bool result = await googleSignIn.canAccessScopes( + ['testScope'], + accessToken: 'xyz', + ); + + expect(result, isTrue); + _verifyInit(mockPlatform); + verify(mockPlatform.canAccessScopes( + ['testScope'], + accessToken: 'xyz', + )); + }); + + test('userDataEvents are forwarded through the onUserChanged stream', + () async { + final StreamController userDataController = + StreamController(); + + when(mockPlatform.userDataEvents) + .thenAnswer((Invocation _) => userDataController.stream); + when(mockPlatform.isSignedIn()).thenAnswer((Invocation _) async => false); + + final GoogleSignIn googleSignIn = GoogleSignIn(); + await googleSignIn.isSignedIn(); + + // This is needed to ensure `_ensureInitialized` is called! + final Future> nextTwoEvents = + googleSignIn.onCurrentUserChanged.take(2).toList(); + + // Dispatch two events + userDataController.add(kDefaultUser); + userDataController.add(null); + + final List events = await nextTwoEvents; + + expect(events.first, isNotNull); + + final GoogleSignInAccount user = events.first!; + + expect(user.displayName, equals(kDefaultUser.displayName)); + expect(user.email, equals(kDefaultUser.email)); + expect(user.id, equals(kDefaultUser.id)); + expect(user.photoUrl, equals(kDefaultUser.photoUrl)); + expect(user.serverAuthCode, equals(kDefaultUser.serverAuthCode)); + + // The second event was a null... + expect(events.last, isNull); + }); + test('user starts as null', () async { final GoogleSignIn googleSignIn = GoogleSignIn(); expect(googleSignIn.currentUser, isNull); diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart index b27e3aef406c..bce011e7cd92 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart @@ -165,4 +165,17 @@ class MockGoogleSignInPlatform extends _i1.Mock ), returnValue: _i4.Future.value(false), ) as _i4.Future); + @override + _i4.Future canAccessScopes( + List? scopes, { + String? accessToken, + }) => + (super.noSuchMethod( + Invocation.method( + #canAccessScopes, + [scopes], + {#accessToken: accessToken}, + ), + returnValue: _i4.Future.value(false), + ) as _i4.Future); } From 57cd228676e4c2d78774e863fa1c9578a2b2cb7e Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 3 Apr 2023 16:35:47 -0700 Subject: [PATCH 2/5] Update deps, versions and example. --- .../google_sign_in/google_sign_in/CHANGELOG.md | 1 + packages/google_sign_in/google_sign_in/README.md | 4 ++++ .../google_sign_in/example/lib/main.dart | 8 +++++--- .../google_sign_in/example/pubspec.yaml | 15 ++------------- .../google_sign_in/google_sign_in/pubspec.yaml | 10 ++-------- 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index e7c287c7ed84..490063d97881 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -3,6 +3,7 @@ * Exposes the new method `canAccessScopes`. * Updates example app to separate Authentication from Authorization for those platforms where scopes are not automatically granted upon signIn (like the web). + * By popular demand: `signInSilently` returns a User object again on the web. * Updates README with information about these changes. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 316da3fb68bb..c049c1018261 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -155,6 +155,8 @@ For more details, take a look at the ## Working with scopes, and incremental authorization. +If your app supports both mobile and web, read this section! + ### Checking if scopes have been granted Users may (or may *not*) grant all the scopes that your application requests at @@ -171,6 +173,8 @@ There's a new method that allows your app to check this: final bool isAuthorized = await _googleSignIn.canAccessScopes(scopes); ``` +(Only implemented in the web from version 6.1.0) + ### Requesting more scopes when needed If your app determines that the user hasn't granted the scopes it requires, it diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index b1400de07f2e..abb587ccdf88 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:convert' show json; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:http/http.dart' as http; @@ -54,9 +55,10 @@ class _SignInDemoState extends State { _googleSignIn.onCurrentUserChanged .listen((GoogleSignInAccount? account) async { - // Check if the account can access scopes... - bool isAuthorized = false; - if (account != null) { + // In mobile, being authenticated means being authorized... + bool isAuthorized = account != null; + // However, in the web... + if (kIsWeb && account != null) { isAuthorized = await _googleSignIn.canAccessScopes(scopes); } diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index cc90258cae02..46791b746cac 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -16,21 +16,10 @@ 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: ../ - google_sign_in_platform_interface: ^2.2.0 - google_sign_in_web: ^0.11.0 + google_sign_in_platform_interface: ^2.4.0 + google_sign_in_web: ^0.12.0 http: ^0.13.0 -dependency_overrides: - google_identity_services_web: - git: - url: https://github.com/ditman/flutter-packages.git - ref: gis-web-fix-render-button-api - path: packages/google_identity_services_web - google_sign_in_platform_interface: - path: ../../google_sign_in_platform_interface - google_sign_in_web: - path: ../../google_sign_in_web - dev_dependencies: espresso: ^0.2.0 flutter_driver: diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 930f0e3b27ea..f2b7bdd1aa8e 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -24,14 +24,8 @@ dependencies: sdk: flutter google_sign_in_android: ^6.1.0 google_sign_in_ios: ^5.5.0 - google_sign_in_platform_interface: ^2.2.0 - google_sign_in_web: ^0.11.0 - -dependency_overrides: - google_sign_in_platform_interface: - path: ../google_sign_in_platform_interface - google_sign_in_web: - path: ../google_sign_in_web + google_sign_in_platform_interface: ^2.4.0 + google_sign_in_web: ^0.12.0 dev_dependencies: build_runner: ^2.1.10 From 63ca82d91b0919b39ac2b8783c56499072330217 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 5 Apr 2023 18:34:02 -0700 Subject: [PATCH 3/5] Improve docs. --- .../google_sign_in/CHANGELOG.md | 6 ++- .../google_sign_in/google_sign_in/README.md | 45 ++++++++++--------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 490063d97881..2b728ba9cb4c 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,9 +1,11 @@ ## 6.1.0 * Exposes the new method `canAccessScopes`. + * This method is only needed, and implemented, on the web platform. + * Other platforms will throw an `UnimplementedError`. * Updates example app to separate Authentication from Authorization for those - platforms where scopes are not automatically granted upon signIn (like the web). - * By popular demand: `signInSilently` returns a User object again on the web. + platforms where scopes are not automatically granted upon `signIn` (like the web). + * When `signInSilently` is successful, it returns a User object again on the web. * Updates README with information about these changes. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index c049c1018261..154e683c3767 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -97,9 +97,9 @@ be an option. ### Web integration The new SDK used by the web has fully separated Authentication from Authorization, -so `signIn` and `signInSilently` no longer authorize Oauth `scopes`. +so `signIn` and `signInSilently` no longer authorize OAuth `scopes`. -Flutter Apps must be able to detect what scopes have been granted by their users, +Flutter apps must be able to detect what scopes have been granted by their users, and if the grants are still valid. Read below about **Working with scopes, and incremental authorization** for @@ -122,7 +122,7 @@ Add the following import to your Dart code: import 'package:google_sign_in/google_sign_in.dart'; ``` -Initialize GoogleSignIn with the scopes you want: +Initialize `GoogleSignIn` with the scopes you want: ```dart GoogleSignIn _googleSignIn = GoogleSignIn( @@ -159,27 +159,28 @@ If your app supports both mobile and web, read this section! ### Checking if scopes have been granted -Users may (or may *not*) grant all the scopes that your application requests at -Sign In. In fact, in the web, no scopes are granted by signIn or silentSignIn anymore. +Users may (or may *not*) grant all the scopes that an application requests at +Sign In. In fact, in the web, no scopes are granted by `signIn`, `silentSignIn` +or the `renderButton` widget anymore. -Your app must be able to: +Applications must be able to: -* Detect if the authenticated user has authorized the scopes your app needs. -* Detect if the scopes that were granted a few minutes ago are still valid. +* Detect if the authenticated user has authorized the scopes they need. +* Determine if the scopes that were granted a few minutes ago are still valid. -There's a new method that allows your app to check this: +There's a new method that enables the checks above, `canAccessScopes`: ```dart final bool isAuthorized = await _googleSignIn.canAccessScopes(scopes); ``` -(Only implemented in the web from version 6.1.0) +_(Only implemented in the web platform, from version 6.1.0 of this package)_ ### Requesting more scopes when needed -If your app determines that the user hasn't granted the scopes it requires, it -should initiate an Authorization request **from an user interaction** (like a -button press). +If an app determines that the user hasn't granted the scopes it requires, it +should initiate an Authorization request. (Remember that in the web platform, +this request **must be initiated from an user interaction**, like a button press). ```dart Future _handleAuthorizeScopes() async { @@ -209,15 +210,19 @@ the `canAccessScopes` and `requestScopes` methods described above. For more details, take a look at the [`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). -### My app didn't need any of this, what gives!? +### Does an app always need to check `canAccessScopes`? -The new web SDK implicitly grant access to `email`, `profile` and `openid` when -users complete the sign-in process (either via the One Tap UX or the Google Sign -In button). +The new web SDK implicitly grant access to the `email`, `profile` and `openid` +scopes when users complete the sign-in process (either via the One Tap UX or the +Google Sign In button). -If your app only needs an `idToken`, or only requests permissions to some of the -[OpenID Connect scopes](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect), -you might not need to implement any of the scope handling above. +If an app only needs an `idToken`, or only requests permissions to any/all of +the three scopes mentioned above +([OpenID Connect scopes](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect)), +it won't need to implement any additional scope handling. + +If an app needs any scope other than `email`, `profile` and `openid`, it **must** +implement a more complete scope handling, as described above. ## Example From 54114b22d981604a4a382a6c4ff643aa5889e4bf Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 5 Apr 2023 18:34:20 -0700 Subject: [PATCH 4/5] Improve comment. --- .../google_sign_in/google_sign_in/lib/google_sign_in.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index ff94949b6cf4..b79fcf7bbdc1 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -191,8 +191,10 @@ class GoogleSignIn { this.forceCodeForRefreshToken = false, }) { // Start initializing. - // Async methods in the plugin will await for this to be done. if (kIsWeb) { + // Start initializing the plugin ASAP, so the `userDataEvents` Stream for + // the web can be used without calling any other methods of the plugin + // (like `silentSignIn` or `isSignedIn`). unawaited(_ensureInitialized()); } } From 4d14626f76deccffa2f12ec1c00c2f047e71a3a6 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 6 Apr 2023 15:21:23 -0700 Subject: [PATCH 5/5] Improve docs. --- packages/google_sign_in/google_sign_in/lib/google_sign_in.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index b79fcf7bbdc1..f3afb11c8e93 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -293,7 +293,7 @@ class GoogleSignIn { return _currentUser; } - // Future that completes when we've finished calling `init` on the native side. + // Future that completes when `init` has completed on the native side. Future? _initialization; // Performs initialization, guarding it with the _initialization future.