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
4 changes: 4 additions & 0 deletions flutter_appauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 CANCEL_PENDING_SESSION_METHOD = "cancelPendingSession";

private static final String DISCOVERY_ERROR_CODE = "discovery_failed";
private static final String AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE =
Expand Down Expand Up @@ -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<>());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a reason to pass an empty HashMap<>()? I believe passing null would suffice

break;
default:
result.notImplemented();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#import "AppAuthIOSAuthorization.h"

@implementation AppAuthIOSAuthorization
@implementation AppAuthIOSAuthorization {
id<OIDExternalUserAgent> _currentExternalUserAgent;
}

- (id<OIDExternalUserAgentSession>)
performAuthorization:(OIDServiceConfiguration *)serviceConfiguration
Expand Down Expand Up @@ -37,6 +39,7 @@ @implementation AppAuthIOSAuthorization
id<OIDExternalUserAgent> agent =
[self userAgentWithViewController:rootViewController
externalUserAgent:externalUserAgent];
_currentExternalUserAgent = agent;
return [OIDAuthState
authStateByPresentingAuthorizationRequest:request
externalUserAgent:agent
Expand Down Expand Up @@ -69,6 +72,7 @@ @implementation AppAuthIOSAuthorization
id<OIDExternalUserAgent> agent =
[self userAgentWithViewController:rootViewController
externalUserAgent:externalUserAgent];
_currentExternalUserAgent = agent;
return [OIDAuthorizationService
presentAuthorizationRequest:request
externalUserAgent:agent
Expand Down Expand Up @@ -137,6 +141,7 @@ @implementation AppAuthIOSAuthorization
id<OIDExternalUserAgent> externalUserAgent =
[self userAgentWithViewController:rootViewController
externalUserAgent:requestParameters.externalUserAgent];
_currentExternalUserAgent = externalUserAgent;

return [OIDAuthorizationService
presentEndSessionRequest:endSessionRequest
Expand Down Expand Up @@ -191,4 +196,21 @@ - (UIViewController *)rootViewController {
return [UIApplication sharedApplication].delegate.window.rootViewController;
}

- (void)cancelPendingSessionWithCompletion:(void (^)(void))completion {
id<OIDExternalUserAgent> agent = _currentExternalUserAgent;
_currentExternalUserAgent = nil;
if (agent) {
[agent dismissExternalUserAgentAnimated:NO
completion:^{
if (completion) {
completion();
}
}];
} else {
if (completion) {
completion();
}
}
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
11 changes: 11 additions & 0 deletions flutter_appauth/lib/src/flutter_appauth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@ class FlutterAppAuth {
Future<EndSessionResponse> 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<void> cancelPendingSession() {
return FlutterAppAuthPlatform.instance.cancelPendingSession();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#import "AppAuthMacOSAuthorization.h"

@implementation AppAuthMacOSAuthorization
@implementation AppAuthMacOSAuthorization {
id<OIDExternalUserAgent> _currentExternalUserAgent;
}

- (id<OIDExternalUserAgentSession>)
performAuthorization:(OIDServiceConfiguration *)serviceConfiguration
Expand Down Expand Up @@ -37,6 +39,7 @@ @implementation AppAuthMacOSAuthorization
NSObject<OIDExternalUserAgent> *agent =
[self userAgentWithPresentingWindow:keyWindow
externalUserAgent:externalUserAgent];
_currentExternalUserAgent = agent;
return [OIDAuthState
authStateByPresentingAuthorizationRequest:request
externalUserAgent:agent
Expand Down Expand Up @@ -69,6 +72,7 @@ @implementation AppAuthMacOSAuthorization
NSObject<OIDExternalUserAgent> *agent =
[self userAgentWithPresentingWindow:keyWindow
externalUserAgent:externalUserAgent];
_currentExternalUserAgent = agent;
return [OIDAuthorizationService
presentAuthorizationRequest:request
externalUserAgent:agent
Expand Down Expand Up @@ -137,6 +141,7 @@ @implementation AppAuthMacOSAuthorization
id<OIDExternalUserAgent> externalUserAgent =
[self userAgentWithPresentingWindow:keyWindow
externalUserAgent:requestParameters.externalUserAgent];
_currentExternalUserAgent = externalUserAgent;
return [OIDAuthorizationService
presentEndSessionRequest:endSessionRequest
externalUserAgent:externalUserAgent
Expand Down Expand Up @@ -172,4 +177,21 @@ @implementation AppAuthMacOSAuthorization
initWithPresentingWindow:presentingWindow];
}

- (void)cancelPendingSessionWithCompletion:(void (^)(void))completion {
id<OIDExternalUserAgent> agent = _currentExternalUserAgent;
_currentExternalUserAgent = nil;
if (agent) {
[agent dismissExternalUserAgentAnimated:NO
completion:^{
if (completion) {
completion();
}
}];
} else {
if (completion) {
completion();
}
}
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -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(@{});

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment on Android side, from what I've seen and tried, calling result(nil) would have sufficed

}];
} else {
result(FlutterMethodNotImplemented);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,15 @@ abstract class FlutterAppAuthPlatform extends PlatformInterface {
Future<EndSessionResponse> 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<void> cancelPendingSession() {
throw UnimplementedError('cancelPendingSession() has not been implemented');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ class MethodChannelFlutterAppAuth extends FlutterAppAuthPlatform {
return EndSessionResponse(result['state']);
}

@override
Future<void> cancelPendingSession() async {
await _channel.invokeMethod<void>('cancelPendingSession');
}

Future<Map<dynamic, dynamic>> invokeMethod(
String method, dynamic arguments) async {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,9 @@ void main() {
})
]);
});

test('cancelPendingSession', () async {
await flutterAppAuth.cancelPendingSession();
expect(log, <Matcher>[isMethodCall('cancelPendingSession', arguments: null)]);
});
}