Looking for

Monday, January 12, 2015

I've been wanted to do a blog post on how to achieve SSO on iOS using sharing Keychain for a bit...

And at the same time, I also wanted to try app extension very badly. So in an attempt to get the best of the two worlds, let's talk about writing a share extension to an app which need to store OAuth2 access token in a secure manner. We'll see how to share Keychain content through group-id between an app and its extension.

Remember Shoot'nShare app?
A simple app that takes pictures and allows you to share them with Facebook, GoogleDrive or even your own Keycloak backend. If we want to learn more about it, visit previous blog posts:

NOTES: Enter a correct bundle id as it will be use in URL schema to specify the callback URL. Please use your own unique BUNDLE_ID with format like org.YOUR_DOMAIN.Shoot replacing YOUR_DOMAIN with your actual domain.

Once completed you will have your information displayed as below:

Now that we've got your google project set up, let's add an Share Extension to Shoot app and see what's involved.

Share Extension

What it is?

An App extension add feature to an existing application.
There are several types of extensions. The one we're interested in today is the share extensions. As the name says it all, this extension lets you share content with the external world. By default Xcode template will inherit from SLComposeServiceViewController. Therefore when hitting share button, a pop-up appears to send a message with image. Before iOS8, only a handset of providers were available to share content with. Those providers were defined directly in the operating system directly so the list was not flexible at all. Those days are over (yay!), you can now share with your favourite or even your own social networks directly from Photos app. This is exactly what we're going to do: let's share to GoogleDrive from Photos app via Shoot'nShare app.

One important thing to bear in mind extensions are not deployed by themselves. They must be packaged within a container app. Concretely in Xcode extensions are extension target within you container app.

2. Define you own bundle_id
To be able to work with extension you need to enable App Groups. App Groups are closely linked to bundle identifiers. So let's change the BUNDLE_ID of the project to match your name. Select the Shoot project in the Project Navigator, and then select the Shoot target from the list of targets. On the General tab, update the Bundle Identifier to org.YOUR_DOMAIN.Shoot replacing YOUR_DOMAIN with your actual domain. Do the same for the extension target: select the Shoot project in the Project Navigator and then select the ShootExt target. On the General tab, update the Bundle Identifier to org.YOUR_DOMAIN.Shoot.ShootExt replacing YOUR_DOMAIN with your actual domain.

3. Configure App Group for Shoot target
In order for Shoot'nShare to share content with its extension, you’ll need to set up an App Group. App Groups allow access to group containers that are shared amongst related apps, or in this case your container app and extension. Select the Shoot project, switch to the Capabilities tab and enable App Groups by flicking the switch. Add a new group, name it group.org.YOUR_DOMAIN.Shoot, again replacing YOUR_DOMAIN with your actual domain.

4. Configure your App Group for ShootExt target
Open the Capabilities tab and enable App Groups. Select the group you created when setting up the Shoot project. The App Group simply allows both the extension and container app to share files.
This is important because of the way files are uploaded when using the extension. Before uploading, image files are saved to the shared container. Then, they are scheduled for upload via a background task.

Sharing Keychain

With the same idea of sharing group between apps (or app and extension) to be able to have a common space for saving files, we can use Keychain group so that app and extension can share Keychain items. In our case we want a common space for Shoot app and Shoot Ext to share OAuth2 access token.

1. Configure Keychain Sharing for Shoot target
In order for Shoot'nShare to share access tokens with its extensions, you’ll need to set up a Keychain Sharing Group. Select the Shoot project in the Project Navigator, and then select the Shoot target from the list of targets.
Now switch to the Capabilities tab and enable Keychain Sharing by flicking the switch. Add a new group, name it org.YOUR_DOMAIN.Shoot, again replacing YOUR_DOMAIN with your actual domain.

2. Configure Keychain Sharing for ShootExt target
Select the Shoot project in the Project Navigator and then select the ShootExt target. Open the Capabilities tab and enable Keychain Sharing. Select the group you created when setting up the Shoot project.

5. Run the extension
To run shoot extension, select ShootExt target and run it, select Photos app as host app.
Select a photo, click on share button and select Shoot app. A Pop-up will appear, select send: you photo is uploaded on the background... and we're done. We've done all the configuration needed. Let's look at the code now.

Spot the Difference

Actually what we want to do from Share extension is basically the same as we do from Shoot'nShare app. But we do in an extension to allow us to do from Photos app. What about playing the difference game? What are the differences between uploading from Shoot'nShare app or uploading from ShootExt?

1. you can not trigger the OAuth2 danse from the extension
Extensions have limitations. Some API are not available. An app extension cannot access a sharedApplication object, and so cannot use any of the methods on that object. Difficult to trigger an external browser to launch the OAuth2 danse. Opening the container app in case no access tokens is available could be an alternative... However this alternative is offered only for today widget extension...

Indeed depending on extension type, some actions are allowed or forbidden. For example quoting apple doc : "only a today widget (and no other app extension type) can ask the system to open its containing app by calling the openURL:completionHandler: method of the NSExtensionContext class."

With our ShootExt, it would have been handy to be able to open Shoot'nShare app if no access token is available in the shared keychain. As our extension is a share extension, this is not available. As a result, we take as a pre-requisite that the end user has already shared a photo from Shoot'nShare app before using the extension. To do so we override OAuth2Module's requestAuthorizationCode method:

public class OAuth2ModuleExtension: OAuth2Module {
// For extension we do not want to be redirected to browser to authenticate
// As a pre-requisite we should have a valid access_token stored in Keychain
override public func requestAuthorizationCode(completionHandler: (AnyObject?, NSError?) -> Void) {
completionHandler("NO_TOKEN", nil)
}
}

In case there is no token, we will return an error message to the end user asking him to use Shoot'nShare first.

2. you use the same redirect-uri OAuth2 for both extension and app
To do so, in ShootExt/ViewController.swift we can not use GoogleConfig class, we'll have to use Config class ans spscify shoot'nShare's redirect url as shown below:

3. you need to save in the Keychain using group-id
When we first save the access token in Shoot'nShare app, we need to specified the group-id, in Shoot/Viewcontroller.swift, we modify shareWithGoogleDrive method to accomodate it: In line7-10 we create a TrustedPersistantOAuth2Session object with a keychain group-id:

4. you upload your photo in the background
Last but not least, when dealing with extension, remember that action will take place in the background!
In our case we want to perform a multipart upload (we're using multipart Google endpoint) in the background. using aerogear-ios-http you can perform multipart background upload either using upload method with stream or file as shown line 28. It's also possible to use a POST method with multipart params (behind the scene a NSURSLSession upload is performed):