[google_sign_in] Implement `disconnect` for Android (#9991)
Adds the missing implementation of `disconnect` using the new `revokeAccess` API, updating `play-services-auth` to the version containing the new API.
Fixes https://github.com/flutter/flutter/issues/169612
## Pre-Review Checklist
[^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.
diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
index eabc8cb..bbabd96 100644
--- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
+++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 7.2.0
+
+* Adds support for `disconnect`.
+
## 7.1.0
* Adds support for the `clearAuthorizationToken` method.
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
index 7242103..1c3b8ab 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
+++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
@@ -34,6 +34,7 @@
import com.google.android.gms.auth.api.identity.AuthorizationResult;
import com.google.android.gms.auth.api.identity.ClearTokenRequest;
import com.google.android.gms.auth.api.identity.Identity;
+import com.google.android.gms.auth.api.identity.RevokeAccessRequest;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.Scope;
import com.google.android.libraries.identity.googleid.GetGoogleIdOption;
@@ -59,6 +60,9 @@
private @Nullable BinaryMessenger messenger;
private ActivityPluginBinding activityPluginBinding;
+ // The account type to use to create an Account object for a Google Sign In account.
+ private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
+
private void initInstance(@NonNull BinaryMessenger messenger, @NonNull Context context) {
initWithDelegate(
messenger,
@@ -384,7 +388,7 @@
}
if (params.getAccountEmail() != null) {
authorizationRequestBuilder.setAccount(
- new Account(params.getAccountEmail(), "com.google"));
+ new Account(params.getAccountEmail(), GOOGLE_ACCOUNT_TYPE));
}
AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build();
authorizationClientFactory
@@ -456,6 +460,28 @@
}
@Override
+ public void revokeAccess(
+ @NonNull PlatformRevokeAccessRequest params,
+ @NonNull Function1<? super Result<Unit>, Unit> callback) {
+ List<Scope> scopes = new ArrayList<>();
+ for (String scope : params.getScopes()) {
+ scopes.add(new Scope(scope));
+ }
+ authorizationClientFactory
+ .create(context)
+ .revokeAccess(
+ RevokeAccessRequest.builder()
+ .setAccount(new Account(params.getAccountEmail(), GOOGLE_ACCOUNT_TYPE))
+ .setScopes(scopes)
+ .build())
+ .addOnSuccessListener(unused -> ResultUtilsKt.completeWithUnitSuccess(callback))
+ .addOnFailureListener(
+ e ->
+ ResultUtilsKt.completeWithUnitError(
+ callback, new FlutterError("revokeAccess failed", e.getMessage(), null)));
+ }
+
+ @Override
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_CODE_AUTHORIZE) {
if (pendingAuthorizationCallback != null) {
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt
index 149c72e..9f446f2 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt
+++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt
@@ -276,6 +276,53 @@
}
/**
+ * Parameters for revoking authorization.
+ *
+ * Corresponds to the native RevokeAccessRequest.
+ * https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/RevokeAccessRequest
+ *
+ * Generated class from Pigeon that represents data sent in messages.
+ */
+data class PlatformRevokeAccessRequest(
+ /** The email for the Google account to revoke authorizations for. */
+ val accountEmail: String,
+ /**
+ * A list of requested scopes.
+ *
+ * Per docs, all granted scopes will be revoked, not only the ones passed here. However, at
+ * least one currently-granted scope must be provided.
+ */
+ val scopes: List<String>
+) {
+ companion object {
+ fun fromList(pigeonVar_list: List<Any?>): PlatformRevokeAccessRequest {
+ val accountEmail = pigeonVar_list[0] as String
+ val scopes = pigeonVar_list[1] as List<String>
+ return PlatformRevokeAccessRequest(accountEmail, scopes)
+ }
+ }
+
+ fun toList(): List<Any?> {
+ return listOf(
+ accountEmail,
+ scopes,
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is PlatformRevokeAccessRequest) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ return MessagesPigeonUtils.deepEquals(toList(), other.toList())
+ }
+
+ override fun hashCode(): Int = toList().hashCode()
+}
+
+/**
* Pigeon equivalent of the native GoogleIdTokenCredential.
*
* Generated class from Pigeon that represents data sent in messages.
@@ -525,20 +572,23 @@
}
}
134.toByte() -> {
+ return (readValue(buffer) as? List<Any?>)?.let { PlatformRevokeAccessRequest.fromList(it) }
+ }
+ 135.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PlatformGoogleIdTokenCredential.fromList(it)
}
}
- 135.toByte() -> {
+ 136.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { GetCredentialFailure.fromList(it) }
}
- 136.toByte() -> {
+ 137.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { GetCredentialSuccess.fromList(it) }
}
- 137.toByte() -> {
+ 138.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { AuthorizeFailure.fromList(it) }
}
- 138.toByte() -> {
+ 139.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { PlatformAuthorizationResult.fromList(it) }
}
else -> super.readValueOfType(type, buffer)
@@ -567,26 +617,30 @@
stream.write(133)
writeValue(stream, value.toList())
}
- is PlatformGoogleIdTokenCredential -> {
+ is PlatformRevokeAccessRequest -> {
stream.write(134)
writeValue(stream, value.toList())
}
- is GetCredentialFailure -> {
+ is PlatformGoogleIdTokenCredential -> {
stream.write(135)
writeValue(stream, value.toList())
}
- is GetCredentialSuccess -> {
+ is GetCredentialFailure -> {
stream.write(136)
writeValue(stream, value.toList())
}
- is AuthorizeFailure -> {
+ is GetCredentialSuccess -> {
stream.write(137)
writeValue(stream, value.toList())
}
- is PlatformAuthorizationResult -> {
+ is AuthorizeFailure -> {
stream.write(138)
writeValue(stream, value.toList())
}
+ is PlatformAuthorizationResult -> {
+ stream.write(139)
+ writeValue(stream, value.toList())
+ }
else -> super.writeValue(stream, value)
}
}
@@ -615,6 +669,8 @@
callback: (Result<AuthorizeResult>) -> Unit
)
+ fun revokeAccess(params: PlatformRevokeAccessRequest, callback: (Result<Unit>) -> Unit)
+
companion object {
/** The codec used by GoogleSignInApi. */
val codec: MessageCodec<Any?> by lazy { MessagesPigeonCodec() }
@@ -742,6 +798,29 @@
channel.setMessageHandler(null)
}
}
+ run {
+ val channel =
+ BasicMessageChannel<Any?>(
+ binaryMessenger,
+ "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.revokeAccess$separatedMessageChannelSuffix",
+ codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List<Any?>
+ val paramsArg = args[0] as PlatformRevokeAccessRequest
+ api.revokeAccess(paramsArg) { result: Result<Unit> ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(MessagesPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(MessagesPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
}
}
}
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
index 7fa058c..e34bd3f 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
+++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
@@ -41,6 +41,7 @@
import com.google.android.gms.auth.api.identity.AuthorizationRequest;
import com.google.android.gms.auth.api.identity.AuthorizationResult;
import com.google.android.gms.auth.api.identity.ClearTokenRequest;
+import com.google.android.gms.auth.api.identity.RevokeAccessRequest;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.tasks.OnSuccessListener;
@@ -72,7 +73,7 @@
@Mock CustomCredential mockGenericCredential;
@Mock GoogleIdTokenCredential mockGoogleCredential;
@Mock Task<AuthorizationResult> mockAuthorizationTask;
- @Mock Task<Void> mockClearTokenTask;
+ @Mock Task<Void> mockVoidTask;
private GoogleSignInPlugin flutterPlugin;
// Technically this is not the plugin, but in practice almost all of the functionality is in this
@@ -90,8 +91,8 @@
.thenReturn(GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL);
when(mockAuthorizationTask.addOnSuccessListener(any())).thenReturn(mockAuthorizationTask);
when(mockAuthorizationTask.addOnFailureListener(any())).thenReturn(mockAuthorizationTask);
- when(mockClearTokenTask.addOnSuccessListener(any())).thenReturn(mockClearTokenTask);
- when(mockClearTokenTask.addOnFailureListener(any())).thenReturn(mockClearTokenTask);
+ when(mockVoidTask.addOnSuccessListener(any())).thenReturn(mockVoidTask);
+ when(mockVoidTask.addOnFailureListener(any())).thenReturn(mockVoidTask);
when(mockAuthorizationIntent.getIntentSender()).thenReturn(mockAuthorizationIntentSender);
when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);
@@ -1150,9 +1151,39 @@
}
@Test
+ public void revokeAccess_callsClient() {
+ final List<String> scopes = new ArrayList<>(List.of("openid"));
+ final String accountEmail = "someone@example.com";
+ PlatformRevokeAccessRequest params = new PlatformRevokeAccessRequest(accountEmail, scopes);
+ when(mockAuthorizationClient.revokeAccess(any())).thenReturn(mockVoidTask);
+ plugin.revokeAccess(
+ params,
+ ResultCompat.asCompatCallback(
+ reply -> {
+ return null;
+ }));
+
+ ArgumentCaptor<RevokeAccessRequest> requestCaptor =
+ ArgumentCaptor.forClass(RevokeAccessRequest.class);
+ verify(mockAuthorizationClient).revokeAccess(requestCaptor.capture());
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<OnSuccessListener<Void>> callbackCaptor =
+ ArgumentCaptor.forClass(OnSuccessListener.class);
+ verify(mockVoidTask).addOnSuccessListener(callbackCaptor.capture());
+ callbackCaptor.getValue().onSuccess(null);
+
+ RevokeAccessRequest request = requestCaptor.getValue();
+ assertEquals(scopes.size(), request.getScopes().size());
+ assertEquals(scopes.get(0), request.getScopes().get(0).getScopeUri());
+ // Account is mostly opaque, so just verify that one was set.
+ assertNotNull(request.getAccount());
+ }
+
+ @Test
public void clearAuthorizationToken_callsClient() {
final String testToken = "testToken";
- when(mockAuthorizationClient.clearToken(any())).thenReturn(mockClearTokenTask);
+ when(mockAuthorizationClient.clearToken(any())).thenReturn(mockVoidTask);
plugin.clearAuthorizationToken(
testToken,
ResultCompat.asCompatCallback(
@@ -1167,7 +1198,7 @@
@SuppressWarnings("unchecked")
ArgumentCaptor<OnSuccessListener<Void>> callbackCaptor =
ArgumentCaptor.forClass(OnSuccessListener.class);
- verify(mockClearTokenTask).addOnSuccessListener(callbackCaptor.capture());
+ verify(mockVoidTask).addOnSuccessListener(callbackCaptor.capture());
callbackCaptor.getValue().onSuccess(null);
ClearTokenRequest request = authRequestCaptor.getValue();
diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
index 81915a6..4cfacbe 100644
--- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
+++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
@@ -21,6 +21,9 @@
String? _serverClientId;
String? _hostedDomain;
String? _nonce;
+ // A cache of accounts that have been successfully authenticated via this
+ // plugin instance, and one of the scopes that has been authorized for it.
+ final Map<String, String> _cachedAccounts = <String, String>{};
/// Registers this class as the default instance of [GoogleSignInPlatform].
static void registerWith() {
@@ -114,10 +117,26 @@
@override
Future<void> disconnect(DisconnectParams params) async {
- // TODO(stuartmorgan): Implement this once Credential Manager adds the
- // necessary API (or temporarily implement it with the deprecated SDK if
- // it becomes a significant issue before the API is added).
- // https://github.com/flutter/flutter/issues/169612
+ // AuthorizationClient requires an account, and at least one currently
+ // granted scope, to request revocation. The app-facing API currently
+ // does not take any parameters, and is documented to revoke all authorized
+ // accounts, so disconnect every account that has been authorized.
+ // TODO(stuartmorgan): Consider deprecating the account-less API at the
+ // app-facing level, and have it instead be an account-level method, to
+ // better align with the current SDKs.
+ for (final MapEntry<String, String> entry in _cachedAccounts.entries) {
+ // Because revokeAccess removes all authorizations for the app, not just
+ // the scopes provided, (per
+ // https://developer.android.com/identity/authorization#revoke-permissions)
+ // an arbitrary granted scope is used here.
+ await _hostApi.revokeAccess(
+ PlatformRevokeAccessRequest(
+ accountEmail: entry.key,
+ scopes: <String>[entry.value],
+ ),
+ );
+ }
+ _cachedAccounts.clear();
await signOut(const SignOutParams());
}
@@ -215,6 +234,10 @@
details: authnResult.details,
);
case GetCredentialSuccess():
+ // Store a preliminary entry using the 'openid' scope, which in practice
+ // always seems to be granted at authentication time, so that an account
+ // that is authenticated but never authorized can still be disconnected.
+ _cachedAccounts[authnResult.credential.id] = 'openid';
return authnResult.credential;
}
}
@@ -223,10 +246,11 @@
AuthorizationRequestDetails request, {
required bool requestOfflineAccess,
}) async {
+ final String? email = request.email;
final AuthorizeResult result = await _hostApi.authorize(
PlatformAuthorizationRequest(
scopes: request.scopes,
- accountEmail: request.email,
+ accountEmail: email,
hostedDomain: _hostedDomain,
serverClientIdForForcedRefreshToken:
requestOfflineAccess ? _serverClientId : null,
@@ -263,6 +287,19 @@
if (accessToken == null) {
return (accessToken: null, serverAuthCode: null);
}
+ // Update the account entry with a scope that was reported as granted,
+ // just in case for some reason 'openid' isn't valid. If the request
+ // wasn't associated with an account, then it won't be available to
+ // disconnect.
+ // TODO(stuartmorgan): If this becomes an issue, see if there is an
+ // indirect way to get the associated email address that's not
+ // deprecated.
+ if (email != null) {
+ final String? scope = result.grantedScopes.firstOrNull;
+ if (scope != null) {
+ _cachedAccounts[email] = scope;
+ }
+ }
return (
accessToken: accessToken,
serverAuthCode: result.serverAuthCode,
diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
index d23bbe6..10bdccd 100644
--- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
+++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
@@ -271,6 +271,59 @@
int get hashCode => Object.hashAll(_toList());
}
+/// Parameters for revoking authorization.
+///
+/// Corresponds to the native RevokeAccessRequest.
+/// https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/RevokeAccessRequest
+class PlatformRevokeAccessRequest {
+ PlatformRevokeAccessRequest({
+ required this.accountEmail,
+ required this.scopes,
+ });
+
+ /// The email for the Google account to revoke authorizations for.
+ String accountEmail;
+
+ /// A list of requested scopes.
+ ///
+ /// Per docs, all granted scopes will be revoked, not only the ones passed
+ /// here. However, at least one currently-granted scope must be provided.
+ List<String> scopes;
+
+ List<Object?> _toList() {
+ return <Object?>[accountEmail, scopes];
+ }
+
+ Object encode() {
+ return _toList();
+ }
+
+ static PlatformRevokeAccessRequest decode(Object result) {
+ result as List<Object?>;
+ return PlatformRevokeAccessRequest(
+ accountEmail: result[0]! as String,
+ scopes: (result[1] as List<Object?>?)!.cast<String>(),
+ );
+ }
+
+ @override
+ // ignore: avoid_equals_and_hash_code_on_mutable_classes
+ bool operator ==(Object other) {
+ if (other is! PlatformRevokeAccessRequest ||
+ other.runtimeType != runtimeType) {
+ return false;
+ }
+ if (identical(this, other)) {
+ return true;
+ }
+ return _deepEquals(encode(), other.encode());
+ }
+
+ @override
+ // ignore: avoid_equals_and_hash_code_on_mutable_classes
+ int get hashCode => Object.hashAll(_toList());
+}
+
/// Pigeon equivalent of the native GoogleIdTokenCredential.
class PlatformGoogleIdTokenCredential {
PlatformGoogleIdTokenCredential({
@@ -555,21 +608,24 @@
} else if (value is GetCredentialRequestGoogleIdOptionParams) {
buffer.putUint8(133);
writeValue(buffer, value.encode());
- } else if (value is PlatformGoogleIdTokenCredential) {
+ } else if (value is PlatformRevokeAccessRequest) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
- } else if (value is GetCredentialFailure) {
+ } else if (value is PlatformGoogleIdTokenCredential) {
buffer.putUint8(135);
writeValue(buffer, value.encode());
- } else if (value is GetCredentialSuccess) {
+ } else if (value is GetCredentialFailure) {
buffer.putUint8(136);
writeValue(buffer, value.encode());
- } else if (value is AuthorizeFailure) {
+ } else if (value is GetCredentialSuccess) {
buffer.putUint8(137);
writeValue(buffer, value.encode());
- } else if (value is PlatformAuthorizationResult) {
+ } else if (value is AuthorizeFailure) {
buffer.putUint8(138);
writeValue(buffer, value.encode());
+ } else if (value is PlatformAuthorizationResult) {
+ buffer.putUint8(139);
+ writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -593,14 +649,16 @@
readValue(buffer)!,
);
case 134:
- return PlatformGoogleIdTokenCredential.decode(readValue(buffer)!);
+ return PlatformRevokeAccessRequest.decode(readValue(buffer)!);
case 135:
- return GetCredentialFailure.decode(readValue(buffer)!);
+ return PlatformGoogleIdTokenCredential.decode(readValue(buffer)!);
case 136:
- return GetCredentialSuccess.decode(readValue(buffer)!);
+ return GetCredentialFailure.decode(readValue(buffer)!);
case 137:
- return AuthorizeFailure.decode(readValue(buffer)!);
+ return GetCredentialSuccess.decode(readValue(buffer)!);
case 138:
+ return AuthorizeFailure.decode(readValue(buffer)!);
+ case 139:
return PlatformAuthorizationResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@@ -776,4 +834,31 @@
return (pigeonVar_replyList[0] as AuthorizeResult?)!;
}
}
+
+ Future<void> revokeAccess(PlatformRevokeAccessRequest params) async {
+ final String pigeonVar_channelName =
+ 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.revokeAccess$pigeonVar_messageChannelSuffix';
+ final BasicMessageChannel<Object?> pigeonVar_channel =
+ BasicMessageChannel<Object?>(
+ pigeonVar_channelName,
+ pigeonChannelCodec,
+ binaryMessenger: pigeonVar_binaryMessenger,
+ );
+ final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
+ <Object?>[params],
+ );
+ final List<Object?>? pigeonVar_replyList =
+ await pigeonVar_sendFuture as List<Object?>?;
+ if (pigeonVar_replyList == null) {
+ throw _createConnectionError(pigeonVar_channelName);
+ } else if (pigeonVar_replyList.length > 1) {
+ throw PlatformException(
+ code: pigeonVar_replyList[0]! as String,
+ message: pigeonVar_replyList[1] as String?,
+ details: pigeonVar_replyList[2],
+ );
+ } else {
+ return;
+ }
+ }
}
diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
index 4b0f5b8..625ae6d 100644
--- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
+++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
@@ -66,6 +66,26 @@
bool autoSelectEnabled;
}
+/// Parameters for revoking authorization.
+///
+/// Corresponds to the native RevokeAccessRequest.
+/// https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/RevokeAccessRequest
+class PlatformRevokeAccessRequest {
+ PlatformRevokeAccessRequest({
+ required this.accountEmail,
+ required this.scopes,
+ });
+
+ /// The email for the Google account to revoke authorizations for.
+ String accountEmail;
+
+ /// A list of requested scopes.
+ ///
+ /// Per docs, all granted scopes will be revoked, not only the ones passed
+ /// here. However, at least one currently-granted scope must be provided.
+ List<String> scopes;
+}
+
/// Pigeon equivalent of the native GoogleIdTokenCredential.
class PlatformGoogleIdTokenCredential {
String? displayName;
@@ -206,4 +226,7 @@
PlatformAuthorizationRequest params, {
required bool promptIfUnauthorized,
});
+
+ @async
+ void revokeAccess(PlatformRevokeAccessRequest params);
}
diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml
index b1f2292..c45ec03 100644
--- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml
+++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml
@@ -2,7 +2,7 @@
description: Android implementation of the google_sign_in plugin.
repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22
-version: 7.1.0
+version: 7.2.0
environment:
sdk: ^3.7.0
diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
index 50ea9b4..894999f 100644
--- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
+++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
@@ -959,10 +959,109 @@
verify(mockApi.clearCredentialState());
});
- test('disconnect also signs out', () async {
- await googleSignIn.disconnect(const DisconnectParams());
+ group('disconnect', () {
+ test('calls through with previously authorized accounts', () async {
+ // Populate the cache of users.
+ const String userEmail = 'user@example.com';
+ const String aScope = 'grantedScope';
+ when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer(
+ (_) async => PlatformAuthorizationResult(
+ grantedScopes: <String>[aScope],
+ accessToken: 'token',
+ ),
+ );
+ await googleSignIn.init(const InitParameters(serverClientId: 'id'));
+ await googleSignIn.clientAuthorizationTokensForScopes(
+ const ClientAuthorizationTokensForScopesParameters(
+ request: AuthorizationRequestDetails(
+ scopes: <String>[aScope],
+ userId: null,
+ email: userEmail,
+ promptIfUnauthorized: false,
+ ),
+ ),
+ );
- verify(mockApi.clearCredentialState());
+ await googleSignIn.disconnect(const DisconnectParams());
+
+ final VerificationResult verification = verify(
+ mockApi.revokeAccess(captureAny),
+ );
+ final PlatformRevokeAccessRequest hostParams =
+ verification.captured[0] as PlatformRevokeAccessRequest;
+ expect(hostParams.accountEmail, userEmail);
+ expect(hostParams.scopes.first, aScope);
+ });
+
+ test(
+ 'calls through with non-authorized accounts, using "openid"',
+ () async {
+ // Populate the cache of users.
+ when(mockApi.getCredential(any)).thenAnswer(
+ (_) async => GetCredentialSuccess(
+ credential: PlatformGoogleIdTokenCredential(
+ displayName: _testUser.displayName,
+ profilePictureUri: _testUser.photoUrl,
+ id: _testUser.email,
+ idToken: _testAuthnToken.idToken!,
+ ),
+ ),
+ );
+ await googleSignIn.init(const InitParameters(serverClientId: 'id'));
+ await googleSignIn.authenticate(const AuthenticateParameters());
+
+ await googleSignIn.disconnect(const DisconnectParams());
+
+ final VerificationResult verification = verify(
+ mockApi.revokeAccess(captureAny),
+ );
+ final PlatformRevokeAccessRequest hostParams =
+ verification.captured[0] as PlatformRevokeAccessRequest;
+ expect(hostParams.accountEmail, _testUser.email);
+ expect(hostParams.scopes.first, 'openid');
+ },
+ );
+
+ test('does not re-revoke for repeated disconnect', () async {
+ // Populate the cache of users.
+ const String userEmail = 'user@example.com';
+ const String aScope = 'grantedScope';
+ when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer(
+ (_) async => PlatformAuthorizationResult(
+ grantedScopes: <String>[aScope],
+ accessToken: 'token',
+ ),
+ );
+ await googleSignIn.init(const InitParameters(serverClientId: 'id'));
+ await googleSignIn.clientAuthorizationTokensForScopes(
+ const ClientAuthorizationTokensForScopesParameters(
+ request: AuthorizationRequestDetails(
+ scopes: <String>[aScope],
+ userId: null,
+ email: userEmail,
+ promptIfUnauthorized: false,
+ ),
+ ),
+ );
+
+ await googleSignIn.disconnect(const DisconnectParams());
+
+ verify(mockApi.revokeAccess(any));
+
+ reset(mockApi);
+
+ // Since no accounts have authorized since the last disconnect, this
+ // should not attempt to revoke anything.
+ await googleSignIn.disconnect(const DisconnectParams());
+
+ verifyNever(mockApi.revokeAccess(any));
+ });
+
+ test('also signs out', () async {
+ await googleSignIn.disconnect(const DisconnectParams());
+
+ verify(mockApi.clearCredentialState());
+ });
});
// Returning null triggers the app-facing package to create stream events,
diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
index 5acdee9..45eeb22 100644
--- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
+++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
@@ -124,4 +124,13 @@
),
)
as _i4.Future<_i2.AuthorizeResult>);
+
+ @override
+ _i4.Future<void> revokeAccess(_i2.PlatformRevokeAccessRequest? params) =>
+ (super.noSuchMethod(
+ Invocation.method(#revokeAccess, [params]),
+ returnValue: _i4.Future<void>.value(),
+ returnValueForMissingStub: _i4.Future<void>.value(),
+ )
+ as _i4.Future<void>);
}