Apple Swift Logo

SFSafariViewController and OAuth – the Instagram example

I think, as a developer, I’ve never been so excited and frustrated at the same time by the same thing. Sorry for the non-tech reading that, you can stop here, it’s fine ;) My oh-so-frustrating thing is the brand new SFSafariViewController introduced in WWDC 2015, and especially its interaction with OAuth.

If you are reading this article, you have probably watched WWDC’s session 504: Introduction to Safari View Controller. And you’ve probably browsed through a lot of articles arguing the pros and cons of SFSafariViewController.

Here, I would like to jump straight to the “Web-based authentication” section, when Ricky Mondello showed us the slide below, while saying:


“Because since Safari View Controller has access to a user’s credentials, synced across all of their devices with iCloud Keychain, logging in is going to be a breeze.[…]

 

It takes two steps.
The first is where you would’ve used your own in-app browser, just present an instance of SFSafariViewController.

And once the user is finished logging in and the third-party web service redirects back to your app with the custom URL scheme that you fed it, you can accept that in your AppDelegate‘s handleOpenURL method.

From there you can inspect the response and dismiss the instance of SFSafariViewController because you know that the authentication is done.

That’s it.
Two steps.”

Source: http://asciiwwdc.com/2015/sessions/504#t=1558.026

WWDC2015 504 SFSafariViewController
WWDC 2015 – Session 504 – SFSafariViewController

Intriguing. Two steps? That sounds nearly too easy. I wanted to see some implementations of it. While searching the web, I found that very interesting article from Rizwan Sattar: How iOS 9’s Safari View Controller could completely change your app’s onboarding experience. All of you who have submitted apps to the AppStore know that Apple doesn’t really like the opening of a third-party app in the login workflow. And the “Safari bounce” is not very user-friendly… So let’s have a look at SFSafariViewController, it may solve our dilemma.

Without further ado, let’s do it!

Two steps, let’s see…

  1. Call the SFSafariViewController with your favorite login URL
  2. In the AppDelegate (application:HandleOpenUrl), when you have parsed the response, dismiss the SFSafariViewController.

That sounds so simple, yet it feels like there a little something missing when you try to implement it. Strangely enough, I couldn’t find any tutorial or example of the implementation of OAuth in SFSafariViewController. I searched the web, GitHub, even Twitter: nothing. So I dug into it, and came to a “viable solution”: a communication via Notification between the AppDelegate and the ViewController. It is not revolutionary, but it works like a charm (if you have any suggestions, please leave a comment below or on Twitter @cath_schwz :)

In your ViewController

Here is the basic setup of your ViewController: import SafariServices, SFSafariViewControllerDelegate and its finish method, declare a name (constant) for our notification. And let’s create an outlet to a button and a SFSafariViewController variable.

import SafariServices

let kCloseSafariViewControllerNotification = "kCloseSafariViewControllerNotification"

class ViewController: UIViewController, SFSafariViewControllerDelegate {

    @IBOutlet var loginButton: UIButton!
    var safariVC: SFSafariViewController?
    
    //
    // Magic goes here
    //


    // MARK: - SFSafariViewControllerDelegate
    // Called on "Done" button
    func safariViewControllerDidFinish(controller: SFSafariViewController) {
        controller.dismissViewControllerAnimated(true, completion: nil)
    }
}

Then, let’s create the method called when you tap on the button: the one that will open the SFSafariViewController :

// MARK: - Action
	@IBAction func loginButtonTapped(sender: AnyObject) {
		let authURL = NSURL(string: "") // the URL goes here
		safariVC = SFSafariViewController(URL: authURL)
		safariVC!.delegate = self
		self.presentViewController(safariVC!, animated: true, completion: nil)
	}

At that point, if your outlet and action are correctly linked to your view, you should be able to display and dismiss manually the SFSafariViewController. Don’t forget to put a URL. Any URL will do for now.

Now let’s add the magic: in viewDidLoad, add an observer that will trigger the safariLogin method when the notification is sent. It’s in your safariLogin method that you will deal with the OAuth response and “automatically” dismiss the SFSafariViewController.

override func viewDidLoad() {
        super.viewDidLoad()
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "safariLogin:", name: kCloseSafariViewControllerNotification, object: nil)
    }
    
    func safariLogin(notification: NSNotification) {
        // get the url form the auth callback
        let url = notification.object as! NSURL
        // then do whatever you like, for example :
        // get the code (token) from the URL
        // and do a request to get the information you need (id, name, ...)
        // Finally dismiss the Safari View Controller with:
        self.safariVC!.dismissViewControllerAnimated(true, completion: nil)
    }
}

In your AppDelegate

We just saw what happens when the ViewController receives a notification. Now let’s see how the AppDelegate sends it. Easy, no surprises:

func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {

// just making sure we send the notification when the URL is opened in SFSafariViewController
    if (sourceApplication == "com.apple.SafariViewService") {
        NSNotificationCenter.defaultCenter().postNotificationName(kCloseSafariViewControllerNotification, object: url)
        return true
    }
    return true
}

What about OAuth?

We just covered the easiest part: displaying and dismissing the SFSafariViewController. Now that you have that running, we can add the OAuth component. And to illustrate my example I’ve chosen Instagram. There is no iOS SDK and no specific way to login via Instagram except by displaying a web view, so it makes the perfect candidate for our study.

I have put the whole project on GitHub: SafariOauthLogin. Don’t hesitate to fork it and to play with it. Just follow the instructions and it should be working out of the box :)

Inside the box

I would like to draw your attention on 2 little things:

  1. The OAuth magic that I’m using for Instagram comes from here. You will find it in the Auth.swift file. It’s pretty short and clean. I like it.
  2. In the safariLogin method, I mention a token called “code” that comes form the response URL. That “code” needs to be extracted in order to be used to “trade” actual information from your third-party API. I do so thanks to the method called extractCode

Further reading

I would love to have a one size fits all for OAuth logins, but something light enough that it could be kept up to date with the third-parties’ APIs and wouldn’t break at each and every update. One can dream…
I know there are pretty cool OAuth tools out there, for example: OAuthSwift, OAuth2, aerogear-ios-oauth2. But none of them is using SFSafariViewController yet, except mwermuth who has forked OAuthSwift to use the SFSafariViewController on Instagram login too. I will definitely keep an eye on that!

Two more words, bear with me:
Reddit has a nice OAuth tutorial. I believe it can be easily tweaked to use the SFSafariViewController instead of the web view.
And finally, if you want a comprehensive OAuth 2 with Swift tutorial, you’ll be in good hands there. You may want to keep it for an other day though, it’s quite a long read too!

That’s all folks! I hope you enjoyed it and that you have found some answers there. Happy coding!

Find the full project on GitHub: SafariOauthLogin

31 thoughts on “SFSafariViewController and OAuth – the Instagram example

  1. Fantastic web site. Plenty of helpful information here.
    I am sending it to some buddies ans also sharing in delicious.
    And certainly, thank you on your sweat!

    1. Hi Viraf, thanks for your question. I don’t have any example of code here unfortunately, but have you tried to customize the “code extractor” function I use in my project on GitHub (link at the end of the article). You should be able to tweak it to extract the access token from the URL. I hope that will do!

  2. Hi,
    I have done the Google Sign-In in my app and it work fine.

    Apple reject my app because I don’t use SFSafariViewController, do you have an example of how to preform Google Sign-In with SFSafariViewController?

    1. Hi, thanks for your question.

      I just double checked what I did on an app. How did you integrate Google Sign-In?
      I used the latest version of Google Sign-In for iOS. It is using SFSafariViewController.
      In the AppDelegate, in the application:openUrl.. function, I added the following condition:
      if (url.absoluteString.containsString(GOOGLE_REVERSED_CLIENT_ID)) {
      return GIDSignIn.sharedInstance().handleURL(url, sourceApplication: sourceApplication, annotation: annotation)
      }

      It is similar to what I did for Facebook in this tutorial.
      The app is not published yet so I don’t know if it will be fine on Apple’s side, but I suppose so as it is using SFSafariViewController.

      Hope that helps :)

  3. Hello,
    I tried to follow the tutorial to implement Twitter login but I got stuck on modifying the (Auth.swift) file. How can I get the authorizationURL and access token ?

    Thank you in advance.

    1. Hey Sondos,
      It may be a bit tricky to use it with Twitter, but you can give it a try with those endpoints: https://api.twitter.com/oauth/[authorize | request_token | access_token].
      Personally I used Fabric on my projects. It can be a pain to setup but then it’s easy to use.
      I hope that helps.

  4. hi, in appdelegate,

    func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {

    never gets fired. I noticed that it’s deprecate in iOS9 but the new method,

    func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {

    also don’t get fired. Is there an extra step to connect sfsafariviewcontroller to app delegates methods?

  5. @StrawberryCode, I am having the same issue as qbo, the application openURL function isn’t being called. Any idea what we could be doing wrong?

    Also is that function supposed to be called once the Safari controller opens a URL, or when the controller sees a URL redirect? (such as when the user hits the login button)

    Thanks, your blog is very helpful :D

  6. Hi @qbo and @Paul,

    Thanks for pointing that out! I just updated the code on GitHub to make it work with iOS9 and Cocoapods 1.0.0 beta.

    Also I found out in Apple documentation that the openURL function won’t be called if application:willFinishLaunchingWithOptions: and application:didFinishLaunchingWithOptions: return false.

    I don’t know if that was the issue, but it get’s fired now in the updated GitHub project. Please let me know if you find anything else ;)

  7. Thank you very much for posting this. I have been hunting for weeks to find a way to automatically dismiss the safari view controller without the user having to tap ‘Done’. Using notifications with URL schemes is perfect. The tutorial is very easy to follow. Great job!

  8. Hey, I m not able to extract code and access token for the login with Instagram.
    Can you please help?

  9. I’m like @Paul et @qbo, the app delegate method is never triggered.
    I’m using Swift 3, paid attention to create the URL scheme with the correct URL I set in Instagram and info.plist but nothing…

    1. Hi Nico, I just updated the GitHub project, you can give it a try to see if it solves your problem. I’ve also added a note on the GitHub page, to clarify the URL scheme bit.
      Also, you can double check by reply to John’s question below.
      Hope this helps!

  10. Thanks so much for this post. I am having implementation trouble. After user login is successful, the user is sent to redirect url and nothing happens. On Instagram developer portal, I can only input a http or https url. How to I get the user to be sent back to the app?

    1. Hey @John, when you want the user to be redirected to your app, you can use the app scheme as a URI, for example: myAppName://. You should be able to put that as a “Valid redirect URIs” in the Instagram developer portal.
      In that case, in Auth.swift, you use:
      let INSTAGRAM_REDIRECT_URL = "myAppName://"
      And in info.plist, you put the value myAppName for the key CFBundleURLSchemes.
      Hope this helps :)

  11. Hi @Nico and @John, it looks like it’s related to the Swift 3 update. I’ll have a look at it tonight and I’ll keep you updated as soon as I have a fix for it. Thanks for flagging it up!

  12. Thanks for telling me about url schemes but Instagram developer does not accept formats like myAppName:// as “valid redirect URIs”. The dev portal only accepts web URI’s, for example http://myAppName.com. I’m stuck. Any help I’d appreciate

  13. Hey @StrawberryCode. Same problem just like @john has. Instagram dev portal doesn’t allow custom URL schemes, only http and https. That makes SFSafariViewController usage impossible with Instagram :-(

  14. I’m also lost at how to get the redirect URI to work correctly. Like john, using something like myAppName:// is not working for facebook or instagram.

  15. Hi Im trying to get the Redirect URI for Oauth to work eg:myappname:// but i get an error invalid redirect uri : Missing Authority

  16. Hi guys,

    I will try to find a way around that as soon as possible. I see that Instagram has changed their registration process and don’t take the app name as a redirect URI anymore. That’s a shame. In the meantime, if you have found a way around it, please let me know, I would be very interested in hearing from you.
    While doing research, I stumbled upon this article, it is worth reading: LinkedIn Sign In.

  17. Hi StrawberryCode,
    The opn url function in appdelegate doesnt get called..what changes do i have to make to enable that?
    Thanks

    1. Hey @Dhruv, I just had a look at the code and experimented a bit, but I think that the Open Url function in appDelegate gets called only when/if it returns to the app after login (see the value of CFBundleURLScheme in this example). But Instagram doesn’t seem to accept App Schemes anymore as a callback URL. So the only solution that may work is if you can point to a URL (backend call, for example) that has a redirect to your app, then the app may be able to handle the return from SFSafariViewController via Open Url in the appDelegate. Does it make sense?

Leave a Comment

You want to leave a comment? Sweet! Please do so, but keep in mind that all comments are moderated and rel="nofollow" is in use. So no fake comment or advertisement will be approved. And don't worry, your email address won't be published. Thanks!

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>