Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

User Accounts on iOS with Ruby on Rails and Swift

$
0
0
Add Secure User Accounts

Add Secure User Accounts

Authentication is often the foundation in most iOS applications. Whether you’re next app is the next Instagram or Facebook, the user needs to be able to open the door to the wonderful universe you create with Sign up and Sign In functions.

In a mobile environment, you’d typically implement this essential function by exposing your user services’ APIs to the app and allowing it to make changes on your server. While this sounds relatively straight forward, there is an elephant in the room — you need to do all you can to protect your user’s security and privacy.

In this tutorial you’ll:

  • Learn how to deploy your own Ruby on Rails application server on Heroku
  • Build a Swift application that interacts with the backend server to authenticate your user
  • Learn the concepts of secure API design and storage
  • Capture the moment with a selfie

What’s all this about a selfie? Well, you’ll need something practical to work with, and all the cool kids take selfies, so you’ll build an app that lets the user sign-in to your service and upload their selfie. From ducklips to crazy eyes, the user will be able to safely and securely upload and manage a gallery of selfies, all without concern over whether their image will get out to the masses and become the next meme victim on Social.

Here’s a video demoing the app that you’ll build:


Getting Started

First order of business is to set up your Heroku account and deploy the Rails backend for the app to use.

Setting Up Your Rails Application

Start by cloning the railsauth git repository. This is a simple Ruby on Rails app that already includes all the functionality your app needs. You’ll deploy this directly on Heroku, so there’s no need for you to install Ruby on Rails locally.

Open the terminal, which you can find in Applications\Utilities\Terminal, and type:

git clone https://github.com/subhransu/railsauth

If you don’t already have git installed on your Mac then you can follow this guide.

Heroku Account Creation
Next, create a Heroku account so you can deploy Rails.

If you have an existing Heroku account and have already installed the Heroku Toolbelt for Mac then you can skip directly to the next section.

Go to heroku.com and click Sign up for free

Heroku Sign up

Enter your email address and click Sign Up

Heroku Signup

Check your inbox for an email from Heroku and click on the verification link in-order to verify your email.

Heroku Confirmation

Enter a password of your choice and click Save to complete the registration process.

Heroku Signup

Next, download the Heroku Toolbelt for Mac OS X

Heroku Toolbelt

Locate the heroku-toolbelt.pkg file you just downloaded and double-click it to launch the installer.

Toolbelt Installation

When installation has finished you can quit the installer.

Deploying Rails Application on Heroku

Open the Terminal and type:

heroku login

Enter your Heroku email and password and press Enter. If you’re prompted to create an SSH key, then type Y and press Enter

Your Heroku account does not have a public ssh key uploaded.
Could not find an existing public key at ~/.ssh/id_rsa.pub
Would you like to generate one? [Yn] Y

You’ll see an “Authentication Successful” message on the Terminal. That’s your cue that the basic setup is done and you’re ready to create your first Heroku application.

Type the following into the Terminal:

heroku create

Take a note of both your Heroku application and git repository urls, which will look like this:

Creating XXXXX-XXX-1234... done, stack is cedar
http://XXXXX-XXX-1234.herokuapp.com/ | git@heroku.com:XXXXX-XXX-1234.git

Now it’s back to the Rails application you cloned earlier — you’ve not forgotten about it have you? :]

In the Terminal, change the directory to the railsauth application directory:

cd ~/location/where/you/cloned/railsauth/directory

Next, add your Heroku repository as a remote branch by using the git repo url you noted when you created the application. Enter the following, but replace the placeholder text with your url:

git remote add heroku git@heroku.com:XXXXX-XXX-1234.git

To deploy the railsauth application on Heroku, type:

git push heroku master

If it prompts you to approve the connection to Heroku, simply type yes.

The authenticity of host 'heroku.com (50.19.85.132)' can't be established.
RSA key fingerprint is 8b:48:5e:67:0e:c9:16:47:32:f2:87:0c:1f:c8:60:ad.
Are you sure you want to continue connecting (yes/no)? yes

Congratulations! You’ve successfully deployed your Rails app on Heroku. You can verify this by typing:

heroku open

This will open the application in your browser and you’ll see the friendly message, “Glad to find you here!

Heroku Deployment

Now enter the following into the Terminal:

heroku run rake db:migrate

This runs a Rails migration which creates the necessary database tables on Heroku.

That jig you do

There are two more steps you need to complete before you can start working on the Swift application.

Configuring Amazon S3 (Simple Storage Service)

You’ll store selfies on Amazon S3 (Simple Storage Service). This is a popular service for developers who need to cheaply store and retrieve files. Web developers often use S3 to store their media assets.

Go to the Amazon Web Service (AWS) Portal and click Create a Free Account

AWS Signup

Follow the on-screen instructions to create a free account, and select I am a new user.

Note: You may need to provide your credit card details. Amazon won’t charge you for 12 months, so long as you don’t exceed their Free Tier usage.

The Free Tier is more than sufficient for this tutorial and other basic projects. You can cancel the subscription at any time if you no longer wish to use S3 or any of the other services provided by Amazon.

Next, go to Amazon Web Service (AWS) Portal and click on Sign In.

Click S3 to go to S3 management console:

AWS S3

Click on Create Bucket to create an S3 bucket.

S3 Bucket Creation

S3 buckets are collections of objects, and you can store any number of objects in your bucket, kind of like a digital bag of holding. It acts much like a folder on your Mac, but it’s more fun to think of it with a mystical twist.

Enter the name of the bucket as yourname-railsauth-assets and choose your region from the drop down. Make sure you take a note of this name. Next, click Create to make an S3 bucket.

Note: Prefix the bucket with your name or username. Buckets on S3 have to be unique across all of S3. You can read more about bucket naming here.

S3 Bucket Creation

You’ll need to use your AWS security credentials in the Rails application to store selfies on S3.

In the Amazon Web Services Portal click on your name in the top-right corner and choose Security Credentials from the drop down menu.

AWS Security Credentials

Expand the Access Keys (Access Key ID and Secret Access Key) section by clicking the +.

AWS Security Credentials

Click Create New Access Key.

AWS Security Credentials

Click Show Access Key to obtain your Access Key ID and your Secret Key.

Note: Download the key file so you have a backup of the keys. You won’t be able to re-obtain your secret key if you lose it. Hence the need to download and stash it away somewhere safe.

Setting up Heroku Environment Variables

You should always use environment variables to keep your keys secure. Never hard code such things in your application code.

Open the Terminal and set the following variables one by one. Remember to replace the dummy keys with your actual AWS credentials and S3 bucket name:

heroku config:set AWS_ACCESS_KEY_ID=<Your_AWS_Access_Key_Id>
heroku config:set AWS_SECRET_ACCESS_KEY=<Your_AWS_Secret_Key>
heroku config:set S3_BUCKET_NAME="yourname-railsauth-assets"

With that complete, you now need a secret API username and password to protect your API from unwanted access.

Use this random password generator to create a 64-bit password.

Then, create an API username and password by entering the following in the Terminal:

heroku config:set API_AUTH_NAME=<USERNAME> API_AUTH_PASSWORD=<PASSWORD>

Here’s an example of what this should look like:

heroku config:set API_AUTH_NAME=MYAPIADMINNAME API_AUTH_PASSWORD=20hWfR1QM75fFJ2mjQNHkslpEF9bXN0SiBzEqDB47QIxBmw9sTR9q0B7kiS16m7e

About the APIs

Your server is now set up and ready for use, and you have eight API endpoints at the disposal of your Swift app:

  1. Sign Up
  2. Sign In
  3. Get Token
  4. Upload Photo
  5. Get Photos
  6. Delete Photo
  7. Reset Password
  8. Clear Token

The first three endpoints are locked down using HTTP Basic Authentication. The remaining endpoints expect a username and secret token. Without these details, nobody — including you — can access the APIs directly. Also, the temporary token has a time limit.

You encrypt the user’s password with the AES (Advanced Encryption Standard) encryption algorithm.

Refer to the documentation of the API to find out more, including the request and response format.

Getting Started with the Swift Application

You’re all done with the backend setup, so now you get to play around with Swift :]

Start by downloading the starter project for this tutorial.

Open Main.storyboard and you’ll see the user interface; this have been provided to allow you to focus on Swift:

Storyboard

SelfieCollectionViewController is the main view controller and contains a collection view where you’ll display the selfies.

Another key point to know is that each collection view cell contains an image view to display the selfie, and a label to display the caption.

Take a look at viewDidAppear(_:):

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(true)
 
  let defaults = NSUserDefaults.standardUserDefaults()
 
  if defaults.objectForKey("userLoggedIn") == nil {
    let loginController: ViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as ViewController
    self.navigationController?.presentViewController(loginController, animated: true, completion: nil)
  }
}

Here you check if the user is already logged in, and if they aren’t you prompt the user to Sign in. Specifically, it checks for a userLoggedIn flag in NSUserDefaults.

Note: Although data stored in NSUserDefaults persists across restarts, you should never use it to store any sensitive information like a users email or password. It resides in the apps Library folder which is accessible to anyone with physical access to the device. However, it’s perfectly fine to use it for storing non-sensitive data like preference settings or temporary flags, like you’re doing above.

Build and run. You should see the Sign in view.

sign_in

Now you’re ready to take the world by storm and build an app that lets your users capture the essence of their very souls with a single tap, by snapping a selfie :]

You’ll need to complete five primary tasks:

  1. Let a user create a new account
  2. Allow a user to sign in to your application
  3. Display selfies that the user has already uploaded
  4. Upload new selfies
  5. Delete old, dated and undesirable selfies. After-all, everyone likes a second chance to make the perfect funny face!

Before you can proceed, you need to spend a little more time working with the server and API details — specifically, you need to update them in your application.

Open HTTPHelper.swift from the Selfie group and update API_AUTH_NAME, API_AUTH_PASSWORD, and BASE_URL with your heroku server details.

static let API_AUTH_NAME = "<YOUR_HEROKU_API_ADMIN_NAME>"
static let API_AUTH_PASSWORD = "<YOUR_HEROKU_API_PASSWORD>"
static let BASE_URL = "https://XXXXX-XXX-1234.herokuapp.com/api"

Make sure that BASE_URL still has /api at the end.

With the server details updated, you’re now ready to implement the authentication flow!

Sign up and Sign in

Open ViewController.swift. Replace the content of signupBtnTapped(sender:) with the following:

@IBAction func signupBtnTapped(sender: AnyObject) {
  // Code to hide the keyboards for text fields
  if self.signupNameTextField.isFirstResponder() {
    self.signupNameTextField.resignFirstResponder()
  }
 
  if self.signupEmailTextField.isFirstResponder() {
    self.signupEmailTextField.resignFirstResponder()
  }
 
  if self.signupPasswordTextField.isFirstResponder() {
    self.signupPasswordTextField.resignFirstResponder()
  }
 
  // start activity indicator
  self.activityIndicatorView.hidden = false
 
  // validate presence of all required parameters
  if countElements(self.signupNameTextField.text) > 0 && countElements(self.signupEmailTextField.text) > 0 
      && countElements(self.signupPasswordTextField.text) > 0 {
    makeSignUpRequest(self.signupNameTextField.text, userEmail: self.signupEmailTextField.text, 
        userPassword: self.signupPasswordTextField.text)
  } else {
    self.displayAlertMessage("Parameters Required", alertDescription: 
        "Some of the required parameters are missing")
  }
}

The code here dismissed the keyboard for the text fields, and then validates whether the required parameters are present. It then calls makeSignUpRequest(userName:userEmail:userPassword:) to make a signup request.

Next, you’ll implement makeSignUpRequest(userName:userEmail:userPassword:). Add the following:

func makeSignUpRequest(userName:String, userEmail:String, userPassword:String) {
  // 1. Create HTTP request and set request header
  let httpRequest = httpHelper.buildRequest("signup", method: "POST", 
      authType: HTTPRequestAuthType.HTTPBasicAuth)
 
  // 2. Password is encrypted with the API key
  let encrypted_password = AESCrypt.encrypt(userPassword, password: HTTPHelper.API_AUTH_PASSWORD)
 
  // 3. Send the request Body
  httpRequest.HTTPBody = "{\"full_name\":\"\(userName)\",\"email\":\"\(userEmail)\",\"password\":\"\
      (encrypted_password)\"}".dataUsingEncoding(NSUTF8StringEncoding)
 
  // 4. Send the request
  httpHelper.sendRequest(httpRequest, completion: {(data:NSData!, error:NSError!) in
    if error != nil {
      let errorMessage = self.httpHelper.getErrorMessage(error)
      self.displayAlertMessage("Error", alertDescription: errorMessage)
 
      return
    }
 
    self.displaSigninView()
    self.displayAlertMessage("Success", alertDescription: "Account has been created")
  })
}

Here’s the section-by-section breakdown of the implementation.

  1. Uses the buildRequest(_:method:authType:) method to create an instance of NSMutableURLRequest and sets the necessary HTTP request parameters. buildRequest(_:method:authType:) is a helper method that’s implemented in the HTTPHelper struct.
  2. Encrypt the users password using AES encryption. The block uses the API password as the encryption key.
  3. Creates the JSON request body and sets all the appropriate parameters and values. For sign-up, the request requires the full name, email and encrypted password from the user.
  4. Use sendRequest(_:completion:) from the HTTPHelper struct to create an instance of NSURLSessionDataTask, which makes a request to create a new user on the Rails server. Also, the user will receive an alert when they create a new account, or the request fails.

The above method uses two helper methods which are implemented in HTTPHelper.swift

  • buildRequest(_:method:authType:)
  • sendRequest(_:completion:)

Let’s take a look at the implementations of these methods. Open HTTPHelper.swift.

buildRequest(_:method:authType:) creates an instance of NSMutableURLRequest and sets all necessary HTTP parameters for a request.

func buildRequest(path: String!, method: String, authType: HTTPRequestAuthType,
  requestContentType: HTTPRequestContentType = HTTPRequestContentType.HTTPJsonContent, requestBoundary:NSString = "") -> NSMutableURLRequest {
    // 1. Create the request URL from path
    let requestURL = NSURL(string: "\(HTTPHelper.BASE_URL)/\(path)")
    var request = NSMutableURLRequest(URL: requestURL!)
 
    // Set HTTP request method 
    request.HTTPMethod = method
 
    // 2. Set the correct Content-Type for the HTTP Request. This will be multipart/form-data for photo upload request and application/json for other requests in this app
    switch requestContentType {
    case .HTTPJsonContent:
      request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    case .HTTPMultipartContent:
      let contentType = NSString(format: "multipart/form-data; boundary=%@", requestBoundary)
      request.addValue(contentType, forHTTPHeaderField: "Content-Type")
    }
 
    // 3. Set the correct Authorization header.
    switch authType {
    case .HTTPBasicAuth:
      // Set BASIC authentication header
      let basicAuthString = "\(HTTPHelper.API_AUTH_NAME):\(HTTPHelper.API_AUTH_PASSWORD)"
      let utf8str = basicAuthString.dataUsingEncoding(NSUTF8StringEncoding)
      let base64EncodedString = utf8str?.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(0))
 
      request.addValue("Basic \(base64EncodedString!)", forHTTPHeaderField: "Authorization")
    case .HTTPTokenAuth:
      // Retreieve Auth_Token from Keychain
      let userToken : NSString? = KeychainAccess.passwordForAccount("Auth_Token", service: "KeyChainService")
      // Set Authorization header
      request.addValue("Token token=\(userToken!)", forHTTPHeaderField: "Authorization")
    }
 
    return request
}
  1. Creates the instance of NSMutableURLRequest and sets the HTTP method.
  2. Sets the Content-Type to application/json or multipart/form-data. The default Content-Type is application/json, which tells the server that the request body contains JSON data.
  3. Sets the Authorization header to protect your API and user data. For Sign Up, you’re setting a HTTP Basic Authentication header. This is being passed in the 3rd parameter HTTPRequestAuthType.HTTPBasicAuth where you’re creating the HTTP request.

Basic Authentication is the first line of defense against any API attack; it combines your API username and password into a string and encodes it with Base64 to provide another layer of security.

This will deny all access to your API unless the user has the correct username and password.

Note: Despite sounding like it’s secure, Basic Authentication is not the best way secure your API as there are ways to get around it, but it’s more than sufficient in the scope of this tutorial.

sendRequest(_:completion:) creates an NSURLSession task, which is then used to send the request to the server:

func sendRequest(request: NSURLRequest, completion:(NSData!, NSError!) -> Void) -> () {
  // Create a NSURLSession task
  let session = NSURLSession.sharedSession()
  let task = session.dataTaskWithRequest(request) { (data: NSData!, response: NSURLResponse!, 
      error: NSError!) in
    if error != nil {
      dispatch_async(dispatch_get_main_queue(), { () -> Void in
        completion(data, error)
      })
 
      return
    }
 
    dispatch_async(dispatch_get_main_queue(), { () -> Void in
      let httpResponse = response as NSHTTPURLResponse
 
      if httpResponse.statusCode == 200 {
        completion(data, nil)
      } else {
        var jsonerror:NSError?
        let errorDict = NSJSONSerialization.JSONObjectWithData(data, 
            options: NSJSONReadingOptions.AllowFragments, error:&jsonerror) as NSDictionary
 
        let responseError : NSError = NSError(domain: "HTTPHelperError", code: httpResponse.statusCode, 
            userInfo: errorDict)
        completion(data, responseError)
      }
    })
  }
 
  // start the task
  task.resume()
}

Build and run. Once the application launches, tap the Don’t have an account yet? button to create a new account.

Note: Verify your API_AUTH_NAME, API_AUTH_PASSWORD and BASE_URL are set correctly in HTTPHelper.swift if the request fails.

Selfie Signup

Next, you’ll implement the Sign in functionality, so replace the existing implementation of signinBtnTapped(sender:) with the following:

@IBAction func signinBtnTapped(sender: AnyObject) {
  // resign the keyboard for text fields
  if self.signinEmailTextField.isFirstResponder() {
    self.signinEmailTextField.resignFirstResponder()
  }
 
  if self.signinPasswordTextField.isFirstResponder() {
    self.signinPasswordTextField.resignFirstResponder()
  }
 
  // display activity indicator
  self.activityIndicatorView.hidden = false
 
  // validate presense of required parameters
  if countElements(self.signinEmailTextField.text) > 0 && 
      countElements(self.signinPasswordTextField.text) > 0 {
    makeSignInRequest(self.signinEmailTextField.text, userPassword: self.signinPasswordTextField.text)
  } else {
    self.displayAlertMessage("Parameters Required", 
        alertDescription: "Some of the required parameters are missing")
  }
}

When all the required parameters are present, the above code calls makeSignInRequest(userEmail:userPassword) to make a sign in request. Next, implement makeSignInRequest(userEmail:userPassword). Add the following:

func makeSignInRequest(userEmail:String, userPassword:String) {
  // Create HTTP request and set request Body
  let httpRequest = httpHelper.buildRequest("signin", method: "POST", 
      authType: HTTPRequestAuthType.HTTPBasicAuth)
  let encrypted_password = AESCrypt.encrypt(userPassword, password: HTTPHelper.API_AUTH_PASSWORD)
 
  httpRequest.HTTPBody = "{\"email\":\"\(self.signinEmailTextField.text)\",\"password\":\"
      \(encrypted_password)\"}".dataUsingEncoding(NSUTF8StringEncoding);
 
  httpHelper.sendRequest(httpRequest, completion: {(data:NSData!, error:NSError!) in
    // Display error
    if error != nil {
      let errorMessage = self.httpHelper.getErrorMessage(error)
      self.displayAlertMessage("Error", alertDescription: errorMessage)
 
      return
    }
 
    // hide activity indicator and update userLoggedInFlag
    self.activityIndicatorView.hidden = true
    self.updateUserLoggedInFlag()
 
    var jsonerror:NSError?
    let responseDict = NSJSONSerialization.JSONObjectWithData(data, 
        options: NSJSONReadingOptions.AllowFragments, error:&jsonerror) as NSDictionary
    var stopBool : Bool
 
    // save API AuthToken and ExpiryDate in Keychain
    self.saveApiTokenInKeychain(responseDict)
  })
}

The code above is almost identical to the implementation of the makeSignUpRequest(userName:userEmail:userPassword:) , except this time when the user successfully signs in to the application, you’ll receive an api_authtoken and authtoken_expiry date.

For all subsequent requests, you need to use the api_authtoken instead of HTTP Basic Authentication.

Implement the following methods that update the userLoggedIn flag in NSUserDefaults and save the API token respectively:

func updateUserLoggedInFlag() {
  // Update the NSUserDefaults flag
  let defaults = NSUserDefaults.standardUserDefaults()
  defaults.setObject("loggedIn", forKey: "userLoggedIn")
  defaults.synchronize()
}
 
func saveApiTokenInKeychain(tokenDict:NSDictionary) {
  // Store API AuthToken and AuthToken expiry date in KeyChain
  tokenDict.enumerateKeysAndObjectsUsingBlock({ (dictKey, dictObj, stopBool) -> Void in
    var myKey = dictKey as NSString
    var myObj = dictObj as NSString
 
    if myKey == "api_authtoken" {
      KeychainAccess.setPassword(myObj, account: "Auth_Token", service: "KeyChainService")
    }
 
    if myKey == "authtoken_expiry" {
      KeychainAccess.setPassword(myObj, account: "Auth_Token_Expiry", service: "KeyChainService")
    }
  })
 
  self.dismissViewControllerAnimated(true, completion: nil)
}

The api_authtoken is sensitive information, so you shouldn’t store it in NSUserDefaults as that would be kind of like putting out a data buffet for a would-be attacker. Hence, the code above stores it in the Keychain.

On iOS, a Keychain is an encrypted container that holds sensitive information. saveApiTokenInKeychain(tokenDict:) uses Keychain service APIs that provide ways to encrypt one or more key-value pairs.

After dismissing the current view, the application displays SelfieCollectionViewController.

Take it for a test drive! Build and run. After successfully signing in, you should see a blank view.

blank_screen

Wait, why is it blank? Shouldn’t you be able to snap a selfie or something?

There’s nothing to do just yet because you’ve not implemented the code to display anything.

Display Existing Selfies

Open SelfieCollectionViewController.swift and replace the existing implementation of viewDidAppear(_:) with the following:

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(true)
 
  // check if user is signed in
  let defaults = NSUserDefaults.standardUserDefaults()
 
  // is user is not signed in display controller to sign in or sign up
  if defaults.objectForKey("userLoggedIn") == nil {
    let loginController: ViewController = self.storyboard?.
        instantiateViewControllerWithIdentifier("ViewController") as ViewController
    self.navigationController?.presentViewController(loginController, animated: true, completion: nil)
  } else {
    // check if API token has expired
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
    let userTokenExpiryDate : NSString? = KeychainAccess.passwordForAccount("Auth_Token_Expiry", 
        service: "KeyChainService")
    let dateFromString : NSDate? = dateFormatter.dateFromString(userTokenExpiryDate!)
    let now = NSDate()
 
    let comparision = now.compare(dateFromString!)
 
    // check if should fetch new data
    if shouldFetchNewData {
      shouldFetchNewData = false
      self.setNavigationItems()
      loadSelfieData()
    }
 
    // logout and ask user to sign in again if token is expired
    if comparision != NSComparisonResult.OrderedAscending {
      self.logoutBtnTapped()
    }
  }
}

This checks if the user is signed in, and if not, or if the API token expired, then it’ll prompt the user to sign in. Otherwise, it’ll call loadSelfieData() to fetch any existing selfies.

Next, replace the contents of loadSelfieData() with the following:

func loadSelfieData () {
  // Create HTTP request and set request Body
  let httpRequest = httpHelper.buildRequest("get_photos", method: "GET", 
      authType: HTTPRequestAuthType.HTTPTokenAuth)
 
  // Send HTTP request to load existing selfie
  httpHelper.sendRequest(httpRequest, completion: {(data:NSData!, error:NSError!) in
    // Display error
    if error != nil {
      let errorMessage = self.httpHelper.getErrorMessage(error)
      let errorAlert = UIAlertView(title:"Error", message:errorMessage, delegate:nil, 
          cancelButtonTitle:"OK")
      errorAlert.show()
 
      return
    }
 
    var eror: NSError?
    let jsonDataArray = NSJSONSerialization.JSONObjectWithData(data, 
        options: NSJSONReadingOptions(0), error: &eror) as NSArray!
 
    // load the collection view with existing selfies
    if jsonDataArray != nil {
      for imageDataDict in jsonDataArray {
        var selfieImgObj = SelfieImage()
 
        selfieImgObj.imageTitle = imageDataDict.valueForKey("title") as NSString
        selfieImgObj.imageId = imageDataDict.valueForKey("random_id") as NSString
        selfieImgObj.imageThumbnailURL = imageDataDict.valueForKey("image_url") as NSString
 
        self.dataArray.append(selfieImgObj)
      }
 
      self.collectionView?.reloadData()
    }
  })
}

This code makes a GET request to fetch the user’s existing selfies.

Once the task is complete, it iterates through the array of JSON objects it’s received and updates dataArray, which will be used by the collection view cell to display images and captions.

Here instead of using HTTP Basic Authentication the buildRequest(_:method:authType:requestContentType:requestBoundary:) method uses HTTP Token Authentication. This is indicated by passing the correct authType parameter.

httpHelper.buildRequest("get_photos", method: "GET", authType: HTTPRequestAuthType.HTTPTokenAuth)

The buildRequest(_:method:authType:requestContentType:requestBoundary:) method retrieves the API auth token from the Keychain and passes it in the Authorization header.

// This is implemented in buildRequest method in HTTPHelper struct 
 
case .HTTPTokenAuth:
// Retreieve Auth_Token from Keychain
let userToken : NSString? = KeychainAccess.passwordForAccount("Auth_Token", service: "KeyChainService")
// Set Authorization header
request.addValue("Token token=\(userToken!)", forHTTPHeaderField: "Authorization")

Build and run. You should see the following screen if you’ve signed in at least once before, otherwise the application will prompt you to sign in.

Logout

Since you’ve signed in at least once, the application remembers the API auth_token for the user and sets the necessary flag in NSUserDefaults. Hence, it displays SelfieViewController without prompting you to login again.

Next, replace the content of collectionView(_:cellForItemAtIndexPath:) inside SelfieCollectionViewController.swift with the following code:

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, 
      forIndexPath: indexPath) as SelfieCollectionViewCell
 
  // Configure the cell
  var rowIndex = self.dataArray.count - (indexPath.row + 1)
  var selfieRowObj = self.dataArray[rowIndex] as SelfieImage
 
  cell.backgroundColor = UIColor.blackColor()
  cell.selfieTitle.text = selfieRowObj.imageTitle
 
  var imgURL: NSURL = NSURL(string: selfieRowObj.imageThumbnailURL)!
 
  // Download an NSData representation of the image at the URL
  let request: NSURLRequest = NSURLRequest(URL: imgURL)
  NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), 
      completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
    if error == nil {
      var image = UIImage(data: data)
 
      dispatch_async(dispatch_get_main_queue(), {
        cell.selfieImgView.image = image
      })
    } else {
      println("Error: \(error.localizedDescription)")
    }
  })
 
  return cell
}

The rowIndex displays the most recent selfie on top and puts the older ones below. This also sets the title and image of any individual collection view cell, then it downloads a remote image asynchronously without blocking the main thread.

You’ve just implemented the code to display existing selfies for the user, but you still need a selfie to work with here!

Uploading a Selfie to the Server

When the user taps the camera icon on the navigation bar, it calls cameraBtnTapped(_:), which in turn calls displayCameraControl() to bring up the image picker controller.

In SelfieCollectionViewController.swift locate imagePickerController(_:didFinishPickingMediaWithInfo:) and replace it with the following code:

func imagePickerController(picker: UIImagePickerController!, didFinishPickingMediaWithInfo info: NSDictionary!) {
  // dismiss the image picker controller window
  self.dismissViewControllerAnimated(true, completion: nil)
 
  var image:UIImage!
 
  // fetch the selected image
  if picker.allowsEditing {
    image = info.objectForKey(UIImagePickerControllerEditedImage) as UIImage
  } else {
    image = info.objectForKey(UIImagePickerControllerOriginalImage) as UIImage
  }
 
  presentComposeViewControllerWithImage(image)
}

In the above code snippet, you’re extracting the selected image and calling presentComposeViewControllerWithImage(_:) with the selected image. imagePickerController(_:didFinishPickingMediaWithInfo:) gets called when the user selects an image.

Implement presentComposeViewControllerWithImage(_:) now by adding the following:

func presentComposeViewControllerWithImage(image:UIImage!) {
  // instantiate compose view controller to capture a caption
  let composeVC: ComposeViewController = self.storyboard?.
      instantiateViewControllerWithIdentifier("ComposeViewController") as ComposeViewController
  composeVC.composeDelegate = self
  composeVC.thumbImg = image
 
  // set the navigation controller of compose view controlle
  let composeNavVC = UINavigationController(rootViewController: composeVC)
 
  // present compose view controller
  self.navigationController?.presentViewController(composeNavVC, animated: true, completion: nil)
}

presentComposeViewControllerWithImage(_:) instantiates and presents the compose view controller where you’ll prompt the user to add a caption to the image.

In SelfieCollectionViewController.swift you’ll notice few extensions. These extensions conform to specific protocols and keep the related methods grouped together with the protocol. For example, camera extension groups the methods that are responsible to display the camera control and manages the image picker delegate methods.

Open ComposeViewController.swift and replace viewDidLoad() with the following:

override func viewDidLoad() {
  super.viewDidLoad()
 
  // Do any additional setup after loading the view.
  self.titleTextView.becomeFirstResponder()
  self.thumbImgView.image = thumbImg
  self.automaticallyAdjustsScrollViewInsets = false
  self.activityIndicatorView.layer.cornerRadius = 10
 
  setNavigationItems()
}

This uses the same image the user selects as a thumbnail image and asks user to enter a caption.

Next, replace the contents of postBtnTapped() with the following code:

func postBtnTapped() {
  // resign the keyboard for text view
  self.titleTextView.resignFirstResponder()
  self.activityIndicatorView.hidden = false
 
  // Create Multipart Upload request
  var imgData : NSData = UIImagePNGRepresentation(thumbImg)
  let httpRequest = httpHelper.uploadRequest("upload_photo", data: imgData, title: self.titleTextView.text)
 
  httpHelper.sendRequest(httpRequest, completion: {(data:NSData!, error:NSError!) in
    // Display error
    if error != nil {
      let errorMessage = self.httpHelper.getErrorMessage(error)
      self.displayAlertMessage("Error", alertDescription: errorMessage)
 
      return
    }
 
    var eror: NSError?
    let jsonDataDict = NSJSONSerialization.JSONObjectWithData(data, 
        options: NSJSONReadingOptions(0), error: &eror) as NSDictionary
 
    var selfieImgObjNew = SelfieImage()
 
    selfieImgObjNew.imageTitle = jsonDataDict.valueForKey("title") as NSString
    selfieImgObjNew.imageId = jsonDataDict.valueForKey("random_id") as NSString
    selfieImgObjNew.imageThumbnailURL = jsonDataDict.valueForKey("image_url") as NSString
 
    self.composeDelegate.reloadCollectionViewWithSelfie(selfieImgObjNew)
    self.activityIndicatorView.hidden = true
    self.dismissViewControllerAnimated(true, completion: nil)
  })
}

The above code uses uploadRequest(_:data:title:) method instead of buildRequest(_:method:authType:requestContentType:requestBoundary:) to create the request.

If you take a look at uploadRequest(path:data:title:) method in HTTPHelper.swift, you’ll notice the implementation is a little different from buildRequest(_:method:authType:requestContentType:requestBoundary:). So take a moment to understand it before you move ahead.

func uploadRequest(path: String, data: NSData, title: NSString) -> NSMutableURLRequest {
  let boundary : NSString = "---------------------------14737809831466499882746641449"
  var request = buildRequest(path, method: "POST", authType: HTTPRequestAuthType.HTTPTokenAuth,
      requestContentType:HTTPRequestContentType.HTTPMultipartContent, 
      requestBoundary:boundary) as NSMutableURLRequest
 
  let bodyParams : NSMutableData = NSMutableData()
 
  // build and format HTTP body with data
  // prepare for multipart form uplaod
 
  let boundaryString = NSString(format: "--%@\r\n",boundary)
  let boundaryData = boundaryString.dataUsingEncoding(NSUTF8StringEncoding) as NSData!
  bodyParams.appendData(boundaryData)
 
  // set the parameter name
  let imageMeteData = "Content-Disposition: attachment; name=\"image\"; 
      filename=\"photo\"\r\n".dataUsingEncoding(NSUTF8StringEncoding, 
      allowLossyConversion: false)
  bodyParams.appendData(imageMeteData!)
 
  // set the content type
  let fileContentType = "Content-Type: application/octet-stream\r\n\r\n".
      dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(fileContentType!)
 
  // add the actual image data
  bodyParams.appendData(data)
 
  let imageDataEnding = "\r\n".dataUsingEncoding(NSUTF8StringEncoding, 
      allowLossyConversion: false)
  bodyParams.appendData(imageDataEnding!)
 
  let boundaryString2 = NSString(format: "--%@\r\n",boundary)
  let boundaryData2 = boundaryString.dataUsingEncoding(NSUTF8StringEncoding) as NSData!
 
  bodyParams.appendData(boundaryData2)
 
  // pass the caption of the image
  let formData = "Content-Disposition: form-data; name=\"title\"\r\n\r\n".
      dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(formData!)
 
  let formData2 = title.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(formData2!)
 
  let closingFormData = "\r\n".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  bodyParams.appendData(closingFormData!)
 
  let closingData = NSString(format: "--%@--\r\n",boundary)
  let boundaryDataEnd = closingData.dataUsingEncoding(NSUTF8StringEncoding) as NSData!
 
  bodyParams.appendData(boundaryDataEnd)
 
  request.HTTPBody = bodyParams
  return request
}

The initial part should be familiar — the part where it uses buildRequest(_:method:authType:requestContentType:requestBoundary:) to create an instance of NSMutableURLRequest and sets an Authorization header. However, there are 2 additional parameters it passes to buildRequest(_:method:authType:requestContentType:requestBoundary:) method.

buildRequest(path, method: "POST", authType: HTTPRequestAuthType.HTTPTokenAuth, requestContentType:HTTPRequestContentType.HTTPMultipartContent, requestBoundary:boundary) as NSMutableURLRequest
  1. requestContentType:HTTPRequestContentType.HTTPMultipartContent
  2. requestBoundary:boundary

The Content-Type used here is different from the Content-Type that you passed in other requests. Instead of application/json you passed multipart/form-data which tells the server the request body is a series of parts. And each part is separated by a boundary.

That’s why in next few lines of code you’ll notice the boundary multiple times.

Usually a server delimits request parameters and value combinations by &. However, for uploading images, where you send the actual binary data, there might be one or more & in the data itself, so with the help of the boundary, it knows how to split the data that’s being sent.

Open SelfieCollectionViewController.swift and update reloadCollectionViewWithSelfie(_:) with the following:

func reloadCollectionViewWithSelfie(selfieImgObject: SelfieImage) {
  self.dataArray.append(selfieImgObject)
  self.collectionView?.reloadData()
}

This updates the dataArray and reloads the collection view

Build and run. Upload your first selfie, and make it a good one! :]

Note: If you’re using Simulator, you can select an image from the photo album. But if it’s empty, open Safari on the simulator, use Google to find an appropriate image, then hold and press the mouse button to invoke the context menu, and then save the image.

Photo Upload

Looks great! Well, go ahead and upload a few more — you’ll want a nice little collection. :]

If this didn’t work for you, then go back and double check that you followed all the steps to generate your Amazon S3 credentials and that the Heroku configuration is also correct.

Deleting a Selfie

What if the user thinks their nose looks a little too shiny or they notice there’s something in their teeth? You need to provide a way for them to wipe that humiliating shot from existence.

Open SelfieCollectionViewController.swift and replace collectionView(_:didSelectItemAtIndexPath:) with the following:

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
  // fetch the Selfie Image Object
  var rowIndex = self.dataArray.count - (indexPath.row + 1)
  var selfieRowObj = self.dataArray[rowIndex] as SelfieImage
 
  pushDetailsViewControllerWithSelfieObject(selfieRowObj)
}

This calls pushDetailsViewControllerWithSelfieObject(_:) passing a SelfieImage object. Implement pushDetailsViewControllerWithSelfieObject(_:) by adding the following:

func pushDetailsViewControllerWithSelfieObject(selfieRowObj:SelfieImage!) {
  // instantiate detail view controller
  let detailVC = self.storyboard?.instantiateViewControllerWithIdentifier("DetailViewController") as 
      DetailViewController
  detailVC.editDelegate = self
  detailVC.selfieCustomObj = selfieRowObj
 
  // push detail view controller to the navigation stack
  self.navigationController?.pushViewController(detailVC, animated: true)
}

The above instantiates DetailViewController from the storyboard and sets the selfie image object. When the user taps on a selfie, DetailViewController is displayed. Here the user can check out their selfie, and delete it if they don’t like it.

Open DetailViewController.swift and replace the viewDidLoad() with the following:

override func viewDidLoad() {
  super.viewDidLoad()
 
  self.activityIndicatorView.layer.cornerRadius = 10
  self.detailTitleLbl.text = self.selfieCustomObj.imageTitle
  var imgURL = NSURL(string: self.selfieCustomObj.imageThumbnailURL)
 
  // Download an NSData representation of the image at the URL
  let request = NSURLRequest(URL: imgURL!)
 
  NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), 
      completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
    if error == nil {
      var image = UIImage(data: data)
 
      dispatch_async(dispatch_get_main_queue(), {
        self.detailThumbImgView.image = image
      })
    } else {
      println("Error: \(error.localizedDescription)")
    }
  })
}

This sets the title of the image in detail view and asynchronously downloads the selfie from Amazon S3 before displaying it. When the user deletes an image, the action triggers deleteBtnTapped(_:).

Replace deleteBtnTapped(_:) with the following:

@IBAction func deleteBtnTapped(sender: AnyObject) {
  // show activity indicator
  self.activityIndicatorView.hidden = false
 
  // Create HTTP request and set request Body
  let httpRequest = httpHelper.buildRequest("delete_photo", method: "DELETE", 
      authType: HTTPRequestAuthType.HTTPTokenAuth)
  httpRequest.HTTPBody = "{\"photo_id\":\"\(self.selfieCustomObj.imageId)\"}".
      dataUsingEncoding(NSUTF8StringEncoding);
 
  httpHelper.sendRequest(httpRequest, completion: {(data:NSData!, error:NSError!) in
    // Display error
    if error != nil {
      let errorMessage = self.httpHelper.getErrorMessage(error)
      self.displayAlertMessage("Error", alertDescription: errorMessage)
 
      return
    }
 
    self.editDelegate.deleteSelfieObjectFromList(self.selfieCustomObj)
    self.activityIndicatorView.hidden = true
    self.navigationController?.popToRootViewControllerAnimated(true)
  })
}

The above creates an HTTP DELETE request to delete the selfie from the server. Upon successful completion, the above code also calls the method deleteSelfieObjectFromList(_:), which deletes the selfie from the local list of selfies and updates the collection view.

Open SelfieCollectionViewController.swift and add the following two methods:

// This is in the base SelfieCollectionViewController class implementation
func removeObject<T:Equatable>(inout arr:Array<T>, object:T) -> T? {
  if let indexOfObject = find(arr,object) {
    return arr.removeAtIndex(indexOfObject)
  }
  return nil
}
// This is in edit selfie extension
func deleteSelfieObjectFromList(selfieImgObject: SelfieImage) {
  if contains(self.dataArray, selfieImgObject) {
    removeObject(&self.dataArray, object: selfieImgObject)
    self.collectionView?.reloadData()
  }
}

The first method deletes an object from an array, and the second method is the protocol implementation that deletes a local selfie and reloads the collection view.

Build and run. Delete your least favorite selfie — And just like that, it’s a distant memory. Good thing for Mr. Panda — ever since he became synonymous with SEO, he’s been rather particular about his selfies.

delete

Handling Signing Out

Open SelfieCollectionViewController.swift and replace the contents of logoutBtnTapped() with the following:

func logoutBtnTapped() {
  clearLoggedinFlagInUserDefaults()
  clearDataArrayAndReloadCollectionView()
  clearAPITokensFromKeyChain()
 
  // Set flag to display Sign In view
  shouldFetchNewData = true
  self.viewDidAppear(true)
}

Next, implement the following three methods that get called from logoutBtnTapped

// 1. Clears the NSUserDefaults flag
func clearLoggedinFlagInUserDefaults() {
  let defaults = NSUserDefaults.standardUserDefaults()
  defaults.removeObjectForKey("userLoggedIn")
  defaults.synchronize()
}
 
// 2. Removes the data array
func clearDataArrayAndReloadCollectionView() {
  self.dataArray.removeAll(keepCapacity: true)
  self.collectionView?.reloadData()
}
 
// 3. Clears API Auth token from Keychain
func clearAPITokensFromKeyChain () {
  // clear API Auth Token
  if let userToken = KeychainAccess.passwordForAccount("Auth_Token", service: "KeyChainService") {
    KeychainAccess.deletePasswordForAccount(userToken, account: "Auth_Token", service: "KeyChainService")
  }
 
  // clear API Auth Expiry
  if let userTokenExpiryDate = KeychainAccess.passwordForAccount("Auth_Token_Expiry", 
      service: "KeyChainService") {
    KeychainAccess.deletePasswordForAccount(userTokenExpiryDate, account: "Auth_Token_Expiry", 
        service: "KeyChainService")
  }
}

The above methods carry out the following tasks:

  1. Removes userLoggedIn flag from NSUserDefaults.
  2. Removes the data array and reloads the collection view. This is to make sure that when a new user signs in they don’t see any cached data.
  3. Clears the API auth token and credentials from the Keychain.

The logoutBtnTapped() also gets triggered when the API auth token expires, forcing the user to sign in again and obtain a new auth token.

Build and run. Tap Logout; you should be taken back to the Sign In screen.

Where To Go From Here?

Here’s the finished sample project with all the code from tutorial.

Congratulations! You’ve just successfully set up a backend server on Heroku that provides your API, configured an Amazon S3 bucket to store your users’ selfie images, and built an application that allows a user to upload a selfie to your service.

No doubt this absolutely essential app will help you capture your moods, snapping those awesome moments like this one right now. You did take a selfie of your victory face, right?

Take a look at the Authentication Cheat Sheet by OWASP; it’s also a good resource for many other security guides and materials that you can access for free.

Thank you for taking the time to work through this tutorial! If you have any questions, comments, or find a unique requirement about secure API design or mobile security, feel free to chime in below and I will be happy to help.

User Accounts on iOS with Ruby on Rails and Swift is a post from: Ray Wenderlich

The post User Accounts on iOS with Ruby on Rails and Swift appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>