Episode #222

Easy Auth - Part 3

23 minutes
Published on May 27, 2016

This video is only available to subscribers. Get access to this video and 572 others.

In this episode we wrap up the Easy Auth series building the tvOS application to use our API. We'll create an authentication client and discuss how to pass around a set of values to and from the API, as well as polling for status.

Episode Links

Setting up a data structure For our Workflow

struct AuthenticationRequest {
    var clientID: String?
    var token: String?
    var code: String?
    var error: NSError?
    var status: String?
    var authTokenData: String?

    init(error: NSError) {
        self.error = error
    }

    init(clientID: String, code: String, token: String) {
        self.clientID = clientID
        self.code = code
        self.token = token
    }
}

This will hold all the relevant pieces of information that we'll use for the workflow.

Creating the AuthenticationClient

We'll use an authentication client to perform the tasks required for the workflow:

class AuthenticationClient {
    let session: NSURLSession
    let clientID: String
    let baseURL: NSURL

    init(clientID: String) {
        self.clientID = clientID
        self.session = NSURLSession.sharedSession()
        self.baseURL = NSURL(string: "http://localhost:9393")!
    }

    var activateURLString: String {
        return baseURL.URLByAppendingPathComponent("/activate").absoluteString
    }   
}

Next we'll add a method to start the process and get a code:

func requestCode(completion: (AuthenticationRequest) -> Void) {
        let url = baseURL.URLByAppendingPathComponent("/easy_auth")
        let request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = "POST"
        request.HTTPBody = "client_id=\(clientID)".dataUsingEncoding(NSUTF8StringEncoding)

        let mainCallback: (AuthenticationRequest) -> Void = { (request) in
            dispatch_async(dispatch_get_main_queue()) {
                completion(request)
            }
        }

        let task = session.dataTaskWithRequest(request) { (data, response, error) in

            if let e = error {
                mainCallback(AuthenticationRequest(error: e))
            } else {
                let http = response as! NSHTTPURLResponse
                let body = String(data: data!, encoding: NSUTF8StringEncoding)!
                if http.statusCode == 200 {

                    let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: [])
                    let code = (json["code"] as! NSNumber).integerValue
                    let codeString = "\(code)"
                    let token = json["token"] as! String
                    let authReq = AuthenticationRequest(clientID: self.clientID, code: codeString, token: token)
                    mainCallback(authReq)

                } else {
                    print("Received HTTP \(http.statusCode)")
                    print("Body: \(body)")
                    let error = NSError(domain: "authError", code: http.statusCode, userInfo: ["body": body])
                    mainCallback(AuthenticationRequest(error: error))
                }
            }
        }

        task.resume()
}

Checking Status

We'll need to continuously check status for a request and be able to react when the request has been authenticated:

func statusForRequest(authRequest: AuthenticationRequest, completion: (AuthenticationRequest) -> Void) {
        let url = baseURL.URLByAppendingPathComponent("/easy_auth/\(authRequest.token!)")

        let mainCallback: (AuthenticationRequest) -> Void = { (request) in
            dispatch_async(dispatch_get_main_queue()) {
                completion(request)
            }
        }

        let task = session.dataTaskWithURL(url) { (data, response, error) in

            var updatedRequest = authRequest

            if let e = error {
                updatedRequest.error = e
            } else {
                let http = response as! NSHTTPURLResponse
                let body = String(data: data!, encoding: NSUTF8StringEncoding)!
                if http.statusCode == 200 {

                    let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: [])
                    updatedRequest.status = json["status"] as? String
                    if updatedRequest.status == "authenticated" {
                        updatedRequest.authTokenData = json["auth_token_data"] as? String
                    }
                } else {
                    print("Received HTTP \(http.statusCode)")
                    print("Body: \(body)")
                    let error = NSError(domain: "authError", code: http.statusCode, userInfo: ["body": body])
                    updatedRequest.error = error
                }
            }

            mainCallback(updatedRequest)
        }
        task.resume()
    }

Implementing the View Controller

In our LoginViewController we'll kick off the process and show our code on the screen:

override func viewDidLoad() {
        super.viewDidLoad()

        authCodeLabel.hidden = true
        activityIndicator.startAnimating()

        let clientID = UIDevice.currentDevice().identifierForVendor!.UUIDString
        authClient = AuthenticationClient(clientID: clientID)

        directionLabel.text = directionLabel.text?.stringByReplacingOccurrencesOfString("URL", withString: authClient.activateURLString)
        requestCode()
    }

    func requestCode() {
        authClient.requestCode { (req) in
            self.activityIndicator.stopAnimating()

            if let e = req.error {
                self.displayAlert(e)
            } else {
                self.authCodeLabel.text = req.code!
                self.authCodeLabel.hidden = false

                self.pollForStatus(req, delay: 1)
            }
        }
    }

Polling For Status

Our pollForStatus method is written so that we can have it call itself over and over again:

    func pollForStatus(authReq: AuthenticationRequest, delay: NSTimeInterval) {
        let time: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)))
        dispatch_after(time, dispatch_get_main_queue()) {
            self.authClient.statusForRequest(authReq, completion: { (updatedRequest) in
                if let e = updatedRequest.error {
                    self.displayAlert(e)
                } else if updatedRequest.status == "authenticated" {
                    self.finalizeAuthentication(updatedRequest)
                } else {
                    self.pollForStatus(updatedRequest, delay: delay)
                }
            })
        }
    }

Finalizing the Workflow

    func finalizeAuthentication(authRequest: AuthenticationRequest) {
        let authToken = deriveAuthToken(authRequest)
        AuthStore.instance.authToken = authToken

        NSNotificationCenter.defaultCenter().postNotificationName("LoggedIn", object: nil)
        self.dismissViewControllerAnimated(true, completion: nil)
    }

func deriveAuthToken(authRequest: AuthenticationRequest) -> String {

        let algorithm = CCHmacAlgorithm(kCCHmacAlgSHA1)

        let digestLength = Int(CC_SHA1_DIGEST_LENGTH)
        let buffer = UnsafeMutablePointer<UInt8>.alloc(digestLength)

        let key = authRequest.clientID!
        let data = authRequest.authTokenData!

        CCHmac(algorithm, key, key.lengthOfBytesUsingEncoding(NSUTF8StringEncoding),
               data, data.lengthOfBytesUsingEncoding(NSUTF8StringEncoding),
               buffer)

        var str = ""
        for i in 0..<digestLength {
            str = str.stringByAppendingFormat("%x", buffer[i])
        }

        return str
    }

This episode uses Tvos 9.0.