/ aws

Developer Authenticated Identities Using AWS Cognito in Swift

AWS Cognito is a very scalable, cost-effective way to authenticate users on any platform. It is the opposite of incognito! This tutorial will show you how to authenticate users using Cognito and your own custom back end authentication server (Developer Authenticated).

Update July 15, 2017:
Since AWS Mobile Hub was released, I would highly recommend using it to set up Cognito. AWS made a service called User Pools, an authentication service that plugs into Cognito. I would recommend it instead of making your own developer authentication, it is very customizable. Mobile Hub will generate an SDK in your mobile app language that will already be configure (with examples) to work with your AWS services.

If you are still on an outdated (as of Dec. 2015) AWS SDK and too stubborn to change, read on).

First install AWS SDK in Swift

Follow the setup guide by Amazon found here: Set Up the SDK for iOS
There are 2 options to install the SDK in your iOS project:

  • CocoaPods
  • Frameworks

I find CocoaPods very useful and, once setup, it automates installing new pods (or frameworks).

You will then need to create a bridging header and add the following to your didFinishLaunchingWithOptions method in your AppDelegate.swift file, which runs when you open your app:

let identityProvider = myAppIdentityProvider(
                regionType: AWSRegionType.USEast1,
                identityId: nil,
                accountId: Constants.AWSAccountID.value,
                identityPoolId: Constants.CognitoPoolID.value,
                logins: nil,
                providerName: "www.example.com"
            )

let credentialsProvider = AWSCognitoCredentialsProvider(regionType: AWSRegionType.USEast1, identityProvider: identityProvider, unauthRoleArn: Constants.ARNUnauth.value, authRoleArn: Constants.ARNAuth.value)

let defaultServiceConfiguration = AWSServiceConfiguration(
        region: AWSRegionType.USEast1, credentialsProvider: credentialsProvider)

AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = defaultServiceConfiguration

I created a Constants.swift file where I store constant strings like cognitoIdentityPoolId.

About Cognito Authorization

Cognito has a few purposes, but it's main one is to grant users identities that are tied to roles (which control what access you have to the AWS services API). Here are a couple of examples:

  • You want to run a Lambda script (AWS node-based backend service) to add a user to your newsletter. This user shouldn't have to create a login and password to do this, so Cognito can provide them with an unauthenticated identity, which can be configured to allow the user to invoke a lambda function from their machine.

  • You want to store data specific to a user on your web app in DynamoDB. You can authenticate that user using a social media platform, or your own "Developer Authentication" and then provide these tokens to Cognito in order to grant that user an authenticated identity.

Unauthenticated Identities

First, you might want to be able to get an unauthenticated identity from Cognito for users in Swift. Remember in your AppDelegate file you added code in the didFinishLaunchingWithOptions method? This will be sufficient for AWS to retrieve an unauthenticated identity from the Cognito pool you specified in the credentials provider when you try to invoke a service using the API, so you've already got this set up.

Developer-Authenticated Identities

In order to use your own custom authentication server, you must configure AWS's identityProvider class and override some of it's methods. For this example we will be using LambdAuth as the backend Developer Authentication server which uses AWS Lambda via AWS API Gateway (see How to setup API Gateway with LambdAuth) and is coded in javscript. First, create a new class called myAppIdentityProvider.swift. This file should look like this to begin with:

//
//  myAppIdentityProvider.swift
//

import Foundation
import AWSCore
import AWSCognito

class myAppIdentityProvider: AWSAbstractCognitoIdentityProvider {
    var _token: String!
    var _logins: [ NSObject : AnyObject ]!
    
    override var token: String {
        get {
            return _token
        }
    }
    
    override var logins: [ NSObject : AnyObject ]! {
        get {
            return _logins
        }
        set {
            _logins = newValue!
        }
    }
    
    override func getIdentityId() -> AWSTask! {
        
        if self.identityId != nil {
            return AWSTask(result: self.identityId)
        }else{
            return AWSTask(result: nil).continueWithBlock({ (task) -> AnyObject! in
                if self.identityId == nil {
                    return self.refresh()
                }
                return AWSTask(result: self.identityId)
            })
        }
    }
    
    override func refresh() -> AWSTask! {
        return task        
    }
}

About myAppIdentityProvider

This class extends a class in the AWS Cognito SDK called AWSAbstractCognitoIdentityProvider which we will override to use our custom Developer Authentication. AWS SDK uses this class behind the scenes whenever you call a service in order to determine the user's identity and retrieve any authorization you might want. Let's quickly go through what this class does.

Starting with the properties, _token and _logins, they are used to store the token retrieved from our custom authentication system, and the logins store strings which represent the different systems by which the user has been authenticated (Developer, Facebook, etc.). There are also getters and setters for these properties just below their declaration.

Next, there is a method called getIdentityId which is asynchronous (you can tell because it returns an AWSTask object, which is based on BFTask, but is already included in the SDK). This method checks to see if there is already an identity, and if not, call the refresh method.

The refresh method is also asynchronous, and is what we will use to retrieve authorization for our user in order to update the _token and _logins properties so the SDK can assign the user to an authenticated role which can be granted additional levels of access to your AWS services. Let's flesh out this method a bit.

refresh

** Note: You cannot directly invoke a Lambda function using the SDK, you must use API Gateway, or something similar to invoke your function indirectly or you'll have a problem switching from unauthenticated to an authenticated role. **

In order to make sure the SDK grants an authorized identity to the user, we must store a value in both the _token and _logins properties as well as another property which we don't declare because it is extended, identityId. We won't go into the developer authentication system in this post, but suffice it to say that it is a Lambda script which takes a username/email and a password and returns a token and an identity id. Another thing to note is that this Lambda function is not invoked directly using the SDK, but through an http call to API Gateway because of a problem with the SDK not changing roles after first calling a service with an unauthenticated role. To invoke the Lambda script, import AFNetworking and add the following to your refresh method:

override func refresh() -> AWSTask! {
    let task = AWSTaskCompletionSource()
    let request = AFHTTPRequestOperationManager()
    request.requestSerializer.setValue(email, forHTTPHeaderField: "email")
    request.requestSerializer.setValue(password, forHTTPHeaderField: "password")
    request.GET(Constants.loginUrl.value, parameters: nil, success: { (request: AFHTTPRequestOperation!, response: AnyObject!) -> Void in
        // The following 3 lines are required as referenced here: http://stackoverflow.com/a/26741208/535363
        self.logins = [self.developerProvider: self.email]
        
        // Get the properties from my server response
        let identityId = response.objectForKey("identityId")as! String
        let token = response.objectForKey("token")as! String
        
        // Set the identityId and token
        self.identityId = identityId
        self._token = token
        
        task.setResult(self.identityId)
        }, failure: { (request: AFHTTPRequestOperation?, error: NSError!) -> Void in
            task.setError(error)
    })
    return task.task
}

First, we create an AWSTaskCompletionSource which will return our AWSTask object from the method. Next, we create a AFHTTPRequestOperationManager to run the Lambda script through API Gateway. The email and password for authentication are set as headers in the request and the API Gateway is set up to read these and input them as parameters to the Lambda function. The function is invoked asyncronously, and will print an error if it is invoked inproperly. If it invokes properly, it will check for a response and retrieve the identity id and token and store them to our class properties. That's it!

Your class should look like this:

//
//  myAppIdentityProvider.swift
//

import Foundation
import AWSCore
import AWSCognito
import AFNetworking

class myAppIdentityProvider: AWSAbstractCognitoIdentityProvider {
    var _token: String!
    var _logins: [ NSObject : AnyObject ]!
    
    var email: String!
    var password: String!
    
    override var token: String {
        get {
            return _token
        }
    }
    
    override var logins: [ NSObject : AnyObject ]! {
        get {
            return _logins
        }
        set {
            _logins = newValue!
        }
    }
    
    override func getIdentityId() -> AWSTask! {
        
        if self.identityId != nil {
            return AWSTask(result: self.identityId)
        }else{
            return AWSTask(result: nil).continueWithBlock({ (task) -> AnyObject! in
                if self.identityId == nil {
                    return self.refresh()
                }
                return AWSTask(result: self.identityId)
            })
        }
    }
    
    override func refresh() -> AWSTask! {
        let task = AWSTaskCompletionSource()
        let request = AFHTTPRequestOperationManager()
        request.requestSerializer.setValue(email, forHTTPHeaderField: "email")
        request.requestSerializer.setValue(password, forHTTPHeaderField: "password")
        request.GET(Constants.loginUrl.value, parameters: nil, success: { (request: AFHTTPRequestOperation!, response: AnyObject!) -> Void in
            // The following 3 lines are required as referenced here: http://stackoverflow.com/a/26741208/535363
            self.logins = [self.developerProvider: self.email]
            
            // Get the properties from my server response
            let identityId = response.objectForKey("identityId")as! String
            let token = response.objectForKey("token")as! String
            
            // Set the identityId and token
            self.identityId = identityId
            self._token = token
            
            task.setResult(self.identityId)
            }, failure: { (request: AFHTTPRequestOperation?, error: NSError!) -> Void in
                task.setError(error)
        })
        return task.task
    }
}