Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class FlutterAppauthPlugin
private static final String AUTHORIZE_METHOD = "authorize";
private static final String TOKEN_METHOD = "token";
private static final String END_SESSION_METHOD = "endSession";
private static final String RESUME_PENDING_AUTHORIZATION_METHOD = "resumePendingAuthorization";

private static final String DISCOVERY_ERROR_CODE = "discovery_failed";
private static final String AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE =
Expand Down Expand Up @@ -88,6 +89,7 @@ public class FlutterAppauthPlugin
private Context applicationContext;
private Activity mainActivity;
private PendingOperation pendingOperation;
private PendingAuthorization pendingAuthorization;
Comment thread
moritz-j marked this conversation as resolved.
private String clientSecret;
private boolean allowInsecureConnections;
private AuthorizationService defaultAuthorizationService;
Expand Down Expand Up @@ -198,6 +200,9 @@ public void onMethodCall(MethodCall call, @NonNull Result result) {
finishWithError(END_SESSION_ERROR_CODE, ex.getLocalizedMessage(), ex);
}
break;
case RESUME_PENDING_AUTHORIZATION_METHOD:
handleResumePendingAuthorizationMethodCall(result);
break;
default:
result.notImplemented();
}
Expand Down Expand Up @@ -669,20 +674,32 @@ private String getCauseFromException(@Nullable Exception ex) {

@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent intent) {
if (pendingOperation == null) {
return false;
}
if (requestCode == RC_AUTH_EXCHANGE_CODE || requestCode == RC_AUTH) {
if (intent == null) {
if (pendingOperation == null) {
return false;
}
finishWithError(NULL_INTENT_ERROR_CODE, NULL_INTENT_ERROR_FORMAT, null);
} else {
final AuthorizationResponse authResponse = AuthorizationResponse.fromIntent(intent);
AuthorizationException ex = AuthorizationException.fromIntent(intent);
processAuthorizationData(authResponse, ex, requestCode == RC_AUTH_EXCHANGE_CODE);
return true;
}
final AuthorizationResponse authResponse = AuthorizationResponse.fromIntent(intent);
final AuthorizationException authException = AuthorizationException.fromIntent(intent);
final boolean exchangeCode = requestCode == RC_AUTH_EXCHANGE_CODE;
if (pendingOperation == null) {
// The Flutter call that started this authorization flow no longer has a
// pending Result to complete. This happens when the host activity is killed and recreated
// while in the background of the browser. Stash the response so it can be retrieved
// later via resumePendingAuthorization() to process the authorization on the flutter side.
pendingAuthorization = new PendingAuthorization(authResponse, authException, exchangeCode);
return true;
}
processAuthorizationData(authResponse, authException, exchangeCode);
return true;
}
if (requestCode == RC_END_SESSION) {
if (pendingOperation == null) {
return false;
}
if (intent == null) {
finishWithError(NULL_INTENT_ERROR_CODE, NULL_INTENT_ERROR_FORMAT, null);
} else {
Expand All @@ -701,6 +718,26 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent intent)
return false;
}

private void handleResumePendingAuthorizationMethodCall(Result result) {
if (pendingAuthorization == null) {
result.success(null);
return;
}

final PendingAuthorization pendingAuth = pendingAuthorization;
pendingAuthorization = null;

try {
checkAndSetPendingOperation(RESUME_PENDING_AUTHORIZATION_METHOD, result);
processAuthorizationData(pendingAuth.response, pendingAuth.exception, pendingAuth.exchangeCode);
} catch(Exception ex) {
final String errorCode = pendingAuth.exchangeCode
? AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE
: AUTHORIZE_ERROR_CODE;
finishWithError(errorCode, ex.getLocalizedMessage(), ex);
}
}

private void processAuthorizationData(
final AuthorizationResponse authResponse,
AuthorizationException authException,
Expand Down Expand Up @@ -785,6 +822,22 @@ private class PendingOperation {
}
}

private class PendingAuthorization {
final AuthorizationResponse response;
final AuthorizationException exception;
final boolean exchangeCode;

PendingAuthorization(
AuthorizationResponse response,
AuthorizationException exception,
boolean exchangeCode
) {
this.response = response;
this.exception = exception;
this.exchangeCode = exchangeCode;
}
}

private class TokenRequestParameters {
final String clientId;
final String issuer;
Expand Down
53 changes: 53 additions & 0 deletions flutter_appauth/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);

/*
On Android, the OS can recreate the host Activity (and with it this
Flutter engine/widget tree) while the authorization browser is in the
foreground, e.g. to reclaim memory. When that happens the auth result
arrives natively with nothing on the Dart side awaiting it, so it's
stored and can be retrieved once the app has reinitialized by calling
resumePendingAuthorization().
To test this on Android, enable "Don't keep activities" in the
developer options, start a sign in flow and then complete it. The
Activity will have been destroyed and recreated in the background.
Always returns null on other platforms, so can be called unconditionally.
*/
_resumePendingAuthorization();
}

@override
Expand Down Expand Up @@ -123,6 +137,17 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
child: const Text('Sign in with auto code exchange'),
onPressed: () => _signInWithAutoCodeExchange(),
),
if (Platform.isAndroid)
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
child: const Text(
'Resume pending authorization',
textAlign: TextAlign.center,
),
onPressed: () => _resumePendingAuthorization(),
),
),
if (Platform.isIOS || Platform.isMacOS)
Padding(
padding: const EdgeInsets.all(8.0),
Expand Down Expand Up @@ -405,6 +430,34 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}
}

Future<void> _resumePendingAuthorization() async {
try {
final AuthorizationResumeResponse? result =
await _appAuth.resumePendingAuthorization();
// Returns null when there was nothing pending, e.g. on a normal app
// start rather than a resumption after the Activity was recreated.
if (result == null) {
return;
}
_setBusyState();

// The response depends on whether the pending flow was started with
// authorize() or authorizeAndExchangeCode(). The response can be handled
// just like any standard sign in response.
switch (result) {
case AuthorizationResumeResponseAuthorize(:final response):
_processAuthResponse(response);
case AuthorizationResumeResponseToken(:final response):
_processAuthTokenResponse(response);
await _testApi(response);
}
} catch (e) {
_handleError(e);
} finally {
_clearBusyState();
}
}

bool _isCustomBrowser(ExternalUserAgent externalUserAgent) {
return externalUserAgent == ExternalUserAgent.customBrowserSafari ||
externalUserAgent == ExternalUserAgent.customBrowserChrome ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ static NSString *const AUTHORIZE_AND_EXCHANGE_CODE_METHOD =
@"authorizeAndExchangeCode";
static NSString *const TOKEN_METHOD = @"token";
static NSString *const END_SESSION_METHOD = @"endSession";
static NSString *const RESUME_PENDING_AUTHORIZATION_METHOD =
@"resumePendingAuthorization";
static NSString *const AUTHORIZE_ERROR_CODE = @"authorize_failed";
static NSString *const AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE =
@"authorize_and_exchange_code_failed";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call
[self handleTokenMethodCall:[call arguments] result:result];
} else if ([END_SESSION_METHOD isEqualToString:call.method]) {
[self handleEndSessionMethodCall:[call arguments] result:result];
} else if ([RESUME_PENDING_AUTHORIZATION_METHOD isEqualToString:call.method]) {
// Only Android can lose a pending authorization result to Activity
// recreation; there is never anything to resume here.
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
Expand Down
3 changes: 3 additions & 0 deletions flutter_appauth/lib/flutter_appauth.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart'
show
AuthorizationRequest,
AuthorizationResumeResponse,
AuthorizationResumeResponseAuthorize,
AuthorizationResumeResponseToken,
AuthorizationResponse,
AuthorizationServiceConfiguration,
AuthorizationTokenRequest,
Expand Down
12 changes: 12 additions & 0 deletions flutter_appauth/lib/src/flutter_appauth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,16 @@ class FlutterAppAuth {
Future<EndSessionResponse> endSession(EndSessionRequest request) {
return FlutterAppAuthPlatform.instance.endSession(request);
}

/// On Android, attempts to resume an authorization result that the native
/// platform received while no Dart call is awaiting it. This can happen
/// when the host Activity is recreated (e.g. by the OS reclaiming memory)
/// while the authorization browser is in the foreground.
/// Call this after the app has reinitialized to
/// retrieve that result. Returns null if there is nothing pending.
///
/// Always returns null on other platforms.
Future<AuthorizationResumeResponse?> resumePendingAuthorization() {
return FlutterAppAuthPlatform.instance.resumePendingAuthorization();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call
[self handleTokenMethodCall:[call arguments] result:result];
} else if ([END_SESSION_METHOD isEqualToString:call.method]) {
[self handleEndSessionMethodCall:[call arguments] result:result];
} else if ([RESUME_PENDING_AUTHORIZATION_METHOD isEqualToString:call.method]) {
// Only Android can lose a pending authorization result to Activity
// recreation; there is never anything to resume here.
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'src/authorization_request.dart';
export 'src/authorization_resume_response.dart';
export 'src/authorization_response.dart';
export 'src/authorization_service_configuration.dart';
export 'src/authorization_token_request.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'authorization_response.dart';
import 'authorization_token_response.dart';

/// The result of resuming a pending authorization result that the native
/// platform received while no Dart call was awaiting it.
///
/// Depending on whether the original authorization flow was started with
/// `authorize()` or `authorizeAndExchangeCode()`, this is either
/// [AuthorizationResumeResponseAuthorize] or
/// [AuthorizationResumeResponseToken].
sealed class AuthorizationResumeResponse {
const factory AuthorizationResumeResponse.authorize(
AuthorizationResponse response,
) = AuthorizationResumeResponseAuthorize;

const factory AuthorizationResumeResponse.token(
AuthorizationTokenResponse response,
) = AuthorizationResumeResponseToken;
}

class AuthorizationResumeResponseAuthorize
implements AuthorizationResumeResponse {
final AuthorizationResponse response;

const AuthorizationResumeResponseAuthorize(this.response);
}

class AuthorizationResumeResponseToken implements AuthorizationResumeResponse {
final AuthorizationTokenResponse response;

const AuthorizationResumeResponseToken(this.response);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'authorization_request.dart';
import 'authorization_resume_response.dart';
import 'authorization_response.dart';
import 'authorization_token_request.dart';
import 'authorization_token_response.dart';
Expand Down Expand Up @@ -62,4 +63,17 @@ abstract class FlutterAppAuthPlatform extends PlatformInterface {
Future<EndSessionResponse> endSession(EndSessionRequest request) {
throw UnimplementedError('endSession() has not been implemented');
}

/// On Android, attempts to resume an authorization result that the native
/// platform received while no Dart call is awaiting it. This can happen
/// when the host Activity is recreated (e.g. by the OS reclaiming memory)
/// while the authorization browser is in the foreground.
/// Call this after the app has reinitialized to
/// retrieve that result. Returns null if there is nothing pending.
///
/// Always returns null on other platforms.
Future<AuthorizationResumeResponse?> resumePendingAuthorization() {
throw UnimplementedError(
'resumePendingAuthorization() has not been implemented');
}
}
Loading