diff --git a/flutter_appauth/README.md b/flutter_appauth/README.md index ad6b5e5d..43b67877 100644 --- a/flutter_appauth/README.md +++ b/flutter_appauth/README.md @@ -267,6 +267,10 @@ API docs can be found [here](https://pub.dartlang.org/documentation/flutter_appa ## FAQs +**On iOS/macOS, the "Wants to Use [domain] to Sign In" consent dialog appears automatically when I open the app (e.g. after the app was backgrounded during login). How do I clear it?** + +Call [cancelPendingSession](https://pub.dev/documentation/flutter_appauth/latest/flutter_appauth/FlutterAppAuth/cancelPendingSession.html) early at app startup (e.g. after plugin init, before showing the main UI). This cancels any pending [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) so the system does not re-present it on launch. See [issue #643](https://github.com/MaikuB/flutter_appauth/issues/643). Note: If the app process was killed (cold start), the plugin does not retain a reference to the session; this API helps when the app was only backgrounded. + **When connecting to Azure B2C or Azure AD, the login request redirects properly on Android but not on iOS. What's going on?** The AppAuth iOS SDK has some logic to validate the redirect URL to see if it should be responsible for processing the redirect. This appears to be failing under certain circumstances. Adding a trailing slash to the redirect URL specified in your code has been reported to fix the issue. diff --git a/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java b/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java index a24b4894..a4b4d96f 100644 --- a/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java +++ b/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java @@ -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 CANCEL_PENDING_SESSION_METHOD = "cancelPendingSession"; private static final String DISCOVERY_ERROR_CODE = "discovery_failed"; private static final String AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE = @@ -198,6 +199,10 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { finishWithError(END_SESSION_ERROR_CODE, ex.getLocalizedMessage(), ex); } break; + case CANCEL_PENDING_SESSION_METHOD: + // No-op on Android; only iOS/macOS use ASWebAuthenticationSession. + result.success(new HashMap<>()); + break; default: result.notImplemented(); } diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m index 32e8c19b..e7c0a451 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m @@ -1,6 +1,8 @@ #import "AppAuthIOSAuthorization.h" -@implementation AppAuthIOSAuthorization +@implementation AppAuthIOSAuthorization { + id _currentExternalUserAgent; +} - (id) performAuthorization:(OIDServiceConfiguration *)serviceConfiguration @@ -37,6 +39,7 @@ @implementation AppAuthIOSAuthorization id agent = [self userAgentWithViewController:rootViewController externalUserAgent:externalUserAgent]; + _currentExternalUserAgent = agent; return [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:agent @@ -69,6 +72,7 @@ @implementation AppAuthIOSAuthorization id agent = [self userAgentWithViewController:rootViewController externalUserAgent:externalUserAgent]; + _currentExternalUserAgent = agent; return [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:agent @@ -137,6 +141,7 @@ @implementation AppAuthIOSAuthorization id externalUserAgent = [self userAgentWithViewController:rootViewController externalUserAgent:requestParameters.externalUserAgent]; + _currentExternalUserAgent = externalUserAgent; return [OIDAuthorizationService presentEndSessionRequest:endSessionRequest @@ -191,4 +196,21 @@ - (UIViewController *)rootViewController { return [UIApplication sharedApplication].delegate.window.rootViewController; } +- (void)cancelPendingSessionWithCompletion:(void (^)(void))completion { + id agent = _currentExternalUserAgent; + _currentExternalUserAgent = nil; + if (agent) { + [agent dismissExternalUserAgentAnimated:NO + completion:^{ + if (completion) { + completion(); + } + }]; + } else { + if (completion) { + completion(); + } + } +} + @end diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h index 5683ab88..9de4811a 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h @@ -33,6 +33,7 @@ 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 CANCEL_PENDING_SESSION_METHOD = @"cancelPendingSession"; static NSString *const AUTHORIZE_ERROR_CODE = @"authorize_failed"; static NSString *const AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE = @"authorize_and_exchange_code_failed"; @@ -83,6 +84,10 @@ typedef NS_ENUM(NSInteger, ExternalUserAgent) { requestParameters:(EndSessionRequestParameters *)requestParameters result:(FlutterResult)result; +/// Cancels any pending ASWebAuthenticationSession so it is not re-presented +/// on app launch. Completion is called when the session has been dismissed. +- (void)cancelPendingSessionWithCompletion:(void (^)(void))completion; + @end NS_ASSUME_NONNULL_END diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m index 4d41ea90..e4b5993c 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m @@ -164,6 +164,11 @@ - (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 ([CANCEL_PENDING_SESSION_METHOD isEqualToString:call.method]) { + [authorization cancelPendingSessionWithCompletion:^{ + self.currentAuthorizationFlow = nil; + result(@{}); + }]; } else { result(FlutterMethodNotImplemented); } diff --git a/flutter_appauth/lib/src/flutter_appauth.dart b/flutter_appauth/lib/src/flutter_appauth.dart index 1b326325..1acdef28 100644 --- a/flutter_appauth/lib/src/flutter_appauth.dart +++ b/flutter_appauth/lib/src/flutter_appauth.dart @@ -32,4 +32,15 @@ class FlutterAppAuth { Future endSession(EndSessionRequest request) { return FlutterAppAuthPlatform.instance.endSession(request); } + + /// Cancels any pending [ASWebAuthenticationSession] (iOS/macOS) that may + /// be re-presented on app launch after the app was backgrounded or killed + /// during the auth flow. + /// + /// Call this early at app startup (e.g. after plugin init, before showing + /// the main UI) to clear any stale session so the system consent dialog + /// is not shown automatically. No-op on Android. + Future cancelPendingSession() { + return FlutterAppAuthPlatform.instance.cancelPendingSession(); + } } diff --git a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m index 30a4694c..4449fc90 100644 --- a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m @@ -1,6 +1,8 @@ #import "AppAuthMacOSAuthorization.h" -@implementation AppAuthMacOSAuthorization +@implementation AppAuthMacOSAuthorization { + id _currentExternalUserAgent; +} - (id) performAuthorization:(OIDServiceConfiguration *)serviceConfiguration @@ -37,6 +39,7 @@ @implementation AppAuthMacOSAuthorization NSObject *agent = [self userAgentWithPresentingWindow:keyWindow externalUserAgent:externalUserAgent]; + _currentExternalUserAgent = agent; return [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:agent @@ -69,6 +72,7 @@ @implementation AppAuthMacOSAuthorization NSObject *agent = [self userAgentWithPresentingWindow:keyWindow externalUserAgent:externalUserAgent]; + _currentExternalUserAgent = agent; return [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:agent @@ -137,6 +141,7 @@ @implementation AppAuthMacOSAuthorization id externalUserAgent = [self userAgentWithPresentingWindow:keyWindow externalUserAgent:requestParameters.externalUserAgent]; + _currentExternalUserAgent = externalUserAgent; return [OIDAuthorizationService presentEndSessionRequest:endSessionRequest externalUserAgent:externalUserAgent @@ -172,4 +177,21 @@ @implementation AppAuthMacOSAuthorization initWithPresentingWindow:presentingWindow]; } +- (void)cancelPendingSessionWithCompletion:(void (^)(void))completion { + id agent = _currentExternalUserAgent; + _currentExternalUserAgent = nil; + if (agent) { + [agent dismissExternalUserAgentAnimated:NO + completion:^{ + if (completion) { + completion(); + } + }]; + } else { + if (completion) { + completion(); + } + } +} + @end diff --git a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m index cc36cc1c..f584623c 100644 --- a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m @@ -163,6 +163,11 @@ - (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 ([CANCEL_PENDING_SESSION_METHOD isEqualToString:call.method]) { + [authorization cancelPendingSessionWithCompletion:^{ + self.currentAuthorizationFlow = nil; + result(@{}); + }]; } else { result(FlutterMethodNotImplemented); } diff --git a/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart b/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart index 0985727a..03339a89 100644 --- a/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart +++ b/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart @@ -62,4 +62,15 @@ abstract class FlutterAppAuthPlatform extends PlatformInterface { Future endSession(EndSessionRequest request) { throw UnimplementedError('endSession() has not been implemented'); } + + /// Cancels any pending [ASWebAuthenticationSession] (iOS/macOS) that may + /// be re-presented on app launch after the app was backgrounded or killed + /// during the auth flow. + /// + /// Call this early at app startup (e.g. after plugin init, before showing + /// the main UI) to clear any stale session so the system consent dialog + /// is not shown automatically. No-op on Android. + Future cancelPendingSession() { + throw UnimplementedError('cancelPendingSession() has not been implemented'); + } } diff --git a/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart b/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart index 114a20d3..3f3f4998 100644 --- a/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart +++ b/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart @@ -84,6 +84,11 @@ class MethodChannelFlutterAppAuth extends FlutterAppAuthPlatform { return EndSessionResponse(result['state']); } + @override + Future cancelPendingSession() async { + await _channel.invokeMethod('cancelPendingSession'); + } + Future> invokeMethod( String method, dynamic arguments) async { try { diff --git a/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart b/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart index dd59971e..4099e3ac 100644 --- a/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart +++ b/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart @@ -190,4 +190,9 @@ void main() { }) ]); }); + + test('cancelPendingSession', () async { + await flutterAppAuth.cancelPendingSession(); + expect(log, [isMethodCall('cancelPendingSession', arguments: null)]); + }); }