User Authentication on iOS with Ruby on Rails and Swift
Learn how to secure your iOS app by adding user accounts using Swift and a custom Ruby on Rails backend. By Subhransu Behera.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
User Authentication on iOS with Ruby on Rails and Swift
40 mins
- Getting Started
- Setting Up Your Rails Application
- Deploying Rails Application on Heroku
- Configuring Amazon S3 (Simple Storage Service)
- Setting up Heroku Environment Variables
- About the APIs
- Getting Started with the Swift Application
- Sign up and Sign in
- Display Existing Selfies
- Uploading a Selfie to the Server
- Deleting a Selfie
- Handling Signing Out
- Where To Go From Here?
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:
- Sign Up
- Sign In
- Get Token
- Upload Photo
- Get Photos
- Delete Photo
- Reset Password
- 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:
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 {
if let loginController = 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.
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.
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:
- Let a user create a new account
- Allow a user to sign in to your application
- Display selfies that the user has already uploaded
- Upload new selfies
- 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 count(self.signupNameTextField.text) > 0 && count(self.signupEmailTextField.text) > 0
&& count(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 as String)
return
}
self.displaSigninView()
self.displayAlertMessage("Success", alertDescription: "Account has been created")
})
}
Here’s the section-by-section breakdown of the implementation.
- 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. - Encrypt the users password using AES encryption. The block uses the API password as the encryption key.
- 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.
- 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:String = "") -> 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 and Content-Type
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 = "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
if let userToken = KeychainAccess.passwordForAccount("Auth_Token", service: "KeyChainService") as String? {
// Set Authorization header
request.addValue("Token token=\(userToken)", forHTTPHeaderField: "Authorization")
}
}
return request
}
- Creates the instance of NSMutableURLRequest and sets the HTTP method.
- 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.
- 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.
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
if let httpResponse = response as? NSHTTPURLResponse {
if httpResponse.statusCode == 200 {
completion(data, nil)
} else {
var jsonerror:NSError?
if let errorDict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error:&jsonerror) as? NSDictionary {
let responseError : NSError = NSError(domain: "HTTPHelperError", code: httpResponse.statusCode, userInfo: errorDict as? [NSObject : AnyObject])
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.
Note: Verify your API_AUTH_NAME
, API_AUTH_PASSWORD
and BASE_URL
are set correctly in HTTPHelper.swift if the request fails.
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 count(self.signinEmailTextField.text) > 0 &&
count(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 as String)
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! String
var myObj = dictObj as! String
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.
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.