Tom Harrington Tom Harrington - 1 month ago 8
iOS Question

Logging out with Google OAuth on iOS

I'm using Google's GTMAppAuth to prompt users to log in and authorize access to their Google account. That much is working, and API calls work as expected.

What's not working is logging out. In the GTMAppAuth code, removing authorization is handled by setting the

GTMAppAuthFetcherAuthorization
instance to nil, so that the app can't make API calls for the user account.

Except, when you reauthorize, Google's authorization flow does not require a password to get authorization back. It shows a list of previously used accounts and asks which one you want. If you choose on one, voila, you're in! No need for a password. I have to ask the user's permission, but what if it's a different user? They can get in to the previous user's account, and I need to prevent that. For my app this is not an unusual scenario.

So how do I really log out, so that a password would be required to reauthenticate? I'm setting my own
GTMAppAuthFetcherAuthorization
to nil, and I'm making sure to remove Google's keychain entries, but still no password is required.

Answer Source

In the end the only thing that does what I need is to load Google's "logout" page, as described in another answer. Basically, load https://www.google.com/accounts/Logout.

I had thought that the approach would be to revoke the app's OAuth tokens, but that's not actually what I need. I can revoke them, but Google will then issue another one without requiring a password (or it might be the same key-- it doesn't really matter to me if no password is required). I gather that this is based on browser cookies, but since I'm using SFSafariViewController on iOS I can't inspect the cookies. Revocation is a waste of time if tokens will be reissued like this.

Loading the logout page seems like kind of a hack but it has the useful effect of clearing out whatever browser state allows resuming access without requiring a password from the user.

On iOS this could produce an annoying UI artifact of displaying a web view when the user didn't expect one. But it's easy to prevent that by presenting the SFSafariViewController in a way that keeps the web view hidden. I did it like this, but there are other approaches.

func logout(presentingViewController:UIViewController?) -> Void {
    guard let presentingViewController = presentingViewController else {
        fatalError("A presenting view controller is required")
    }

    let logoutUrl = URL(string: "https://www.google.com/accounts/Logout")!
    let logoutVC = SFSafariViewController(url: logoutUrl)
    logoutVC.delegate = self

    presentingViewController.addChildViewController(logoutVC)
    presentingViewController.view.addSubview(logoutVC.view)
    presentingViewController.view.sendSubview(toBack: logoutVC.view)
    logoutVC.didMove(toParentViewController: presentingViewController)

    // Remove our OAuth token
    self.authorization = nil
}

The delegate assignment is important. In the delegate, I implemented this to dismiss the SFSafariViewController as soon as the logout page loads:

func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
    controller.didMove(toParentViewController: nil)
    controller.view.removeFromSuperview()
    controller.removeFromParentViewController()
}