July 26, 2015
OAuth 2.0 is super common for authentication these days, largely since it lets you login without giving your password to every app you use. If you’re not familiar with OAuth check out the great explanation on RayWenderlich.com. For example, if you wanted to let an iOS app have some access to your Twitter account, then the OAuth2 auth flow would be:
- The app sends you to Twitter to login
- You login to Twitter to authorize the app (possibly giving it specific limited permissions)
- Twitter sends you back to the app with a token for that app to use
The flow can be a little confusing (in fact, there’s an extra step that we’ll add later) but it means that the iOS app never knows your password. It also lets you revoke its permission later without changing your Twitter password.
When building any app build around an API with OAuth 2.0 authentication the first thing you need to do is setting up that login flow to get a token. So that’s what we’ll do today. We’ll set up a simple Swift app to access the GitHub API to get a list of our repositories on GitHub.
The API call that we need to make is https://api.github.com/user/repos. If you click on that or use something like curl or Postman to make a GET request you’ll get this response:
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}
That error tells us that we need to get an OAuth token to send along with that request. Let’s do that. First we’ll set up a simple project to display a list of GitHub repos, then we’ll implement the OAuth login flow and the authenticated call to get a list of repos.
Fair warning: This tutorial is kinda long. You might want to go to the washroom first or grab a snack.
Project Setup
This tutorial uses Swift 1.2 and was creating using Xcode 6.3.2.
To get started boot up Xcode. Then create a new single view Swift project.
We’ll want a model object class to represent the repos, so create that class:
- Add a Model Class to represent the Repos by going to the File menu then selecting “New”
- Create an iOS/Source file with the type Swift File
- Name it Repo.swift
We need to decide what info we’ll include in our model. We don’t need much but when we check the API docs for repos we can see that the JSON representing a repos has tons of data, mostly about the repository’srepo’s owner:
[
{
"id": 1296269,
"owner": {
"login": "octocat",
"id": 1,
...
},
"name": "Hello-World",
"full_name": "octocat/Hello-World",
"description": "This your first repo!",
"private": false,
"fork": false,
"url": "https://api.github.com/repos/octocat/Hello-World",
"html_url": "https://github.com/octocat/Hello-World"
}
]
We don’t need all of it so let’s just pull out a few key fields for now. We can always add more fields later if we need them.:
class Repo {
var id: String?
var name: String?
var description: String?
var ownerLogin: String?
var url: String?
}
As usual, we’ll use Alamofire and SwiftyJSON to make our API calls and JSON parsing cleaner. To add Alamofire & SwiftyJSON to the project:
Close Xcode
Using the terminal in the project top directory run:
pod init
Open the newly created Podfile and replace the contents with:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!
pod 'Alamofire', '1.2.3'
pod 'SwiftyJSON'
Save the Podfile then switch back to Terminal and run:
pod install
Open the .xcworkspace file in Xcode. Navigate back to your Repos class and import Alamofire & SwiftyJSON:
import Foundation
import Alamofire
import SwiftyJSON
class Repo {
// ...
}
We’ll want to create new repos from JSON so we can add an initializer for that class that takes in JSON to create a repo object:
class Repo {
var id: String?
var name: String?
var description: String?
var ownerLogin: String?
var url: String?
required init(json: JSON) {
self.description = json["description"].string
self.id = json["id"].string
self.name = json["name"].string
self.ownerLogin = json["owner"]["login"].string
self.url = json["url"].string
}
}
If that looks confusing to you, check out REST with Alamofire & SwiftyJSON.
Use Alamofire & SwiftyJSON to extend Alamofire.Request and create a function to pull the data from the API and populate an array of Repos (Follow along with Hooking Up a REST API to a UITableView in Swift but adjust the class & variable names as well as the endpoint and JSON parsing).
First the add the function to the repos class to call the API:
class Repo {
var id: String?
var name: String?
var description: String?
var ownerLogin: String?
var url: String?
required init(json: JSON) {
self.description = json["description"].string
self.id = json["id"].string
self.name = json["name"].string
self.ownerLogin = json["owner"]["login"].string
self.url = json["url"].string
}
class func getMyRepos(completionHandler: (Array<Repo>?, NSError?) -> Void)
{
let path = "https://api.github.com/user/repos"
Alamofire.request(.GET, path)
.validate()
.responseRepoArray { (request, response, repos, error) in
if let anError = error
{
println(anError)
// TODO: parse out errors more specifically
completionHandler(nil, error)
return
}
println(response)
println(request)
completionHandler(repos, nil)
}
}
}
Then the extension to the Alamofire Request:
extension Alamofire.Request {
class func repoArrayResponseSerializer() -> Serializer {
return { request, response, data in
if data == nil {
return (nil, nil)
}
var jsonError: NSError?
let jsonData:AnyObject? = NSJSONSerialization.JSONObjectWithData(data!, options: nil, error: &jsonError)
if jsonData == nil || jsonError != nil
{
return (nil, jsonError)
}
let json = JSON(jsonData!)
if json.error != nil || json == nil
{
return (nil, json.error)
}
var repos:Array = Array<Repo>()
for (key, jsonRepo) in json
{
println(key)
println(jsonRepo)
let repo = Repo(json: jsonRepo)
repos.append(repo)
}
return (repos, nil)
}
}
func responseRepoArray(completionHandler: (NSURLRequest, NSHTTPURLResponse?, Array<Repo>?, NSError?) -> Void) -> Self {
return response(serializer: Request.repoArrayResponseSerializer(), completionHandler: { (request, response, repos, error) in
completionHandler(request, response, repos as? Array<Repo>, error)
})
}
}
In the storyboard add a tableview to the ViewController and hook it up as an IBOutlet. Add a prototype cell and set the cell identifier to “Cell”. Declare and hook up the ViewController as the tableview data source and delegate. In ViewController.swift, add an array of Repo objects and set up the ViewController to display the repos in the array:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var repos: Array<Repo>?
// MARK: - Table View
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.repos?.count ?? 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
let repo = repos?[indexPath.row]
cell.textLabel?.text = repo?.description
if let owner = repo?.ownerLogin
{
cell.detailTextLabel?.text = "By " + owner
}
else
{
cell.detailTextLabel?.text = nil
}
return cell
}
}
Fill out the MasterViewController that Xcode generated so it’ll call the function to get an array of Repos on launch and display them in the tableview cells. Put the API calling code in a function called fetchMyRepos() so we can have one place to modify later if we we want to add filters, change endpoints, etc.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var repos: Array<Repo>?var repos: Array<Repo>?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
fetchMyRepos()
}
func fetchMyRepos()
{
Repo.getMyRepos( { (fetchedRepos, error) in
println("got repos:")
if let receivedError = error
{
println(error)
// TODO: display error
}
else
{
self.repos = fetchedRepos
println(self.repos)
self.tableView.reloadData()
}
})
}
// MARK: - Table View
// ...
}
Ok, now we’ve got a basic app that we can run. But when we do we’ll just get told that we need to comply with the docs and implement the authentication:
Requires authentication
documentation_url
https://developer.github.com/v3
Error Domain=com.alamofire.error Code=-1 "The operation couldn’t be completed. (com.alamofire.error error -1.)"
Organizing the OAuth Code
While working with an API you’ll often end up with a bunch of code that isn’t specific to an object. You might have to set custom headers, keep track of OAuth tokens, handle secrets & IDs and handle authorization or other general errors. To keep this code from being spread out in a bunch of places like the App Delegate and our model objects, create a new GitHubAPIManager class in a new Swift file.
Since there’s only one GitHub API that we’re interacting with, it makes sense to only have a single API manager in our app. So let’s set up this class to have a sharedInstance
that we’ll access to get our single GitHubAPIManager object:
import Foundation
import Alamofire
class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()
}
We know we’ll be doing some network connection work with this class so we’ll just import Alamofire
now.
Get OAuth Token Before Loading Repos
When the app starts up we’ll need to get an OAuth token, if we don’t already have one. So before calling fetchMyRepos
we’ll need to check whether we already have an OAuth token and get one if we don’t.
So in the ViewController, we’ll add a method to do the initial data loading. It’ll grab an OAuth token if we need it and load the repos if we already have a token:
func loadInitialData()
{
if (!GitHubAPIManager.sharedInstance.hasOAuthToken())
{
GitHubAPIManager.sharedInstance.startOAuth2Login()
}
else
{
fetchMyRepos()
}
}
It’ll be the GitHubAPIManager’s job to keep track of whether we have an OAuth token, so we’ll be adding a method to check for one there: GitHubAPIManager.sharedInstance.hasOAuthToken()
. We’ll use that to check if we already have a token.
If we don’t have a token, then we’ll need to kick off the OAuth flow: GitHubAPIManager.sharedInstance.startOAuth2Login()
And if we do already have a token, we can load the repos: fetchMyRepos()
This code needs a few more things to work:
- hasOAuthToken and startOAuth2Login need to be implemented
- We need to load the repos after we get an OAuth token (otherwise the user would have to close & reopen the app to see them)
Let’s tackle the second one first, then we’ll switch over to the GitHubAPIManager. We don’t want to freeze up the app while we’re getting the token so we want to use an asynchronous implementation. That’s implicit already in how we’ve named the function: startOAuth2Login
implies that we’re telling the GitHubAPIManager to go do something but we’re not going to hang around waiting for the results. But then how will we know when it’s done?
Normally we could add a completion handler to the method call. That would let us add a block of code to be called when the method is done. The problem is that part of getting the OAuth token is going to kick us out of the app to Safari before we get redirected back to the app. It’s really, really asynchronous. What we can do instead of passing a block of code to the startOAuth2Login
method (that’ll get forgotten we get kicked to Safari) is to give the block of code to the GitHubAPIManager. Then the GitHubAPIManager can hold on to that code block until we’ve received an OAuth token.
It’ll look like this:
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = {
// code that we want to execute when we get an OAuth token
}
More specifically, we’ll want to check for any errors then fetch the repos if there are no errors:
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = {
(error) -> Void in
println("handlin stuff")
if let receivedError = error
{
println(error)
// TODO: handle error
// Something went wrong, try again
GitHubAPIManager.sharedInstance.startOAuth2Login()
}
else
{
self.fetchMyRepos()
}
}
Ok, so now we have 3 things that GitHubAPIManager needs to do:
- Let us check if we have a token with
hasOAuthToken
- Start up the OAuth authorization flow with
startOAuth2Login
- Accept an
OAuthTokenCompletionHandler
block and call it when we receive a token
More requirements than we started with? Yep, that’s programming for you.
We can fill in a structure for those items so we don’t forget about them later:
import Foundation
import Alamofire
class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()
// handlers for the OAuth process
// stored as vars since sometimes it requires a round trip to safari which
// makes it hard to just keep a reference to it
var OAuthTokenCompletionHandler:(NSError? -> Void)?
func hasOAuthToken() -> Bool
{
// TODO: implement
return false
}
// MARK: - OAuth flow
func startOAuth2Login()
{
// TODO: implement
// TODO: call completionHandler after getting token or error
}
}
We’ll be back to this code shortly after a quick trip to GitHub.
The OAuth Login Flow
To request a token from the GitHub API we can follow the flow in the docs, even though it says it’s for web apps:
- Redirect users to request GitHub access
- GitHub redirects back to your site (app for us) with a code
- Exchange the code for an access token
- Use the access token to access the API
Step 3 is the extra step I referred to at the start of this post. The user doesn’t see it happen so we don’t always think of it as part of the OAuth 2.0 flow but as coders we need to implement it.
Step 1: Send Users to GitHub
So the first thing we need to do is to fire off an HTTP request to GET https://github.com/login/oauth/authorize
with a few parameters:
- client_id
- redirect_uri
- scope
- state
Only the client_id
is required but we’ll provide everything except the redirect_uri
since we can specify that in the web interface.
To get a client ID head over to GitHub: Create a new OAuth app
If you don’t have a GitHub account you’ll need to create a free one. You’ll also need to create one or more repos so you can retrieve them in your API call (public ones are free).
Fill out the form. For the Authorization callback URL (which is the same thing as the redirect_uri param), make up a URL format for your app that starts with some kind of unique ID for your app. For example, I’m using grokGithub://?aParam=paramVal
with grokGithub://
as the custom URL protocol. The ?aParam=paramVal
part isn’t necessary for our code but GitHub wouldn’t accept a callback URL without some kind of parameters.
The Authorization callback URL will be used in step 2 when GitHub sends the user back to our app. For step 1 we just need to copy the client_id from GitHub. We’ll need the client_secret later so we’ll copy that too:
class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()
var clientID: String = "1234567890"
var clientSecret: String = "abcdefghijkl"
...
}
Aside: Ideally we wouldn’t store the client ID & secret in our app since a malicious person could extract it from there. But it greatly simplifies showing how to implement OAuth and this tutorial is plenty long enough as it is. BTW, if you’ve been reading from the top this would be a good time to stand up, stretch and maybe grab a glass of water or go for a short walk. The internet will still be here when you get back.
So now that we’ve got our client ID (and had a nice little break, seriously I did, you totally should), we can set up the URL:
let authPath:String = "https://github.com/login/oauth/authorize?client_id=\(clientID)&scope=repo&state=TEST_STATE"
if let authURL:NSURL = NSURL(string: authPath)
{
// do stuff with authURL
}
and send the user over to Safari so they can authorize their GitHub account for our app:
UIApplication.sharedApplication().openURL(authURL)
in the startOAuth2Login()
method:
// MARK: - OAuth flow
func startOAuth2Login()
{
let authPath:String = "https://github.com/login/oauth/authorize?client_id=\(clientID)&scope=repo&state=TEST_STATE"
if let authURL:NSURL = NSURL(string: authPath)
{
UIApplication.sharedApplication().openURL(authURL)
}
}
Here’s what it looks like when UIApplication.sharedApplication().openURL(authURL)
sends the user to Safari so they can authorize our app for their GitHub account:
So that takes care of step 1. But if we click on the Authorize button on that second web page we’ll get an error:
That’s because GitHub is trying to send the user back to our app using the callback URL that we provided: grokGithub://?aParam=paramVal
. But iOS has no idea what to do with a grokGithub://
URL. So we need to tell iOS that our app will handle grokGithub://
URLs.
Step 2: GitHub Redirects Back
In iOS an app can register a URL scheme. That’s what we’ll use to tell the operating system that our app will handle grokGithub://
URLs. Then GitHub will be able to send the user back to our app along with the authorization code that we’ll later exchange for a token.
Aside: Why do we get a code to exchange for a token instead of just getting the token? Did you notice the state
parameter in step 1? That’s for our security, if we want to implement it. We can send a state
parameter and then make sure we get it back. If we don’t get it back then we can just not finish the OAuth flow and a token doesn’t get generated. That way we can be sure that step 2 is getting fired off by our app, not some random person or bot trying to get access to our GitHub account.
To register a custom URL scheme, open up the info.plist in your xcode project:
Right click on the info.plist and select “Add Row”:
Change the identifier to “URL types”:
It should change the type to Array and add a sub-row “Item 0” with a “URL identifier” in it:
The URL identifier should be unique. The easiest thing to use is your app ID:
And right-click on Item 0 to add another row under it. Make that row “URL Schemes”:
Set the URL Schemes’s Item 0 to your custom URL scheme without the :// (set it to grokGithub, not grokGithub://):
Then switch to the AppDelegate file and add an application:handleOpenURL:
function to indicate that we can open URLs (you can delete most of the boilerplate that Xcode generated to leave just this stuff):
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
}
func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool {
return true
}
}
That should be all you need for the custom URL scheme. To test it, launch your app. The code that we set up for OAuth step 1 above should send you to Safari then back to our app. If you’re having issues with it, revoke the GitHub access for your app on the Authorized Applications tab so you can re-authorize it. We’ll stop it from going to Safari every time you launch later but for now it’s handy to make sure the custom URL scheme is working.
Step 3: Swap the Code for a Token
When GitHub calls our custom URL scheme it passes us a code. We’ll need to process the URL that we got to extract that code and then exchange it for an OAuth token. First we need to send the URL that was used to open the app over to our GitHubAPIManager
since it’s responsible for that kinda stuff. So change the function in the app delegate to:
func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool {
GitHubAPIManager.sharedInstance.processOAuthStep1Response(url)
return true
}
And flip over to the GitHubAPIManager
file to implement processOAuthStep1Response
:
class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()
...
func processOAuthStep1Response(url: NSURL)
{
// TODO: implement
}
}
The URL we’re receiving looks like:
grokgithub://?aParam=paramVal&code=123456789&state=TEST_STATE
Don’t believe me? Add println(url)
in processOAuthStep1Response
to check for yourself.
We need to process that URL to extract the argument after &code=
. Fortunately iOS has tools to process URL components as query items with names and values:
func processOAuthStep1Response(url: NSURL)
{
let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
var code:String?
if let queryItems = components?.queryItems
{
for queryItem in queryItems
{
if (queryItem.name.lowercaseString == "code")
{
code = queryItem.value
break
}
}
}
}
So we can turn the URL into an array of queryItems (which each have a name and a value), then go through those by name until we find the code
item, then grab it’s value.
If we get a code, we can set up the Alamofire request to exchange it for an OAuth token. Checking the GitHub docs we can see that we need to make a POST request to https://github.com/login/oauth/access_token
with our client ID, client secret and the code we just received as parameters:
if let receivedCode = code
{
let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret, "code": receivedCode]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams)
.responseString { (request, response, results, error) in
// TODO: handle response to extract OAuth token
}
}
Once we have that response, we can check for errors (kicking out if we’ve got one) and see what the results look like to figure out how to parse out the OAuth token (assuming there wasn’t an error):
if let anError = error
{
println(anError)
if let completionHandler = self.OAuthTokenCompletionHandler
{
let noOAuthError = NSError(domain: AlamofireErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not obtain an OAuth token", NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(noOAuthError)
}
return
}
println(results)
// like "access_token=999999&scope=repo&token_type=bearer"
If we get an OAuth token we’ll need to store it. For now we’ll just stick it in an var on our GitHubAPIManager. A little later we’ll figure out how to persist it between runs of the app and store it securely:
class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()
var OAuthToken: String?
...
}
To parse out the OAuth token, we’ll step through the parameters in the results:
if let receivedResults = results
{
let resultParams:Array<String> = split(receivedResults) {$0 == "&"}
for param in resultParams
{
let resultsSplit = split(param) { $0 == "=" }
if (resultsSplit.count == 2)
{
let key = resultsSplit[0].lowercaseString // access_token, scope, token_type
let value = resultsSplit[1]
switch key {
case "access_token":
self.OAuthToken = value
case "scope":
// TODO: verify scope
println("SET SCOPE")
case "token_type":
// TODO: verify is bearer
println("CHECK IF BEARER")
default:
println("got more than I expected from the OAuth token exchange")
println(key)
println(value))
}
}
}
}
There’s a bit of code here (twice actually) that might be confusing:
let anArray = split(aString) { $0 == "=" }
The split
global function takes a string and splits it using a code block (ours is { $0 == “=” }
, which means to split on characters that are equal signs).
After we’ve split the string up into key-value pairs, we check each key and figure out what to do with it. To keep things simple, I’ve just tossed in a TODO for each key that we don’t need right now. If you were really deploying this code in an app you’d want to make sure that you get the right type of token (bearer) and have the right kind of scope (repo).
Refresh Tokens
Some OAuth implementations will give you an access token that expires along with a refresh token that you can use to get a new access token later (along with your client ID and client secret). GitHub doesn’t do that. If you’re working with a different OAuth API then you should check the documentation to see if it returns a refresh token. If so, store the refresh token along with access token so you can use it later.
The advantage of using refresh tokens is that a compromised access token can only be used for a set time period. A refresh token is useless without the client ID and secret, so it would be best to secure those as well, but this tutorial is long enough without adding something like creating a webserver to host those values.
Completion Handler
Ok, so we’ve got the OAuth token saved (if we got one):
self.OAuthToken = value
Now we need to tell our View Controller that we got a token and it can use it to make authenticated API calls, like getting our list of repos. Remember way back when we set up a variable to hold a completion handler? Well, now we have a completion to handle :) So let’s do that:
if self.hasOAuthToken()
{
if let completionHandler = self.OAuthTokenCompletionHandler
{
completionHandler(nil)
}
}
else
{
if let completionHandler = self.OAuthTokenCompletionHandler
{
let noOAuthError = NSError(domain: AlamofireErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not obtain an OAuth token", NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(noOAuthError)
}
}
If we got a token, call the completion handler without passing an error to indicate success. If we didn’t get a token, then call the completion handler with an error to say what the problem was.
See any issues with that code? We didn’t update self.hasOAuthToken()
to reflect whether we actually have a token! Better do that or we’ll always get an error returned:
func hasOAuthToken() -> Bool
{
if let token = self.OAuthToken
{
return !token.isEmpty
}
return false
}
Now hasOAuthToken()
is true if we have a token and it’s not blank.
Handling Multiple Launches
So what happens if we run the app now? Well, every time the ViewController gets shown we:
- Check for an OAuth token
- If we don’t have one, we start the OAuth process
- If we do have one, we try to fetch our repos
But the ViewController gets shown each time we launch the app. Including when Safari re-opens the app using our custom URL scheme. That’s a problem since at that point we’ll only have a code, not a token! So it’ll try to start the OAuth process again from step 1 :(
To get around that we can check if we’ve already started the OAuth process. So when we start the OAuth process we’ll save a bool to the NSUserDefaults
that says we’re currently loading the OAuth token:
func startOAuth2Login()
{
let authPath:String = "https://github.com/login/oauth/authorize?client_id=\(clientID)&scope=repo&state=TEST_STATE"
if let authURL:NSURL = NSURL(string: authPath)
{
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(true, forKey: "loadingOAuthToken")
UIApplication.sharedApplication().openURL(authURL)
}
}
And we’ll set it to false when we’ve got an OAuth token (or we’ve failed to do so). We need to do that if we got a URL without a code in it, if we get an error from the POST request, or after we’ve parsed the response to trade a code for a token:
func processOAuthStep1Response(url: NSURL)
{
println(url)
let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
var code:String?
if let queryItems = components?.queryItems
{
for queryItem in queryItems
{
if (queryItem.name.lowercaseString == "code")
{
code = queryItem.value
break
}
}
}
if let receivedCode = code {
let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret, "code": receivedCode]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams)
.responseString { (request, response, results, error) in
if let anError = error
{
println(anError)
if let completionHandler = self.OAuthTokenCompletionHandler
{
let nOAuthError = NSError(domain: AlamofireErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not obtain an OAuth token", NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(nOAuthError)
}
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
return
}
println(results)
if let receivedResults = results
{
let resultParams:Array<String> = split(receivedResults) {$0 == "&"}
for param in resultParams
{
let resultsSplit = split(param) { $0 == "=" }
if (resultsSplit.count == 2)
{
let key = resultsSplit[0].lowercaseString // access_token, scope, token_type
let value = resultsSplit[1]
switch key {
case "access_token":
self.OAuthToken = value
case "scope":
// TODO: verify scope
println("SET SCOPE")
case "token_type":
// TODO: verify is bearer
println("CHECK IF BEARER")
default:
println("got more than I expected from the OAuth token exchange")
}
}
}
}
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
if self.hasOAuthToken()
{
if let completionHandler = self.OAuthTokenCompletionHandler
{
completionHandler(nil)
}
}
else
{
if let completionHandler = self.OAuthTokenCompletionHandler
{
let nOAuthError = NSError(domain: AlamofireErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not obtain an OAuth token", NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(nOAuthError)
}
}
}
}
else
{
// no code in URL that we launched with
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
}
}
Then we can change the ViewController to check whether we’re loading the OAuth token before we start loading data or start the OAuth login process:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let defaults = NSUserDefaults.standardUserDefaults()
if (!defaults.boolForKey("loadingOAuthToken"))
{
loadInitialData()
}
}
NSUserDefaults
is a dictionary that’s persisted between runs of the app. It’s a good place to store small bits of data that need to be kept around but don’t need to be secure.
Using the OAuth Token for API Calls
Ok, so we’ve finally got a token, now what do we do with it? We’ve got to pass it in an Authorization
header with each call to the GitHub API. Instead of adding it to each call individually, we’ll set up GitHubAPIManager
to have a single alamofireManager
with the OAuth token set on it. (I know, we’ve only got one API call but really this is the API manager’s job and this is the cleanest way to implement it there. If we were building out a real app we’d have multiple API calls so this isn’t premature optimization.)
So in the GitHubAPIManager
add an alamofireManager
function. It’ll just give us the existing single instance for our app:
class GitHubAPIManager
{
...
func alamofireManager() -> Manager
{
let manager = Alamofire.Manager.sharedInstance
return manager
}
...
}
Instead of setting the auth header every time we ask for the alamofireManager
, let’s set it once when we start up:
class GitHubAPIManager
{
...
init () {
if hasOAuthToken()
{
addSessionHeader("Authorization", value: "token \(OAuthToken!)")
}
}
...
}
Or when we get a new OAuth token:
class GitHubAPIManager
{
...
var OAuthToken: String?
{
set
{
if let valueToSave = newValue
{
addSessionHeader("Authorization", value: "token \(valueToSave)")
}
else // they set it to nil
{
removeSessionHeaderIfExists("Authorization")
}
}
get
{
// TODO: implement
}
}
...
}
Aside: newValue
is what Swift passes in to a getter to tell us what the user is trying to set it to. So if we had:
GitHubManager.sharedInstance().OAuthToken = "new token"
The newValue
within the set
block for OAuthToken
would be “new token”
.
Now we’ve got a problem: our new setter / getter on the OAuthToken means it’s not getting stored anymore. So this is probably a good time to start storing it securely and saving it between runs of the app.
Storing the OAuth Token Securely
The place to save most secure data in an iOS app is the Keychain. The code to work with the keychain can be pretty ugly so we’ll use a nice library called Locksmith to provide a simpler interface.
To add Locksmith, open up your Podfile in a text editor (outside of Xcode) and add pod ‘Locksmith’
so it looks like:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!
pod 'Alamofire', '1.2.3'
pod 'SwiftyJSON'
pod 'Locksmith'
In the terminal, in the same directory as your Podfile (same as when we set up the Podfile at the start of this tutorial), run:
pod install
When that’s done switch back to Xcode. At the top of the GitHubAPIManager file add import Locksmith
:
import Foundation
import Alamofire
import Locksmith
class GitHubAPIManager
{
...
}
Now using Locksmith we can save & retrieve the OAuth token:
var OAuthToken: String?
{
set
{
if let valueToSave = newValue
{
let error = Locksmith.saveData(["token": valueToSave], forUserAccount: "github")
if let errorReceived = error
{
Locksmith.deleteDataForUserAccount("github")
}
addSessionHeader("Authorization", value: "token \(newValue)")
}
else // they set it to nil, so delete it
{
Locksmith.deleteDataForUserAccount("github")
removeSessionHeaderIfExists("Authorization")
}
}
get
{
// try to load from keychain
let (dictionary, error) = Locksmith.loadDataForUserAccount("github")
if let token = dictionary?["token"] as? String {
return token
}
removeSessionHeaderIfExists("Authorization")
return nil
}
}
We’re getting close to being able to make an authenticated API call, I swear.
Oh yeah… We’ve been writing removeSessionHeaderIfExists
and addSessionHeader
but we haven’t implemented them yet. They’ll work by grabbing the Alamofire manager and setting the manager.session.configuration.HTTPAdditionalHeaders
:
func addSessionHeader(key: String, value: String)
{
let manager = Alamofire.Manager.sharedInstance
if var sessionHeaders = manager.session.configuration.HTTPAdditionalHeaders as? Dictionary<String, String>
{
sessionHeaders[key] = value
manager.session.configuration.HTTPAdditionalHeaders = sessionHeaders
}
else
{
manager.session.configuration.HTTPAdditionalHeaders = [
key: value
]
}
}
func removeSessionHeaderIfExists(key: String)
{
let manager = Alamofire.Manager.sharedInstance
if var sessionHeaders = manager.session.configuration.HTTPAdditionalHeaders as? Dictionary<String, String>getMyRe
{
sessionHeaders.removeValueForKey(key)
manager.session.configuration.HTTPAdditionalHeaders = sessionHeaders
}
}
if var
is similar to if let
except it gives you a variable instead of a constant. Handy, huh?
Set the Accept Header
One more nicety to implement: according to the GitHub API docs, we should set the accept header like: Accept: application/vnd.github.v3+json
. Should is often code for “will break things later if you don’t” so we’ll do that:
(For more details on adding custom HTTP headers, see https://grokswift.com/alamofire-custom-headers/):
class Repo
{
...
func alamofireManager() -> Manager
{
let manager = Alamofire.Manager.sharedInstance
addSessionHeader("Accept", value: "application/vnd.github.v3+json")
return manager
}
...
}
Step 4: Making Authenticated Calls
Ok, so the GitHubAPIManager
looks good, how do we use it? Well, earlier we set up the Repo class to have a getMyRepos
function that looked like this:
class func getMyRepos(completionHandler: (Array<Repo>?, NSError?) -> Void)
{
let path = "https://api.github.com/user/repos"
Alamofire.request(.GET, path)
.validate()
.responseRepoArray { (request, response, repos, error) in
if let anError = error
{
// TODO: parse out errors more specifically
completionHandler(nil, error)
return
}
completionHandler(repos, nil)
}
}
To switch that to use our GitHubAPIManager
(which takes care of getting, setting & persisting the OAuth token), we just need to change Alamofire.request
to use our alamofireManager
:
class func getMyRepos(completionHandler: (Array<Repo>?, NSError?) -> Void)
{
let path = "https://api.github.com/user/repos"
GitHubAPIManager.alamofireManager().request(.GET, path)
.validate()
.responseRepoArray { (request, response, repos, error) in
if let anError = error
{
// TODO: parse out errors more specifically
completionHandler(nil, error)
return
}
completionHandler(repos, nil)
}
}
All that work earlier pays off now to make that nice & simple. You can reuse it for other API calls, even on other objects like gists, just like that (as long as GitHubAPIManager
requests the correct scope when it gets an OAuth token).
And That’s All
I know, that’s a lot to process. Add a comment below if something needs more details or you’re running into issues with an OAuth API. If you have issues while testing, the first thing to try is revoking access so the OAuth process can start fresh. For GitHub you can do that on the Authorized Applications tab. You might also want to wipe out the OAuth token if the getMyRepos
call fails:
Repo.getMyRepos( { (fetchedRepos, error) in
if let receivedError = error
{
println(error)
GitHubAPIManager.sharedInstance.OAuthToken = nil
...
If all else fails, there’s a “Reset Content and Settings” option in the iOS Simulator menu that’ll get you back to a vanilla state (along with revoking the app access in GitHub). You’ll probably find you need to do all 3 of those things sometimes while debugging OAuth but just until you get this token stuff working. Then you’ll never have to look at it again ;)
Here’s the finished code on GitHub: GrokGitHub on GitHub or zip.
If you’d like more Swift tutorials like this one, sign up below to get them sent directly to your inbox. Feel free to come back and visit too, we’re pretty friendly around here but we’ll understand if it’s just easier for us to come to you :)