Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Add Authentication to Your iOS Apps With Centralized Login, Part 2

DZone's Guide to

Add Authentication to Your iOS Apps With Centralized Login, Part 2

In this article, we go over how iOS developers can use Swift to create authentication processes in their mobile applicaitons.

· Security Zone ·
Free Resource

Discover how to provide active runtime protection for your web applications from known and unknown vulnerabilities including Remote Code Execution Attacks.

Welcome back! If you missed Part 1, you can check it out here

Add Authentication to Your iOS App

For our example, we will implement two iOS apps using Swift. Our applications will use Auth0 as the authorization server. Auth0 allows clients to log in using different identity providers and many more features (such as passwordless logins, classical username + password, etc.) One application will interact with the authorization server directly by following the usual OpenID Connect process for authentication. It will use Safari as the application handling the credentials and the login flow. The other application will use Auth0 for iOS library, to further simplify our code. This library can interact with Safari for us, making things even easier! Both applications fully implement the recommended procedure for mobile authentication, it is up to you to pick whatever you prefer for your own apps.

Setting Up Auth0

Since our example will use Auth0 as the authorization server, it is first necessary to set some things up. Fortunately, this is very easy. First, sign up for an Auth0 account if you don't have one already. Then go to the dashboard and create a new client. Clients let you create specific settings for each application that interacts with your Auth0 account. To create a client follow these steps:

  1. Open the [dashboard] and go to the clients section.
  2. Click + CREATE CLIENT in the top right corner.
  3. Set a name, select Native, and click on Create.
  4. Go to Settings and put the right value in the Allowed Callback URLs field.
  5. For our first example, the right value for the Allowed Callbacks URLs field is: auth0test1://test.
  6. For our second example, the right value for the Allowed Callbacks URLs field is:
Auth0.Centralized-Login-Test-2://speyrott.auth0.com/ios/Auth0.Centralized-Login-Test-2/callback

Don't forget to click SAVE CHANGES at the bottom of the page once you are done.

You are free to create as many clients as you want to run separate tests with separate mobile apps. We created two different clients, one for each sample app.

Take note of the values of the Domain and Client ID fields in the Settings tab of each client. These values must be configured in each mobile app's code.

Setting Up Xcode

Now let's create a simple sample project.

  1. Open Xcode and click Create a new Xcode project.
  2. Select iOS and Single View App.
  3. Complete the details on the next page and select Swift as the programming language.
  4. Pick a directory for the project and select Create.

Calling OpenID Connect/OAuth2 Endpoints Directly

Before getting our hands dirty with code, let's take a short overview of how authentication and authorization work in the context of OpenID Connect and OAuth2 for mobile apps. The following endpoints are accessed using HTTP requests. Parameters are passed as part of the URL.

The /authorize Endpoint

The common OpenID Connect procedure for logins begins by sending an HTTP GET request to a special endpoint in the authorization server: the /authorize endpoint. This endpoint takes a series of parameters that tell the authorization server what type of request is being made, along with what details the client is requesting the server to provide after a successful authorization flow. This also tells the authorization server what type of authentication and authorization is required before moving forward.

The following arguments are required to be passed to the /authorize endpoint:

  • response_type: the type of information that will be returned to the client after the authorization flow is complete. For mobile apps, this should always be code.
  • client_id: a unique ID that tells the authorization server what client (i.e. what "application") is using the endpoint. When it comes to Auth0, you can get this from the dashboard in your client's settings.
  • redirect_uri: the URL to which the authorization server will redirect the web browser after authentication is complete. Mobile apps can receive information through specially crafted URLs. We will use this to send the results of the authorization flow to our native mobile app. This parameter is optional, and it is usually set in the client configuration in the authorization server, rather than passed in this call.
  • code_challenge_method: this must always be S256. This tells the authorization server what method is used to generate the authorization code that will be returned.
  • code_challenge: a challenge that the authorization server will use to generate the authorization code that is returned after a successful authorization flow. We will see how to generate this challenge below.
  • scope: scopes tell the authorization server what kind of data you are requesting access to. For our purposes, this parameter is not very important, but for other applications it is. For example, for Facebook logins, this parameter tells Facebook what kind of access you are requesting to a user's account (email address, friends list, automatic posts, etc.). We will set this argument to openid profile id_token.

It is also a very good idea to use the state argument: the state argument contains a value that is returned by the /authorize call when doing the redirect with the results. This response is handled by a special handler in our application which can be activated by anyone that constructs a URL with the correct URL scheme. By using the state parameter, malicious calls made to this handler can be checked by comparing the value of the state parameter received with the one sent in the /authorize call. If these don't match, then the request is bogus and should be ignored. For non-native apps, like regular web apps, this prevents CSRF attacks.

Although it is possible to call /authorize without the code_challenge and code_challenge_method parameters, this is not recommended. These parameters tell the authorization server that the returned code must be generated using Proof Key for Code Exchange (PKCE), a method that improves security for mobile applications by making it harder to use returned codes intercepted by other applications running on the same device. For mobile applications, it is always a good idea to use PKCE.

All of these parameters are passed as part of the URL.

That's it! When our mobile app tells Safari to access the /authorize endpoint using these parameters, Auth0 will let the user login using any of the methods enabled for your client in the Auth0 dashboard. For example, if the client has Facebook logins and username + password logins enabled, both of these options will be displayed in the Safari window. If the user picks Facebook, Auth0 will redirect the user to Facebook to log in and authorize the access to the requested data from the user, such as his email and profile picture. After Facebook validates the user's credentials and the user authorizes the app to access the requested data, Facebook will redirect back to Auth0, and Auth0 will redirect to the URL specified in redirect_uri with the authorization code.

This authorization code can be used to make further calls to the authorization server. In particular, the code can be exchanged for an access token which will allow us to do other things, like get user profile information, and call other APIs.

Using the web browser for this part of the authorization flow is great for improving the user experience! For example, if the user had picked Facebook and they were not logged-in, they could use Safari password autocomplete to make things easier. On the other hand, if they were already logged in using Safari, there would be no need for the user to type their credentials! It is easy to find many different ways in which the user experience is improved by using the web browser as the central login hub for all mobile apps. This is what we call Centralized Login.

The /oauth/token Endpoint

Now that the user has authenticated and completed the initial authorization flow, it is time to turn the authorization code into something useful. For this, we will interact with the /oauth/token endpoint. The /oauth/token endpoint can be used to request the authorization server to issue new access tokens that the client can use. To request new tokens, the client must provide either a token (usually a refresh token) or a one-time authorization code. As you can imagine, this one-time authorization code is what we got from the /authorize endpoint above. The type of token returned will depend on the parameters that were used in the original /authorize call. In other words, we cannot request tokens of broader authorization requirements using the /oauth/token endpoint. Doing so would require authorization, which is the domain of /authorize. In contrast with the /authorize endpoint, this endpoint requires a HTTP POST request and all parameters are passed in the body encoded as a JSON object.

The /oauth/token endpoint requires the following parameters:

  • grant_type: must be authorization_code for PKCE code exchanges like the ones used for mobile apps.
  • client_id: our mobile app's client ID, the same ID passed to /authorize.
  • code: the code returned by a successful call to /authorize.
  • code_verifier: a special code generated by the mobile app before calling /authorize. This is part of the PKCE mechanism we will explain below.
  • redirect_uri: this must match the URL sent in the same parameter for the /authorize call.

That's it! If the call to the /oauth/token endpoint succeeds, the result will at least include an access token. Optionally, and according to the request made to /authorize before, the response to this call may also include a refresh token and an ID token.

Wow, so many types of tokens! Want to know what they are used for? Check the "Tokens" doc at Auth0.

Once we have our token or tokens, we have logged in!

The Code

For our example, we are going to add a button to login/logout and a simple label. Of course, common mobile apps are more complex. We will skip how to do this in this tutorial because that is out of scope, but if you are new to iOS development, do check Apple's docs.

This is how it should look for our sample:

For our login button, we will add a handler in the view controller that will open our specially crafted URL to the /authorize endpoint. All of the authorization code is in a separate class: AuthorizationServer:

@IBAction func buttonClick(_ sender: Any) {
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    if appDelegate.tokens == nil {
        appDelegate.authServer.authorize(viewController: self, useSfAuthSession: sfAuthSessionSwitch.isOn, handler: { (success) in
            if !success {
                //TODO: show error
                self.updateUI()
            }
            if self.sfAuthSessionSwitch.isOn {
                self.checkState()
            }
        })
    } else {
        appDelegate.logout()
        updateUI()
    }
}

The authorize function is simple enough:

func authorize(viewController: UIViewController, useSfAuthSession: Bool, handler: @escaping (Bool) -> Void) {
    guard let challenge = generateCodeChallenge() else {
        // TODO: handle error
        handler(false)
        return
    }

    savedState = generateRandomBytes()

    var urlComp = URLComponents(string: domain + "/authorize")!

    urlComp.queryItems = [
        URLQueryItem(name: "response_type", value: "code"),
        URLQueryItem(name: "client_id", value: clientId),
        URLQueryItem(name: "code_challenge_method", value: "S256"),
        URLQueryItem(name: "code_challenge", value: challenge),
        URLQueryItem(name: "state", value: savedState),
        URLQueryItem(name: "scope", value: "id_token profile"),
        URLQueryItem(name: "redirect_uri", value: "auth0test1://test"),
    ]

    if useSfAuthSession {
        sfAuthSession = SFAuthenticationSession(url: urlComp.url!, callbackURLScheme: "auth0test1", completionHandler: { (url, error) in
            guard error == nil else {
                return handler(false)
            }

            handler(url != nil && self.parseAuthorizeRedirectUrl(url: url!))
        })
        sfAuthSession?.start()
    } else {
        sfSafariViewController = SFSafariViewController(url: urlComp.url!)
        viewController.present(sfSafariViewController!, animated: true)
        handler(true)
    }
}

The authorize function first generates the code challenge and then constructs the URL for the request. The redirect_uri has a special scheme: auth0test. iOS allows URL schemes to be associated with specific applications. This way, when a URL containing that scheme is opened by Safari, control can be passed to that application instead. This is the mechanism used by /authorize to send back to our application the results of the operation. We must tell /authorize to what URL it needs to make the redirection. We also need to enable which URLs are valid in the Auth0 dashboard. You can find this in the client settings as shown before (Dashboard -> Clients -> [Client Name] -> Allowed Callback URLs).

Our code also allows for two mechanisms: either by opening the URL as any other URL using UIApplication.open, or by relying on the newer SFAuthenticationSession class. The SFAuthenticationSession class provides a callback that is called after the redirect is performed by the authorization server. This callback carries the URL, so it is very convenient. No other steps or handlers are required. In contrast, by using the older UIApplication.open method, we must set a special handler for our URL schemes in our main application delegate:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    // (...)

    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        return authServer.parseAuthorizeRedirectUrl(url: url)
    }

iOS calls our application when any URL using the auth0test URL scheme is opened. To claim this URL for our application, it must be configured in the Info.plist file of our bundle.

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.auth0.centralizedlogintest1</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>auth0test1</string>
    </array>
  </dict>
</array>

The parseAuthorizedRedirectUrl method is simple enough as well:

func parseAuthorizeRedirectUrl(url: URL) -> Bool {
    guard let urlComp = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
        sfSafariViewController?.dismiss(animated: true, completion: nil)
        return false
    }

    if urlComp.queryItems == nil {
        sfSafariViewController?.dismiss(animated: true, completion: nil)
        return false
    }

    for item in urlComp.queryItems! {
        if item.name == "code" {
            receivedCode = item.value
        } else if item.name == "state" {
            receivedState = item.value
        }
    }

    sfSafariViewController?.dismiss(animated: true, completion: nil)

    return receivedCode != nil && receivedState != nil
}

The code and state elements are returned as part of the redirect URL. By parsing it, we can access them. If these elements are not present, the URL is invalid and an error has occurred. Inspect the URL to get information about the error.

Once we have the code and state parameters we are ready to exchange them for an access token! Our view controller calls the getToken method from our AuthorizationServer class to do this:

func getToken(handler: @escaping (Tokens?) -> Void) {
    if receivedCode == nil || codeVerifier == nil || savedState != receivedState {
        handler(nil)
        return
    }

    let urlComp = URLComponents(string: domain + "/oauth/token")!

    let body = [
        "grant_type": "authorization_code",
        "client_id": clientId,
        "code": receivedCode,
        "code_verifier": codeVerifier,
        "redirect_uri": "auth0test1://test",
    ]

    var request = URLRequest(url: urlComp.url!)
    request.httpMethod = "POST"
    request.httpBody = try? JSONSerialization.data(withJSONObject: body)
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let task = URLSession.shared.dataTask(with: request) {
        (data, response, error) in
        if(error != nil || data == nil) {
            // TODO: handle error
            handler(nil)
            return
        }

        guard let json = try? JSONSerialization.jsonObject(with: data!) as? [String: Any],
              let accessToken = json!["access_token"] as? String
        else {
            handler(nil)
            return
        }

        handler(Tokens(accessToken: accessToken,
                        idToken: json!["id_token"] as? String,
                        refreshToken: json!["refresh_token"] as? String))
    }

    task.resume()
}

In this case, all parameters are sent in the body as a JSON object and the HTTP request uses the POST method. The code_verifier parameter is a value that was generated before calling the /authorize endpoint. By sending the code_verifier parameter, the authorization server knows that the caller can only be the one that originally called /authorize in the first place. In other words, intercepting the response from /authorize is not enough to get an access token. Do note that the URL passed in redirect_uri must match the one passed to /authorize. This parameter is not used to perform a redirect by this endpoint as all data is returned as part of the response body, it is only used to match this request with the one performed for /authorize.

According to the scopes requested in our previous /authorize call, we will get an access token that will allow us to request the user's profile. To do this, our view controller finally calls the getProfile function from AuthorizationServer:

func getProfile(accessToken: String, handler: @escaping (Profile?) -> Void) {
    let urlComp = URLComponents(string: domain + "/userinfo")!

    let urlSessionConfig = URLSessionConfiguration.default;
    urlSessionConfig.httpAdditionalHeaders = [
        AnyHashable("Authorization"): "Bearer " + accessToken
    ]
    let urlSession = URLSession(configuration: urlSessionConfig)
    let task = urlSession.dataTask(with: urlComp.url!) {
        (data, response, error) in
        if(error != nil || data == nil) {
            // TODO: handle error
            handler(nil)
            return
        }

        guard let json = try? JSONSerialization.jsonObject(with: data!) as? [String: String] else {
            handler(nil)
            // TODO: handle error
            return
        }

        let result = Profile(name: json?["name"], email: json?["email"])
        handler(result)
    }

    task.resume()
}

The getProfile function makes an HTTP GET request to the /userinfo endpoint. This endpoint requires a valid access token to operate. This is the token we got in the call to /oauth/token. The token must be passed to the /userinfo endpoint as part of the HTTP headers. The special header that is used for this purpose is the Authorization header. The token must be used in this header by prepending the word Bearer along with a single space. In other words, our authorization header will look like:

Authorization: Bearer the_access_token

Where the_access_token is the access token returned by the call to /oauth/token.

The results of the call to the /userinfo endpoint are returned in the body of the response as a JSON object. The members of the object change according to the information stored about the user. In general, you can assume name and email will be available.

And this is it! Once you have this information, you can update the UI with it or do all the stuff required by the logic of your application. There are other things that can be done by following these same steps. For example, you may request access to other APIs instead of the /userinfo API. You can even set up your custom APIs from your backend so you can get access tokens for them using these steps.

The experience when using SFAuthenticationSession is slightly different.

The PKCE Code Challenge

One of the missing pieces of the puzzle is how to generate the PKCE code challenge. As said before, the PKCE code challenge improves the security of our application by reducing the chances a successful man-in-the-middle attack can be performed. Any interceptors need both the code challenge and the code verifier to get a valid access token. The code verifier can be used to generate a code challenge but not the other way around, so the only application that can use both is the one that generated the code verifier in the first place. And, as it turns out, there are mathematical functions that provide precisely this capability: one-way functions. Of these, hash functions are used in computer science to solve problems such as this.

PKCE uses SHA-256 to generate two values: the code verifier and the code challenge. The code verifier is simply a group of 43 or more random octets. A good, cryptographically secure random number generator must be used for this task. In practice, 32 random bytes are chosen and then Base64-URL encoded into 43 characters of random data. This allows the code verifier to be used as part of HTTP requests in the URL.

The code challenge, in turn, is generated by taking the SHA-256 digest of the code verifier. This challenge is also Base64-URL encoded afterward.

In other words, to generate a valid PKCE code verifier and code challenge:

func base64UrlEncode(_ data: Data) -> String {
    var b64 = data.base64EncodedString()
    b64 = b64.replacingOccurrences(of: "=", with: "")
    b64 = b64.replacingOccurrences(of: "+", with: "-")
    b64 = b64.replacingOccurrences(of: "/", with: "_")
    return b64
}

func generateRandomBytes() -> String? {
    var keyData = Data(count: 32)
    let result = keyData.withUnsafeMutableBytes {
        (mutableBytes: UnsafeMutablePointer<UInt8>) -> Int32 in
        SecRandomCopyBytes(kSecRandomDefault, keyData.count, mutableBytes)
    }
    if result == errSecSuccess {
        return base64UrlEncode(keyData)
    } else {
        // TODO: handle error
        return nil
    }
}

class AuthorizationServer {

  //(...)

  private func generateCodeChallenge() -> String? {
      codeVerifier = generateRandomBytes()
      guard codeVerifier != nil else {
          return nil
      }
      return base64UrlEncode(sha256(string: codeVerifier!))
  }
}

In this code, codeVerifier is a variable from the AuthorizationServer class. The result of this function is first passed to the /authorize endpoint. Later on, the code verifier is passed to the /oauth/token endpoint. The authorization server can easily correlate both requests by computing the SHA-256 digest of the code verifier. If this value matched the code challenge sent to the /authorize endpoint, then these requests are correlated and coming from the same entity: the one that knows the code verifier.

Tune in next time when we'll talk about an SDK for authentication! 

Find out how Waratek’s award-winning application security platform can improve the security of your new and legacy applications and platforms with no false positives, code changes or slowing your application.

Topics:
security ,authentication ,mobile security ,ios security

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}