SafariViewController: How to grab OAuth token from URL?

Brien Crean picture Brien Crean · Aug 7, 2016 · Viewed 21.5k times · Source

Trying to use Facebook OAuth with the SafariViewController. First I open the authURL with SafariViewController, which if the user is logged in to Facebook on Safari, will redirect them and return an OAuth URL with the token for that specific service, e.g. Instagram

RESPONSE: https://www.facebook.com/connect/login_success.html#access_token=BLAHTOKENRESPONSE&expires_in=5114338

When SafariViewController has redirected I want to grab the response URL and store it so I can grab the token. Here is my code:

import SafariServices

let kSafariViewControllerCloseNotification = "kSafariViewControllerCloseNotification"

import UIKit

// facebook OAuth URL for service
let authURL = NSURL(string: "https://www.facebook.com/dialog/oauth?client_id=3627644767&redirect_uri=https://www.facebook.com/connect/login_success.html&scope=basic_info,email,public_profile,user_about_me,user_activities,user_birthday,user_education_history,user_friends,user_interests,user_likes,user_location,user_photos,user_relationship_details&response_type=token")

class ViewController: UIViewController, SFSafariViewControllerDelegate {

    var safariVC: SFSafariViewController?
    @IBOutlet weak var loginButton: UIButton!

    @IBAction func loginButtonTapped(sender: UIButton) {
        safariVC = SFSafariViewController(URL: authURL!)
        safariVC!.delegate = self
        self.presentViewController(safariVC!, animated: true, completion: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // not firing the safariLogin function below
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.safariLogin(_:)), name: kSafariViewControllerCloseNotification, object: nil)
    }

    func safariLogin(notification: NSNotification) {
        print("Safari Login call")
        // get the url form the auth callback
        let url = notification.object as! NSURL
        print(url)
        self.safariVC!.dismissViewControllerAnimated(true, completion: nil)
    }

    func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
        print("application call")
        // just making sure we send the notification when the URL is opened in SFSafariViewController
        if (sourceApplication == "com.application.SafariViewTest") {
            NSNotificationCenter.defaultCenter().postNotificationName(kSafariViewControllerCloseNotification, object: url)
            return true
        }
        return true
    }  

}

It opens the authURL and redirects to the correct response URL, but the observer does not fire the safariLogin function to grab the URL. Any help would be much appreciated!

Thanks so much!

Answer

bfx picture bfx · Feb 24, 2017

iOS 12+

iOS 12 Beta already deprecates SFAuthenticationSession (see below) in favor of ASWebAuthenticationSession. It looks like it is used exactly the same way but requires the new AuthenticationServices framework.

iOS 11

iOS 11 introduced SFAuthenticationSession which is so much easier to handle. Given its nature this beta API may still change but there already are a couple of examples (1, 2) on the internet. First, you need a completion handler that is called with the result of the authentication request:

let completion : SFAuthenticationSession.CompletionHandler = { (callBack:URL?, error:Error?) in
    guard error == nil, let successURL = callBack else {
        return
    }

    let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "oauth_token"}).first

    // Do what you have to do...
}

Then you simply create a SFAuthenticationSession and start it.

let authURL = "https://the.service.you/want/toAuthorizeWith?..."
let scheme = "YOURSCHEME://"
let authSession = SFAuthenticationSession(url: authURL, callbackURLScheme: scheme, completionHandler: completion)
authSession.start()

iOS 10 and before

As some have noted in the comments, the accepted answer is incomplete and won't work on its own. When strolling through Strawberry Code's blog post one can find a link to the related GitHub project. That project's README.MD explains a crucial part of the setup, namely adding the redirect URI to Info.plist. So the whole thing goes down like this:

AppDelegate.swift

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {

   if let sourceApplication = options[.sourceApplication] {
       if (String(describing: sourceApplication) == "com.apple.SafariViewService") {
            NotificationCenter.default.post(name: Notification.Name("CallbackNotification"), object: url)
            return true
        }
    }

    return false
}

ViewController.swift

Register for the notification to call your handler in some sensible place. I'd recommend not doing it in viewDidLoad() but only before you are actually presenting the SFSafariViewController.

NotificationCenter.default.addObserver(self, selector: #selector(safariLogin(_:)), name: Notification.Name("CallbackNotification"), object: nil)
let safariVC = SFSafariViewController(URL: authURL)
safariVC.delegate = self
self.present(safariVC, animated: true, completion: nil)

And then remove the observance in the handler:

@objc func safariLogin(_ notification : Notification) {

    NotificationCenter.default.removeObserver(self, name: Notification.Name("CallbackNotification"), object: nil)

    guard let url = notification.object as? URL else {
        return
    }

    // Parse url ...

}

Remember that the user is able to dismiss the SFSafariViewController by tapping the Done button, so be sure to adopt the SFSafariViewControllerDelegate protocol and remove the observance like this as well:

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
    NotificationCenter.default.removeObserver(self, name: Notification.Name("CallbackNotification"), object: nil)
}

Info.plist

To make it all work you need to add your redirect URI scheme to Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.YOUR.BUNDLE.IDENTIFIER</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>YOURSCHEME</string>
        </array>
    </dict>
</array>

Of course YOURSCHEME has to match the scheme of the redirect URI you registered with the web service you're trying to oAuthorizing with.