Skip to content

Commit b889b40

Browse files
Add support for handling UserRecoverableAuthException (flutter#771)
1 parent 1aef7d9 commit b889b40

File tree

5 files changed

+112
-8
lines changed

5 files changed

+112
-8
lines changed

packages/google_sign_in/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.1.0
2+
3+
* Add support to recover authentication for Android.
4+
15
## 3.0.6
26

37
* Remove flaky displayName assertion

packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import android.support.annotation.NonNull;
1616
import android.util.Log;
1717
import com.google.android.gms.auth.GoogleAuthUtil;
18+
import com.google.android.gms.auth.UserRecoverableAuthException;
1819
import com.google.android.gms.auth.api.Auth;
1920
import com.google.android.gms.auth.api.signin.GoogleSignIn;
2021
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
@@ -89,7 +90,8 @@ public void onMethodCall(MethodCall call, Result result) {
8990

9091
case METHOD_GET_TOKENS:
9192
String email = call.argument("email");
92-
delegate.getTokens(result, email);
93+
boolean shouldRecoverAuth = call.argument("shouldRecoverAuth");
94+
delegate.getTokens(result, email, shouldRecoverAuth);
9395
break;
9496

9597
case METHOD_SIGN_OUT:
@@ -134,8 +136,11 @@ public void init(
134136
/**
135137
* Gets an OAuth access token with the scopes that were specified during initialization for the
136138
* user with the specified email address.
139+
*
140+
* <p>If shouldRecoverAuth is set to true and user needs to recover authentication for method to
141+
* complete, the method will attempt to recover authentication and rerun method.
137142
*/
138-
public void getTokens(final Result result, final String email);
143+
public void getTokens(final Result result, final String email, final boolean shouldRecoverAuth);
139144

140145
/**
141146
* Signs the user out. Their credentials may remain valid, meaning they'll be able to silently
@@ -161,6 +166,7 @@ public void init(
161166
*/
162167
public static final class Delegate implements IDelegate {
163168
private static final int REQUEST_CODE = 53293;
169+
private static final int REQUEST_CODE_RECOVER_AUTH = 12345;
164170
private static final int REQUEST_CODE_RESOLVE_ERROR = 1001;
165171

166172
private static final String ERROR_REASON_EXCEPTION = "exception";
@@ -170,6 +176,8 @@ public static final class Delegate implements IDelegate {
170176
private static final String ERROR_REASON_SIGN_IN_CANCELED = "sign_in_canceled";
171177
private static final String ERROR_REASON_SIGN_IN_REQUIRED = "sign_in_required";
172178
private static final String ERROR_REASON_SIGN_IN_FAILED = "sign_in_failed";
179+
private static final String ERROR_FAILURE_TO_RECOVER_AUTH = "failed_to_recover_auth";
180+
private static final String ERROR_USER_RECOVERABLE_AUTH = "user_recoverable_auth";
173181

174182
private static final String STATE_RESOLVING_ERROR = "resolving_error";
175183

@@ -199,11 +207,15 @@ public GoogleSignInAccount getCurrentAccount() {
199207
}
200208

201209
private void checkAndSetPendingOperation(String method, Result result) {
210+
checkAndSetPendingOperation(method, result, null);
211+
}
212+
213+
private void checkAndSetPendingOperation(String method, Result result, Object data) {
202214
if (pendingOperation != null) {
203215
throw new IllegalStateException(
204216
"Concurrent operations detected: " + pendingOperation.method + ", " + method);
205217
}
206-
pendingOperation = new PendingOperation(method, result);
218+
pendingOperation = new PendingOperation(method, result, data);
207219
}
208220

209221
/**
@@ -308,9 +320,13 @@ public void signIn(Result result) {
308320
/**
309321
* Gets an OAuth access token with the scopes that were specified during initialization for the
310322
* user with the specified email address.
323+
*
324+
* <p>If shouldRecoverAuth is set to true and user needs to recover authentication for method to
325+
* complete, the method will attempt to recover authentication and rerun method.
311326
*/
312327
@Override
313-
public void getTokens(final Result result, final String email) {
328+
public void getTokens(
329+
final Result result, final String email, final boolean shouldRecoverAuth) {
314330
// TODO(issue/11107): Add back the checkAndSetPendingOperation once getTokens is properly
315331
// gated from Dart code. Change result.success/error calls below to use finishWith()
316332
if (email == null) {
@@ -342,7 +358,31 @@ public void run(Future<String> tokenFuture) {
342358
// instead of the value we cached during sign in. At least, that's
343359
// how it works on iOS.
344360
result.success(tokenResult);
345-
} catch (ExecutionException e) {
361+
} catch (final ExecutionException e) {
362+
if (e.getCause() instanceof UserRecoverableAuthException) {
363+
if (shouldRecoverAuth) {
364+
registrar
365+
.activity()
366+
.runOnUiThread(
367+
new Runnable() {
368+
@Override
369+
public void run() {
370+
UserRecoverableAuthException exception =
371+
(UserRecoverableAuthException) e.getCause();
372+
checkAndSetPendingOperation(METHOD_GET_TOKENS, result, email);
373+
registrar
374+
.activity()
375+
.startActivityForResult(
376+
exception.getIntent(), REQUEST_CODE_RECOVER_AUTH);
377+
}
378+
});
379+
} else {
380+
result.error(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null);
381+
}
382+
383+
return;
384+
}
385+
346386
Log.e(TAG, "Exception getting access token", e);
347387
result.error(ERROR_REASON_EXCEPTION, e.getCause().getMessage(), null);
348388
} catch (InterruptedException e) {
@@ -439,10 +479,12 @@ private void finishWithError(String errorCode, String errorMessage) {
439479
private static class PendingOperation {
440480
final String method;
441481
final Result result;
482+
final Object data;
442483

443-
PendingOperation(String method, Result result) {
484+
PendingOperation(String method, Result result, Object data) {
444485
this.method = method;
445486
this.result = result;
487+
this.data = data;
446488
}
447489
}
448490

@@ -464,6 +506,19 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
464506
} else if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
465507
finishWithError(ERROR_REASON_CONNECTION_FAILED, String.valueOf(resultCode));
466508
}
509+
return true;
510+
} else if (requestCode == REQUEST_CODE_RECOVER_AUTH) {
511+
if (resultCode == Activity.RESULT_OK
512+
&& pendingOperation != null
513+
&& pendingOperation.method.equals(METHOD_GET_TOKENS)) {
514+
getTokens(pendingOperation.result, (String) pendingOperation.data, false);
515+
pendingOperation = null;
516+
} else {
517+
finishWithError(
518+
ERROR_FAILURE_TO_RECOVER_AUTH,
519+
"Failed attempt to recover authentication for user " + pendingOperation.data);
520+
}
521+
467522
return true;
468523
}
469524

packages/google_sign_in/lib/google_sign_in.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ class GoogleSignInAccount implements GoogleIdentity {
4040
assert(id != null);
4141
}
4242

43+
// These error codes must match with ones declared on Android and iOS sides.
44+
45+
/// Error code indicating there was a failed attempt to recover user authentication.
46+
static const String kFailedToRecoverAuthError = 'failed_to_recover_auth';
47+
48+
/// Error indicating that authentication can be recovered with user action;
49+
static const String kUserRecoverableAuthError = 'user_recoverable_auth';
50+
4351
@override
4452
final String displayName;
4553

@@ -55,6 +63,16 @@ class GoogleSignInAccount implements GoogleIdentity {
5563
final String _idToken;
5664
final GoogleSignIn _googleSignIn;
5765

66+
/// Retrieve [GoogleSignInAuthentication] for this account.
67+
///
68+
/// [shouldRecoverAuth] sets whether to attempt to recover authentication if
69+
/// user action is needed. If an attempt to recover authentication fails a
70+
/// [PlatformException] is thrown with possible error code
71+
/// [kFailedToRecoverAuthError].
72+
///
73+
/// Otherwise, if [shouldRecoverAuth] is false and the authentication can be
74+
/// recovered by user action a [PlatformException] is thrown with error code
75+
/// [kUserRecoverableAuthError].
5876
Future<GoogleSignInAuthentication> get authentication async {
5977
if (_googleSignIn.currentUser != this) {
6078
throw StateError('User is no longer signed in.');
@@ -63,7 +81,10 @@ class GoogleSignInAccount implements GoogleIdentity {
6381
final Map<dynamic, dynamic> response =
6482
await GoogleSignIn.channel.invokeMethod(
6583
'getTokens',
66-
<String, dynamic>{'email': email},
84+
<String, dynamic>{
85+
'email': email,
86+
'shouldRecoverAuth': true,
87+
},
6788
);
6889
// On Android, there isn't an API for refreshing the idToken, so re-use
6990
// the one we obtained on login.

packages/google_sign_in/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system
33
for signing in with a Google account on Android and iOS.
44
author: Flutter Team <flutter-dev@googlegroups.com>
55
homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in
6-
version: 3.0.6
6+
version: 3.1.0
77

88
flutter:
99
plugin:

packages/google_sign_in/test/google_sign_in_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ void main() {
2929
'signOut': null,
3030
'disconnect': null,
3131
'isSignedIn': true,
32+
'getTokens': <dynamic, dynamic>{
33+
'idToken': '123',
34+
'accessToken': '456',
35+
},
3236
};
3337

3438
final List<MethodCall> log = <MethodCall>[];
@@ -327,6 +331,26 @@ void main() {
327331
],
328332
);
329333
});
334+
335+
test('authentication', () async {
336+
await googleSignIn.signIn();
337+
log.clear();
338+
339+
final GoogleSignInAccount user = googleSignIn.currentUser;
340+
final GoogleSignInAuthentication auth = await user.authentication;
341+
342+
expect(auth.accessToken, '456');
343+
expect(auth.idToken, '123');
344+
expect(
345+
log,
346+
<Matcher>[
347+
isMethodCall('getTokens', arguments: <String, dynamic>{
348+
'email': 'john.doe@gmail.com',
349+
'shouldRecoverAuth': true,
350+
}),
351+
],
352+
);
353+
});
330354
});
331355

332356
group('GoogleSignIn with fake backend', () {

0 commit comments

Comments
 (0)