Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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_sign_in/google_sign_in_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 7.1.0

* Adds support for `disconnect`.

## 7.0.5

* Adds support for `hostedDomain` when authenticating.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dependencies {
implementation 'androidx.credentials:credentials:1.5.0'
implementation 'androidx.credentials:credentials-play-services-auth:1.5.0'
implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1'
implementation 'com.google.android.gms:play-services-auth:21.3.0'
implementation 'com.google.android.gms:play-services-auth:21.4.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-inline:5.2.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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.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;
Expand Down Expand Up @@ -337,12 +338,12 @@ public void clearCredentialState(@NonNull Function1<? super Result<Unit>, Unit>
new CredentialManagerCallback<>() {
@Override
public void onResult(Void result) {
ResultUtilsKt.completeWithClearCredentialStateSuccess(callback);
ResultUtilsKt.completeWithUnitSuccess(callback);
}

@Override
public void onError(@NonNull ClearCredentialException e) {
ResultUtilsKt.completeWithClearCredentialStateError(
ResultUtilsKt.completeWithUnitError(
callback, new FlutterError("Clear Failed", e.getMessage(), null));
}
});
Expand Down Expand Up @@ -440,6 +441,28 @@ public void authorize(
}
}

@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(), "com.google"))
.setScopes(scopes)
.build())
.addOnSuccessListener(unused -> ResultUtilsKt.completeWithUnitSuccess(callback))
.addOnFailureListener(
e ->
ResultUtilsKt.completeWithUnitError(
callback, new FlutterError("removeAccess failed", e.getMessage(), null)));
}

@Override
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_CODE_AUTHORIZE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,53 @@ data class GetCredentialRequestGoogleIdOptionParams(
override fun hashCode(): Int = toList().hashCode()
}

/**
* 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.
*
Expand Down Expand Up @@ -525,20 +572,23 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
}
}
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)
Expand Down Expand Up @@ -567,26 +617,30 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
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)
}
}
Expand All @@ -613,6 +667,8 @@ interface GoogleSignInApi {
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() }
Expand Down Expand Up @@ -717,6 +773,29 @@ interface GoogleSignInApi {
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)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ fun completeWithGetCredentialFailure(
callback(Result.success(failure))
}

fun completeWithClearCredentialStateSuccess(callback: (Result<Unit>) -> Unit) {
fun completeWithUnitSuccess(callback: (Result<Unit>) -> Unit) {
callback(Result.success(Unit))
}

fun completeWithClearCredentialStateError(callback: (Result<Unit>) -> Unit, failure: FlutterError) {
fun completeWithUnitError(callback: (Result<Unit>) -> Unit, failure: FlutterError) {
callback(Result.failure(failure))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.google.android.gms.auth.api.identity.AuthorizationClient;
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.RevokeAccessRequest;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.tasks.OnSuccessListener;
Expand Down Expand Up @@ -71,6 +72,7 @@ public class GoogleSignInTest {
@Mock CustomCredential mockGenericCredential;
@Mock GoogleIdTokenCredential mockGoogleCredential;
@Mock Task<AuthorizationResult> mockAuthorizationTask;
@Mock Task<Void> mockRevokeAccessTask;

private GoogleSignInPlugin flutterPlugin;
// Technically this is not the plugin, but in practice almost all of the functionality is in this
Expand All @@ -88,6 +90,8 @@ public void setUp() {
.thenReturn(GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL);
when(mockAuthorizationTask.addOnSuccessListener(any())).thenReturn(mockAuthorizationTask);
when(mockAuthorizationTask.addOnFailureListener(any())).thenReturn(mockAuthorizationTask);
when(mockRevokeAccessTask.addOnSuccessListener(any())).thenReturn(mockRevokeAccessTask);
when(mockRevokeAccessTask.addOnFailureListener(any())).thenReturn(mockRevokeAccessTask);
when(mockAuthorizationIntent.getIntentSender()).thenReturn(mockAuthorizationIntentSender);
when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity);

Expand Down Expand Up @@ -1144,4 +1148,34 @@ public void clearCredentialState_reportsFailure() {

callbackCaptor.getValue().onError(mock(ClearCredentialException.class));
}

@Test
public void revokeAccess_callsClient() {
final List<String> scopes = new ArrayList<>(List.of("openid"));
final String accountEmail = "[email protected]";
PlatformRevokeAccessRequest params = new PlatformRevokeAccessRequest(accountEmail, scopes);
when(mockAuthorizationClient.revokeAccess(any())).thenReturn(mockRevokeAccessTask);
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(mockRevokeAccessTask).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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class GoogleSignInAndroid extends GoogleSignInPlatform {
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() {
Expand Down Expand Up @@ -109,10 +112,26 @@ class GoogleSignInAndroid extends GoogleSignInPlatform {

@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());
}

Expand Down Expand Up @@ -210,6 +229,10 @@ class GoogleSignInAndroid extends GoogleSignInPlatform {
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;
}
}
Expand All @@ -218,10 +241,11 @@ class GoogleSignInAndroid extends GoogleSignInPlatform {
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,
Expand Down Expand Up @@ -258,6 +282,16 @@ class GoogleSignInAndroid extends GoogleSignInPlatform {
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) {
_cachedAccounts[email] = result.grantedScopes.first;
}
return (
accessToken: accessToken,
serverAuthCode: result.serverAuthCode,
Expand Down
Loading