Skip to content

[google_sign_in] Redesign API for current identity SDKs #9267

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

Open
wants to merge 47 commits into
base: main
Choose a base branch
from

Conversation

stuartmorgan-g
Copy link
Contributor

This is a full overhaul of the google_sign_in API, with breaking changes for all component packages—including the platform interface. The usual model of adding the new approach while keeping the old one is not viable here, as the underlying SDKs have changed significantly since the original API was designed. Web already had some only-partially-compatible shims for this reason, and Android would have had to do something similar; see flutter/flutter#119300 and flutter/flutter#154205, and the design doc for more background.

Pre-Review Checklist

Footnotes

  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. 2 3

@stuartmorgan-g stuartmorgan-g added the override: allow breaking change Override the check preventing breaking changes to platform interfaces label May 19, 2025
Copy link
Member

@ditman ditman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I have anything blocking. I took a look at google_sign_in, google_sign_in_platform_interface and google_sign_in_web.

isAuthorized = await _googleSignIn.canAccessScopes(scopes);
}
const List<String> scopes = <String>[
'email',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'email' seems to me like an "old school" scope docs, maybe don't show it in this example, or replace by 'https://www.googleapis.com/auth/userinfo.email'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that this was an old/new API distinction. That helps explain https://github.com/flutter/packages/pull/9267/files#diff-30357d9348f31b88b886dbd8d3d1e036c8eb13adcbe360b5c997983520eeed9cR175-R176

Removed since we don't seem to need it. The auth UI still says it'll share name, email, and profile photo without it, so it was probably just redundant.


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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon re-reading this... is this paragraph is a little bit incomplete? IIRC The authentication token (idToken) is also not refreshed, so both authorization and authentication end up expiring, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks that way. I can adjust the README accordingly, but maybe you just didn't mention it because normally (IIUC) clients don't need to keep using ID tokens they way they do access tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a similar note in the authentication section, but pointing out that for authentication generally you should only need to use idToken when it's first received.

user = event.user;
case GoogleSignInAuthenticationEventSignOut():
user = null;
case GoogleSignInAuthenticationEventException():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not specifically related to this example, but... why are we adding something that looks like an "error" event through the onData callback, instead of adding them as an error to the stream so they can be dealt with by a separate onError handler?

Adding an error to the stream also makes the case where the user is doing await stream.first throwy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I just got carried away with structured error returns on iOS and Android. I'll revisit this for both layers of streams.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO I would only change the user-facing stream. In the implementation of the plugin we're already looking at the continuous stream of events coming from the platform code, so maybe the platform -> plugin stream can continue be a single one, with error events ("it never crashes") but the plugin -> app stream can emit errors normally? But yeah, if there's a benefit to making the platform -> plugin also emit errors, I won't oppose it changing :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the app-facing stream, but did end up leaving the platform interface version as-is, adding a comment to the stream method saying to always use the event instead of addError to enforce that they are supposed to always use structured errors instead of random PlatformExceptions and the like.


/// The scopes required by this application.
// #docregion Initialize
// #docregion CheckAuthorization
const List<String> scopes = <String>[
'email',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Mentioned this in the readme above, but again, this 'email' scope might not be needed.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

await user.authorizationClient.authorizationHeaders(scopes);
if (headers == null) {
setState(() {
_contactText = 'Failed to construct authorization headers.';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be _errorMessage, probably?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@@ -27,134 +24,129 @@ abstract class GoogleSignInPlatform extends PlatformInterface {

static final Object _token = Object();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use this opportunity to clean up this _token and the set instance setter tricks and make the abstract class GoogleSignInPlatform an abstract base class? Should this be a separate technical debt cleanup issue? (Are there any downsides to this that I'm not seeing? :S)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did seriously consider doing this, since it can only be done during a breaking change to the platform interface.

I may still tackle it here for that reason, but the downside is that I would have to replace the Mockito-generated mocks with manual mocks, and rewrite the tests accordingly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the downside is that I would have to replace the Mockito-generated mocks with manual mocks, and rewrite the tests accordingly.

Ah yes...

Why is this change considered breaking, though? Is it just in case people are using "implements" for their own federated versions of the plugin, or mocks? If you're using the class "as intended" (with extends), your code shouldn't need to change by us moving it to base class. Would it?

One upside of the extra work, should you want to tackle it, is that the mocks that you create here can be distributed as a testing library from the package, so those users who were creating their own mocks can use officially maintained mock classes that we vend. Similar to: https://pub.dev/documentation/http/1.4.0/testing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change considered breaking, though? Is it just in case people are using "implements" for their own federated versions of the plugin, or mocks? If you're using the class "as intended" (with extends), your code shouldn't need to change by us moving it to base class. Would it?

True, it should only break tests in practice. We could probably consider it non-breaking for that reason, although forcing anyone currently doing mocking to completely rewrite their tests in a non-breaking change seems like more of a grey area than our usual policy for federated plugins that breaking test mocks isn't a breaking change.

the mocks that you create here can be distributed as a testing library from the package

I'm still on the fence about that approach for our plugins; it means we essentially have to maintain an extra implementation, taking feature requests for mocking scenarios that we don't care about ourselves.

.thenAnswer((_) => Future<GoogleSignInUserData>.value(someUser));
group('clientAuthorizationTokensForScopes', () {
const String someAccessToken = '50m3_4cc35_70k3n';
const List<String> scopes = <String>['scope1', 'scope2'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to retain the 'asserts no scopes have any spaces' functionality and test here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused by the fact that the web implementation was asserting this in the first place. Is there some web-specific extra requirement on scopes? Is having spaces that shouldn't be there different from just typoing the scope?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code comes from the first version of the plugin, and this is why I added this, when asked about "why not put this in the app-facing package":

flutter/plugins#2280 (comment)

[Spaces are] a problem here because we use the space as a "join" character to pass the scopes to JS. In the mobile versions, the List is preserved all the way through.

I just checked the API docs again, here: https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#redirecting and scope is still "A space-delimited list of scopes that identify the resources that your application could access on the user's behalf."

IMO we should still assert (and test the assert) that scopes don't contain spaces, but it doesn't look like a super big source of problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting; I'll put it back with a comment explaining it. I wonder if the mobile SDKs fail under the hood, or if they escape the spaces.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-added.

@@ -52,7 +52,6 @@ void main() {
expect(user.id, expectedPersonId);
expect(user.displayName, expectedPersonName);
expect(user.photoUrl, expectedPersonPhoto);
expect(user.idToken, isNull);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we no longer did the People API fallback on the web? Maybe this whole test can be removed as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did indeed just forget to delete these files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

@@ -12,7 +12,6 @@ import 'package:web/web.dart' as web;

import 'button_configuration.dart'
show GSIButtonConfiguration, convertButtonConfiguration;
import 'people.dart' as people;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this people.dart file is now dead code, you can maybe delete it and its tests?

Copy link
Contributor

@camsim99 camsim99 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Android implementation!

Future<PlatformGoogleIdTokenCredential?> _authenticate({
required bool filterToAuthorized,
required bool autoSelectEnabled,
required bool useButtonFlow,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if useButtonFlow is true, then basically the other options are ignored? Would it be helpful to warn users here about that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a wrapper class at this layer and the Pigeon layer that encapsulate the options that are specific to the non-button flow, and also added some docs, to make things clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reworked the API here and at the Pigeon layer to put the options specific to non-button-flow in a class, and added comments, to make it clearer what doesn't apply. (When I first wrote this code I didn't use the button flow, so the flat arguments made sense.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok cool this is much clearer, thank you!

ResultCompat.asCompatCallback(
reply -> {
// This is never called, since this test doesn't trigger the getCredentialsAsync callback.
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could throw an exception to ensure it's not called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added fail() to all of these, and adjusted the comment slightly to explain why.

@@ -13,3 +13,28 @@ should add it to your `pubspec.yaml` as usual.

[1]: https://pub.dev/packages/google_sign_in
[2]: https://flutter.dev/to/endorsed-federated-plugin

## Integration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consider linking to the credentials docs on this? https://developer.android.com/identity/sign-in/credential-manager-siwg#set-google for the non-firebase option? If it's more confusing than helpful because some steps don't apply, maybe not though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the time I was updating the READMEs I was so used to skipping down to the code on that page I forgot it had setup instructions! I've adjusted to link to that as the non-Firebase option, and removed some of the details since they are covered more thoroughly there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahaha totally understandable! LGTM!

@stuartmorgan-g stuartmorgan-g requested a review from ash2moon as a code owner May 29, 2025 02:07
@stuartmorgan-g stuartmorgan-g requested review from camsim99 and ditman May 29, 2025 02:07
@stuartmorgan-g stuartmorgan-g added the triage-ios Should be looked at in iOS triage label May 29, 2025
Copy link
Contributor

@camsim99 camsim99 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Android implementation LGTM!

Future<PlatformGoogleIdTokenCredential?> _authenticate({
required bool filterToAuthorized,
required bool autoSelectEnabled,
required bool useButtonFlow,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok cool this is much clearer, thank you!

@@ -13,3 +13,28 @@ should add it to your `pubspec.yaml` as usual.

[1]: https://pub.dev/packages/google_sign_in
[2]: https://flutter.dev/to/endorsed-federated-plugin

## Integration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahaha totally understandable! LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
override: allow breaking change Override the check preventing breaking changes to platform interfaces p: google_sign_in platform-android platform-ios platform-macos platform-web triage-ios Should be looked at in iOS triage
Projects
None yet
4 participants