iOS 13 ASWebAuthenticationSession sign-in alert dismissed without error after app entered background
Describe the bug
On devices running iOS 13, whenever the browser (ASWebAuthenticationSession) is about to be displayed, the system fires the usual 'sign-in' alert. If another event pops to foreground (call, user locks the phone, etc...), upon returning the presentingViewController is correctly displayed but the alert is not visible anymore. In this scenario, the view is left idle waiting for the dialog to complete and the browser to be launched without any additional notification to the app.
To Reproduce Steps to reproduce the behavior:
- Get to a
UIViewControllerand initiate an auth process callingauthStateByPresentingAuthorizationRequest:presentingViewController:callback: - System 'sign-in' alert is displayed
- Lock the device
- Unlock it
-
UIViewControlleris visible but the 'sign-in' alert is not
Expected behavior Either the alert should show up when the app becomes active again (e.g. phone is unlocked), or the app should somehow be notified so that we can cancel the previous attempt and launch a new one.
Test device:
- Device: iPhone SE
- OS: iOS 13.2
- Version: AppAuth-iOS tag
1.2.0
Additional context Exploring app lifecycle callbacks the following behavior has been observed:
- Whenever the alert is displayed
applicationWillResignActiveis called. Afterwards, when locking the phone,applicationDidEnterBackgroundis fired. Then, upon unlocking the device,applicationWillEnterForegroundandapplicationDidBecomeActiveare called back to back. - In any regular
UIViewControllerif the user locks the device,applicationWillResignActiveandapplicationDidEnterBackgroundare called back to back. If then unlocked, behavior remains same as before.
For added background on this topic, I've built a small sample using ASWebAuthenticationSession on iOS 13 as documented here.
Here, a similar behavior is observed. If the device is locked while the alert is being displayed, upon unlocking it's gone and a new session block has to be executed.
import UIKit
import AuthenticationServices
/// Sample built around: https://developer.apple.com/documentation/authenticationservices/authenticating_a_user_through_a_web_service
class ViewController: UIViewController {
@IBOutlet weak var loginButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func tap(sender: UIButton) {
// Use the URL and callback scheme specified by the authorization provider.
guard let authURL = URL(string: "https://example.com/auth") else { return }
let scheme = "exampleauth"
// Initialize the session.
let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: scheme) { callbackURL, error in
print("We are back from auth")
}
session.presentationContextProvider = self
session.start()
}
}
extension ViewController: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return view.window!
}
}
Interesting, this seems to be an Apple bug. Did you file a bug on their bug reporting?
@julienbodet Hadn't. Just did. I'll follow on this.
Update (11/13/2019)
Apple DTS acknowledged that "this is the current functionality" and further request has been forwarded to the engineering team through the Feedback Assistant. Their current position is:
At this time the only course of action is to stay notified the progress of this report and see if it gets into a future release.
No ETA, no roadmap, no nothing so far. ¿Shall the issue be closed?
@jongarate Thanks for the update. I would leave this issue opened for reference until it is fixed, if it ever happens.
So I was trying to use Xamarin.OpenID.AppAuth.iOS which uses this as a dependency, it's basically a C# wrapper around this code but a very old version. I tried to compile the latest AppAuth code to see if I could just drop it into the wrappers project and ran into some major issues. Part of it is I know about nil regarding ObjC. So that being said it was yelling about being built for iOS Deployment target 7. So I ticked it to 10, and then the disappearing browser window went away. Not sure what the lowest version is that would be resolved with this adjustment but I wanted to let you all know and leave it to someone a bit more knowledgeable about this project to see if it was something you could incorporate for the whole thing. Best of luck.
Reproducible on iOS 13.5, iOS 12.4 and iOS 11.4 simulators, which means SFAuthenticationSession is also affected.
So far device locking was the only case I can reproduce it. Are there any other cases?
Weird to see this thread without a workaround. Let me fill that gap =]
According to "Additional context" @jongarate provided (btw, thank you for saving some of my time!) we can observe applicationDidBecomeActive and build a ~hack~ workaround there. This event triggers both when flow goes fine (user complete auth flow or cancel it in system alert) and when this bug happens (user locks device, ask Siri to switch application, etc.). The only difference is that in the 1st case completion handler gets called, while in the 2nd it doesn't.
The only thing I found which differentiates normal cases from this case in applicationDidBecomeActive handler is presenter.presentedViewController: when this bug happens, presenter is not showing anything (private class SFAuthenticationViewController) and this property is nil. In other cases it either has a value or flow finishes itself through OIDExternalUserAgent completion handler.
With RxSwift it looks like that:
appStateDisposable = NotificationCenter.default.rx
.notification(UIApplication.didBecomeActiveNotification)
.delay(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
if self?.presenter.presentedViewController == nil {
// finish flow
}
})
Disposable gets created before calling OIDAuthorizationService.present(...) and gets disposed in its completion handler.
Delay guarantees that workaround won't trigger when system alert closes shortly before opening web overlay.
Here's more logs for different flows I checked. finishAuthFlow there is what normally called from completion handler.
Show logs...
// Cancel in iOS alert >>> OIDAuthorizationService.present >>> AppDelegate.applicationWillResignActive(_:) >>> completionHandler: error: flow cancelled >>> finishAuthFlow >>> AppDelegate.applicationDidBecomeActive(_:)// Dismiss modal overlay
OIDAuthorizationService.present AppDelegate.applicationWillResignActive(:) AppDelegate.applicationDidBecomeActive(:) completionHandler: error: flow cancelled finishAuthFlow
// Move app to background with modal overlay on top
OIDAuthorizationService.present AppDelegate.applicationWillResignActive(:) AppDelegate.applicationDidBecomeActive(:) AppDelegate.applicationWillResignActive(:) // moved AppDelegate.applicationDidEnterBackground(:) AppDelegate.applicationWillEnterForeground(:) AppDelegate.applicationDidBecomeActive(:) completionHandler: error: flow cancelled finishAuthFlow
// Authenticate
OIDAuthorizationService.present AppDelegate.applicationWillResignActive(:) AppDelegate.applicationDidBecomeActive(:) completionHandler: flow finished finishAuthFlow
// Lock device
OIDAuthorizationService.present AppDelegate.applicationWillResignActive(:) AppDelegate.applicationDidEnterBackground(:) AppDelegate.applicationWillEnterForeground(:) AppDelegate.applicationDidBecomeActive(:) // Workaround gets called here
Interesting finding @krin-san. Great effort. Makes me wonder wether this behaviour will remain consistent across different iOS releases, let alone when SwiftUI becomes the standard 🤔.
In my particular case, I ended up launching the system dialog in a login-landing kind of screen. If the issue triggers, the user has a fallback button to start the login once again. Not the most ideal solution, but simple and robust at least.
The way I solved this was by setting a flag to true just before calling performActionWithFreshTokens, which I then set to false again when the function returns.
Then, in sceneDidEnterBackground, if the flag is set to true, I check if the topmost presented view controller is of the same type as the one that has the login button. If it does, it means that we’re still showing the prompt and that we can cancel the current OIDExternalUserAgentSession. Otherwise, the topmost presented view controller will be SFAuthenticationViewController meaning we’re showing the auth flow in the modal view, in which case we don‘t need to cancel.