p2 / OAuth2

OAuth2 framework for macOS and iOS, written in Swift.
Other
1.15k stars 278 forks source link

Embedded auth: View is not in the window hierarchy #244

Closed stnor closed 6 years ago

stnor commented 6 years ago

If authorize() need to present the embedded web view (eg there is no valid token), I need to use a 1s delay for performSegue in my vc for it to succeed. Otherwise I get the following error:

2017-11-16 12:32:44.691668+0100 nomp[80704:14781456] Warning: Attempt to present <nomp.NompWebAppViewController: 0x7ff871426650> on <SFSafariViewController: 0x7ff87584ac00> whose view is not in the window hierarchy!

What am I doing wrong?

I can't spot the error in my code, so I wonder if the callback from authorize() can come before the embedded view is properly closed?

Non-embedded works great. I've also tried authorizedEmbedded(from: view), same result as above.

Thanks, Stefan

Code involved:


class GoogleOAuth {

    static let callbackUrl = "com.googleusercontent.apps.....:/oauth"
    static let instance = GoogleOAuth()

    let oauth2 = OAuth2CodeGrant(settings: [
        "authorize_uri": "https://accounts.google.com/o/oauth2/auth",
        "token_uri": "https://www.googleapis.com/oauth2/v3/token",
        "scope": "profile email",
        "redirect_uris": [callbackUrl],
        ])

    private init() {
        oauth2.logger = OAuth2DebugLogger(.trace)
    }

    func authorize(view: UIViewController, completion: @escaping ( Result<OAuthTokenValidationResult>) -> Void) {
        oauth2.authConfig.authorizeEmbedded = true
        oauth2.authConfig.authorizeContext = view

        oauth2.authorize() { authParameters, error in
            if authParameters != nil {
                 self.validateGoogleOauthToken(idToken: self.oauth2.idToken!, completion: completion)
            }
            else {
                print("Authorization was canceled or went wrong: \(String(describing: error))")
                completion(Result.error(NompError.authenticationFailure))
            }
        }
    }

    private func validateOauthToken(url: String, idToken: String, completion: @escaping (Result<OAuthTokenValidationResult>) -> Void) {
        let parameters: [String: String] = [
            "token" : idToken
        ]

        var acceptableStatusCodes:Array<Int> = []
        acceptableStatusCodes.append(contentsOf: 200..<300)
        acceptableStatusCodes.append(404)

        doPost(url, parameters)
            .validate(statusCode: acceptableStatusCodes)
            .responseJSON{response in
                switch(response.result) {
                case .success:
                    let json = self.jsonToMap(response.data)
                    let username = json?["username"] as! String?
                    let status = json?["status"] as! String
                    completion(Result.success(OAuthTokenValidationResult(username: username, status: OAuthTokenValidationStatus(rawValue: status)!)))
                case .failure(let error):
                    completion(Result.error(self.handleError(data: response.data, error: error)))
                }
        }
    }

}
class HomeViewController: UIViewController {

    @IBAction func startGoogleLogin(_ sender: Any) {
        GoogleOAuth.instance.authorize(view: self, completion: { result in
            self.handleOAuthResult(result)
        })
    }

    @IBAction func startMicrosoftLogin(_ sender: Any) {
        MicrosoftOAuth.instance.authorize(view: self, completion: { result in
            self.handleOAuthResult(result)
        })
    }

    private func handleOAuthResult(_ result: Result<OAuthTokenValidationResult>) {
        switch(result) {
        case .success(let auth):
            switch (auth.status) {
            case .SUCCESS:
                print("Success")
                // Without the delay, performSegue fails without the delay...
                DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                    self.performSegue(withIdentifier: "startWebView", sender: self)
                })
            case .FAILURE:
                self.present(NompError.authenticationFailure.createAlert(), animated: true, completion: nil)
            }
        case .error(let error):
            self.present(error.createAlert(), animated: true, completion: nil)
        }
    }
}

AppDelegate:

    func application(_ app: UIApplication,
                     open url: URL,
                     options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
        if (url.absoluteString.contains(GoogleOAuth.callbackUrl)) {
            GoogleOAuth.instance.oauth2.handleRedirectURL(url)
        }
        if (url.absoluteString.contains(MicrosoftOAuth.callbackUrl)) {
            MicrosoftOAuth.instance.oauth2.handleRedirectURL(url)
        }
        return true
    }
p2 commented 6 years ago

The authorize callback is called as soon as you're authorized, it doesn't wait for UI actions. If you need control over UI actions, you can turn off auto-hide, hide the view yourself when the callback comes in so you have more control. Something along:

oauth.authConfig.authorizeEmbeddedAutoDismiss = false

oauth.authorize(params: ["aud": server.aud]) { parameters, error in
    if let vc = oauth.authConfig.authorizeContext as? UIViewController {
        vc.dismiss(animated: true) {
            // callback when dismiss animation is done
        }
    }
}
stnor commented 6 years ago

Thanks so much. That works great.

/Stefan

stnor commented 6 years ago

I didn't actually manage to get this to work reliably as per your suggestion.

I ended up with the following which works every time:

    let url = try! oauth2.authorizeURL(params: nil)
    let authorizer = oauth2.authorizer as! OAuth2Authorizer
    let web = try! authorizer.authorizeSafariEmbedded(from: view, at: url)
    oauth2.afterAuthorizeOrFail = { authParameters, error in
        web.dismiss(animated: true) {
            self.validateToken(authParameters: authParameters, error: error, completion: completion)
        }
    }