Implementing OAuth with ASWebAuthenticationSession

Learn about what OAuth is and how to implement it using ASWebAuthenticationSession. By Felipe Laso-Marsetti.

5 (7) · 1 Review

Download materials
Save for later
Share

Do you need to authenticate your users against a third-party app? Perhaps your client has requested that you implement such a mechanism using the OAuth standard?

What if you’re working in an enterprise setting and your client uses Active Directory to manage users and Okta or Ping Federate to control how a third-party app interacts with protected resources?

In this tutorial, you’ll create a third-party GitHub app that you can authenticate via the OAuth standard using ASWebAuthenticationSession and display a list of repositories owned by the user. In the process, you’ll learn about:

  • OAuth
  • Session tokens
  • Ephemeral sessions

Getting Started

In a traditional client app that authenticates against a server, the server stores the username and password to authenticate a user and permit user access. This is fine when no third parties are involved.

But what happens when apps like GitHub want to add support for third-party apps?

That’s where things get tricky and a few shortcomings surface. Without OAuth, several issues can come up:

  • To avoid constantly requesting the user’s credentials, the third-party app has to store and manage them somewhere.
  • The password authentication method is more vulnerable.
  • GitHub cannot restrict, revoke or limit access to third-party apps, as they have the user’s full credentials.
  • If the third-party app is compromised, so is the user’s GitHub account.

How can GitHub (the server app) provide you (a third-party app) with access to its protected resources without giving full, unrestricted access? That’s where OAuth comes in.

Understanding OAuth

From the Internet Engineering Task Force’s (IETF) website, here’s the definition of the OAuth 2.0 standard:

“The OAuth 2.0 authorization framework enables a third-party app to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party app to obtain access on its own behalf.”

Note: The OAuth 1.0 standard is obsolete and has been replaced by OAuth 2.0, which this tutorial covers.

Your third-party app doesn’t store any user credentials or handle authenticating users directly. That’s something you’re hoping to have GitHub, with its robust infrastructure and security, do for you.

What you want your third-party app to do is allow users to log in with their own GitHub credentials to access specific resources within GitHub.

Another scenario is using GitHub to implement user authentication — without having to spend too much time on the implementation and security details — but not access any GitHub resources outside of validating the user’s identity.

This is why apps and websites often use the Sign in with buttons for a third-party service like Facebook, Apple or Google. They’re leaving it up to those parties to handle the server security and sign-in infrastructure.

Lock and key characters

Authorization Versus Authentication

OAuth helps by separating the authentication process from the authorization process.

But what exactly is the difference? Aren’t they the same? You might be surprised, after years of using the two terms interchangeably, that they are not.

  • Authentication: Who you are.
  • Authorization: Which permissions the service has given you.

In this tutorial, GitHub will give your app certain accesses and permissions: authorization. But it’s up to users to validate themselves and verify who they are against GitHub: authentication.

OAuth Roles

So far, you’ve read about what OAuth is and the difference between authentication and authorization. Behind the scenes, OAuth has a few more concepts that are important to learn about. The first is roles.

OAuth defines four roles:

  • Resource Owner: Whoever can give access to a protected resource.
  • Resource Server: The server that contains the protected resource.
  • Client: An app that tries to access protected resources with authorization and on behalf of the resource owner.
  • Authorization Server: Gives access tokens to the client upon successful authentication and authorization.

The OAuth Flow

The OAuth standard defines the following as the typical flow of a third-party app. This is what you’ll implement in this tutorial:

  1. Your app asks for an access token, a short-lived token used in requests against the resource owner. To do so, it must present the authorization server with a client ID and allow the user to authenticate with their credentials.
  2. The authorization server authenticates the client, then returns an access token and a refresh token for this user, to be used in your app only. This is assuming everything’s valid and went well.
  3. Your app, the client, makes a request to a protected resource from the resource owner. The client must then present the user’s access token or the request will fail.
  4. The resource owner validates your user’s access token. If it’s valid, then it returns the requested resource.
  5. Your app continues to make requests on behalf of the user until the access token expires. When this happens, the next request you make to the resource owner will result in an invalid token error.
  6. Your app can use the refresh token, often a longer-lived token, to request a new access token from the authorization server. The same scope and restrictions apply as before.
  7. If your refresh token is still valid, the authorization server will issue a new access token for your app. If the refresh token expired, the user must authenticate with their credentials again.
Note: There are other ways to authenticate, including a browser-less option between two clients or when you don’t have access to manual user input. These are outside the scope of this tutorial.

OAuth is heavily web-based, which means most implementations show some sort of web view to your users to let them enter their credentials. Behind the scenes, there are some redirects and callbacks that take place and make everything work. Don’t worry, this tutorial will help you handle all of that.

There’s been quite a bit of theory so far, but now you have a better understanding of OAuth and how it works.

Creating a GitHub App

You want your app to talk to GitHub. GitHub needs to know where the user is trying to access its resources from — your app — and which resources it should grant access to.

It’s time to create a GitHub app from your account. Here’s how to get started!

On your web browser, open and log in to GitHub. In the top-right corner, click your profile image. Then, click Settings:

GitHub user menu

Click Developer settings:

GitHub developer settings menu

Click GitHub Apps. Click the New GitHub App button:

GitHub Apps section

When creating a new app, there are quite a few options and settings. You’ll go through them now.

Name your app AuthHub-. Give your app a description of your choice.

Add the homepage URL for your app. This example uses https://www.raywenderlich.com/. If you’re creating your own app, it should link to your app’s homepage, where users can get more information.

Register new GitHub App page

User Authorization Settings

The next set of settings center around user authorization. The first option is the authorization callback URL. This is the page the authentication provider will redirect the users to when they successfully authenticate. The format you specify here is what your app will listen to, to take back control once a user authenticates.

Use the following for the app’s callback URL:

authhub://oauth-callback

This is just a callback you define. You can use a different value if you want, but note that it will change the setup process when working with ASWebAuthenticationSession. For now, it’s easiest to use the value above, so you can follow along.

The next option is a checkbox asking whether to let a user’s authorization tokens expire. Enable this checkbox because you want to acquire a refresh token and opt into expiring access tokens for a better security model.

Finally, there’s an option to request user authorization during installation, which can remain unchecked because you don’t need it for your app:

Identifying and authorizing users section

Post Installation Options

Moving down the page, there are post installation options and web hooks, which you can leave blank:

Post installation section

Be sure that the Active checkbox under Webhook is unchecked!

Permissions Settings

The next section relates to permissions. This is where you specify what information your app should be able to access.

In the Contents settings, change the access option to Read-only:

Repository permissions section

Finally, there’s a setting for where to install your app. For this example, select Only on this account:

App account options section

Congratulations, you’ve configured your app. Now, click Create GitHub App and you’ll see your app’s details page.

Viewing Your Results

You’ve created an app in GitHub with its own app ID and client ID. Now you can set up a client — in this case, the AuthHub iOS app — to connect and use your GitHub app. If you had three different apps — perhaps iOS, Android and web — that connect to your GitHub app, you’d want to generate three different client secrets.

If you see a yellow banner at the top of the page telling you that you must generate a private key, click the link to do just that.

Next, click the Generate a new client secret button. Copy the alphanumeric value shown and paste it somewhere permanent because GitHub will never let you view this information again.

Generating client secret

Be very careful about how you store your client secret and make sure you don’t share it.

You’re all done on the GitHub side. It’s time to write some Swift code!

Connecting GitHub App with ASWebAuthenticationSession

Download the project materials by clicking the Download Materials button at the top or bottom of the tutorial. Open the starter project in Xcode. Build and run.

Sign-in page

Right now, tapping Sign In doesn’t do anything. You’ll need to implement the sign in feature in the project. Prior to doing that, you’ll explore the starter project.

You have a basic project with two screens: one to sign in and one to view your repositories. Also, you have models for User, Repository and NetworkRequest.

The most interesting model here is NetworkRequest. It acts as a wrapper around URLSession.

Open NetworkRequest.swift. You’ll find enumerations that encapsulate the supported HTTP methods and errors for your network layer. The more interesting enum here is RequestType, which lists cases for the supported requests the app can make to GitHub. In the enumeration, you also have helper methods to construct a NetworkRequest for a specific request type. Pretty handy!

For more information, check out GitHub’s documentation on its REST API.

Updating the Project with GitHub App Values

To let GitHub know about your app, you’ll add your GitHub app’s information to the project.

Open NetworkRequest.swift. Under // MARK: Private Constants, you’ll find three static constants:

static let callbackURLScheme = "YOUR_CALLBACK_SCHEME_HERE"
static let clientID = "YOUR_CLIENT_ID_HERE"
static let clientSecret = "YOUR_CLIENT_SECRET_HERE"

Key character

Replace these values with the values from your newly created GitHub app.

The callback URL scheme doesn’t need to be the entire URL you entered when creating your GitHub app, it just needs to be the scheme. For this example, use authhub as the string for callbackURLScheme.

Moving on, start(responseType:completionHandler:) within NetworkRequest is where the actual network request goes out. Here, you define some parameters for your URL request along with the authorization HTTP header, should your app have an access token available.

The GitHub API expects you to send the access token for requests that require authorization via the Authorization HTTP header. The value of this header will be in the format of:

Bearer YOUR_TOKEN_HERE

In addition, the method handles any errors with the completion handler and parses JSON data into a native Swift type specified in the parameters.

So far, so good!

Views Overview

Next, you have the views. This is a fairly simple app, so there are only two views necessary: SignInView.swift and RepositoriesView.swift. Not much to worry about here, the fun stuff happens in the view models.

Finally, you have the view models.

View Models Overview

Open RepositoriesViewModel.swift. This is where you’ll find code that requests a list of the logged-in user’s repositories from GitHub and provides them to the view to display in a list.

The other view model in the app is in SignInViewModel.swift. This is where you want to add your ASWebAuthenticationSession. You’ll work on that now.

Understanding ASWebAuthenticationSession

ASWebAuthenticationSession is an API that’s part of Apple’s Authentication Services framework and can be used to authenticate a user through a web service.

You create an instance of this class in order to acquire an out-of-the-box solution where you can point your app to an authentication page, allow the user to authenticate, and then receive a callback in your application with the user’s authentication token.

The cool thing about this API is that it’s going to adapt to the native platform it runs on. For iOS this mean an embedded, secure browser, and on macOS your default browser (if it supports web authentication sessions) or Safari.

With just a few parameters, you are up and running against your own (or third-party) authentication services, without having to implement everything from scratch using a web view.

Adding ASWebAuthenticationSession

The way ASWebAuthenticationSession works is that it expects the authentication URL (with all of the required parameters from the authentication provider), your app’s callback URL scheme (in order to get back to your app upon successful login), and a completion handler for you to handle and manage the authentication token.

Open SignInViewModel.swift. Add the following code to signInTapped():

guard let signInURL =
  NetworkRequest.RequestType.signIn.networkRequest()?.url 
else {
  print("Could not create the sign in URL .")
  return
}

You need the URL used to sign in, so you use the RequestType to acquire it. Should the process fail for some reason, an error prints to the console and the method returns without doing anything.

Next, in the same method, add the following:

let callbackURLScheme = NetworkRequest.callbackURLScheme
let authenticationSession = ASWebAuthenticationSession(
  url: signInURL,
  callbackURLScheme: callbackURLScheme) { [weak self] callbackURL, error in
  // Code will be added here next! :)
}

Here, you first create a constant to store your callback URL scheme, then proceed to create a new ASWebAuthenticationSession. The session initializer expects the sign-in URL and callback scheme as well as a completion handler as parameters.

The callback URL scheme is the one you just replaced inside NetworkRequest, but what about the sign-in URL?

Open NetworkRequest.swift. Look at the .signIn case inside url(). Here, you see the host, path and parameters needed to make a successful sign-in request. Of note is client_id, which you added in this file a little while ago.

Checking for Errors

Open SignInViewModel.swift, replace // Code will be added here next! :) with:

// 1
guard 
  error == nil,
  let callbackURL = callbackURL,
  // 2
  let queryItems = URLComponents(string: callbackURL.absoluteString)?
    .queryItems,
  // 3
  let code = queryItems.first(where: { $0.name == "code" })?.value,
  // 4
  let networkRequest =
    NetworkRequest.RequestType.codeExchange(code: code).networkRequest() 
else {
  // 5
  print("An error occurred when attempting to sign in.")
  return
}

This is quite a big guard statement, but necessary nonetheless. Here’s what’s going on:

  1. You check for errors and confirm there’s a valid callback URL.
  2. From there, you acquire the URL’s query items by extracting the components of the callback URL. The query items will help you check whether this response has the authorization code you need to exchange for tokens.
  3. When the callback URL loads, it includes the authorization code as a query parameter.
  4. Next, acquire a NetworkRequest for the code exchange.
  5. Should any of these checks fail, you print an error and return from this method.

Build and run.

Tap the Sign In button. And check out the results… Nothing?!

Setting the Presentation Context Provider

There are two more things you need to do before your authentication session can work. The first is to set a presentation context provider.

To do this, you’ll first need to implement the necessary protocol. Do that now by adding the following extension at the end of SignInViewModel.swift:

extension SignInViewModel: ASWebAuthenticationPresentationContextProviding {
  func presentationAnchor(for session: ASWebAuthenticationSession)
  -> ASPresentationAnchor {
    let window = UIApplication.shared.windows.first { $0.isKeyWindow }
    return window ?? ASPresentationAnchor()
  }
}

Here, you implement ASWebAuthenticationPresentationContextProviding to tell your authentication session how to present itself. Behind the scenes, ASWebAuthenticationSession works with a browser, cookies, sessions and so on to show your users a login screen then redirect them to your app.

Now, add the following to the end of signInTapped():

authenticationSession.presentationContextProvider = self

You set the location for the authorization view. This takes care of the presentation portion.

Cartoon sign-in screen

Starting the Authentication Session

The second thing you need to do before this works is to start the authentication session.

In SignInViewModel, add the following code to signInTapped():

if !authenticationSession.start() {
  print("Failed to start ASWebAuthenticationSession")
}

This verifies whether the session was able to start. If not, then another error will print to the console.

Build and run.

Tap the Sign In button. You’ll may see an alert if you’re on an iOS version prior to 12.4 (this is part of the authentication session API, indicating that your app wants to use GitHub to sign in).

Tap Continue.

You’ll see a modal controller with the GitHub login page. Upon successful login, the modal dismisses and, once again, nothing happens.

But this is good! No, really, it is.

If you enter invalid credentials, you’ll see the authentication error within the GitHub page itself, but if the modal dismisses and no errors print in the console. That’s because GitHub responded to your app with the authorization code.

Handling the Authorization Code

At this point, you need to exchange the authorization code for the access and refresh tokens.

Open SignInViewModel.swift. Inside signInTapped(), add the following code at the end of authenticationSession‘s completion handler:

self?.isLoading = true
networkRequest.start(responseType: String.self) { result in
  switch result {
  case .success:
    self?.getUser()
  case .failure(let error):
    print("Failed to exchange access code for tokens: \(error)")
  }
}

While this takes place, you tell the view that something is loading, which replaces the Sign In button with an activity view. This prevents your users from going through this flow again while you’re in the middle of working on the existing session. You use the network request that you acquired earlier as part of the guard statement to perform the token exchange.

NetworkRequest‘s start(responseType:completionHandler:) also has a completion handler. Inside it, you check the request result for success or failure. If it succeeds, you proceed to call getUser(). If it fails, you print an error to the console.

Displaying the Results

Before running the app again, add the following code to getUser():

isLoading = true
NetworkRequest
  .RequestType
  .getUser
  .networkRequest()?
  .start(responseType: User.self) { [weak self] result in
    switch result {
    case .success:
      self?.isShowingRepositoriesView = true
    case .failure(let error):
      print("Failed to get user, or there is no valid/active session: \(error)")
    }
    self?.isLoading = false
  }

This method is similar to what you did with NetworkRequest, except this gets the signed-in user’s information. If the request succeeds, you’ll set a Boolean to true that tells the view to show the repositories. Otherwise, you print an error to the console.

Regardless of the outcome, you tell the view that loading has finished.

Build and run. Then, sign in like before.

Two unexpected things happen:

  1. The modal just shows and dismisses without giving you a chance to input your GitHub credentials
  2. An error message displays in the Xcode console.

App character swatting a bug

Because ASWebAuthenticationSession works with web views, cookies and a web session behind the scenes, it has cached a state that results in getting an authorization code automatically. Note that this happens for an indeterminate amount of time, not forever, but is still not something you want.

You’ll handle this later in the Creating Ephemeral Sessions section. For now, focus on the error about a failure during the token exchange.

Before you can solve it, you need a bit of theory about the tokens themselves.

Understanding Tokens

Upon login, users can now acquire an authorization code. This, however, is not the final stopping point. This access code has a very short duration and can usually be used just once. Its purpose is for you to exchange it for an access token and a refresh token.

As you saw earlier, the access token is the one you’ll send with every request to authorize your requests. The refresh token is usually longer-lived. You can use it to acquire a new access token when it expires.

Handling Tokens

With the authorization code in hand, it’s time to exchange it for tokens. You’ll also add logic in your app to handle them properly.

Open NetworkRequest.swift.

Inside start(responseType:completionHandler:) and right below the following code block:

guard 
  error == nil,
  let data = data
else {
  DispatchQueue.main.async {
    let error = error ?? NetworkRequest.RequestError.otherError
    completionHandler(.failure(error))
  }
  return
}

Find the line:

if let object = try? JSONDecoder().decode(responseType, from: data) {

Replace this line with the following code:

// 1
if T.self == String.self,
  let responseString = String(data: data, encoding: .utf8) {
  // 2
  let components = responseString.components(separatedBy: "&")
  var dictionary: [String: String] = [:]
  // 3
  for component in components {
    let itemComponents = component.components(separatedBy: "=")
    if let key = itemComponents.first,
       let value = itemComponents.last {
      dictionary[key] = value
    }
  }
  // 4
  DispatchQueue.main.async {
    // 5
    NetworkRequest.accessToken = dictionary["access_token"]
    NetworkRequest.refreshToken = dictionary["refresh_token"]
    completionHandler(.success((response, "Success" as! T)))
  }
  return
} else if let object = try? JSONDecoder().decode(T.self, from: data) {

Whereas the other GitHub API responses are JSON formatted, the token exchange response comes back as a string. Here’s how you handle that case:

  1. By adding this statement, you first see if there’s a string response as its contents.
  2. This response string, with your tokens, is made of key-value pairs separated by the ampersand, which is why you break that string into an array of key-value pairs.
  3. You loop through each of the components to acquire a Swift dictionary of the response.
  4. Right now, everything is running in a background thread due to URLSession‘s default threading model. Therefore, you make a call to DispatchQueue to call the next code on the main thread. You need to do this because the completion handler will be updating the UI, which can only be done on the main thread.
  5. The code within the block stores the access and refresh tokens within two helper properties of NetworkRequest. It then proceeds to call the completion handler, indicating that the process was successful.

How NetworkRequest Handles the Tokens

To see what NetworkRequest does with the tokens, switch over to NetworkRequest+User.swift.

These are properties of type String, but behind the scenes, they read and write the tokens to UserDefaults so they can persist across app launches.

These tokens are sensitive data, so in your apps, you want to ensure you securely store these in the keychain. This is an interesting topic which you can learn more about here in How To Secure iOS User Data: Keychain Services and Biometrics with SwiftUI.

Build and run. Sign in. While the modal may flash on screen again, you’ll now see the list of your repositories afterward:

Repository List

Fantastic!

Creating Ephemeral Sessions

The last topic to cover is ephemeral sessions, which are private authentication sessions. Ephemeral sessions don’t cache session-related data to disk but to RAM. Upon session invalidation, the ephemeral sessions’ data clears the session data. This conveniently gives users the added privacy and security.

Open. SignInViewModel.swift. In signInTapped and below:

authenticationSession.presentationContextProvider = self

Add the following code:

authenticationSession.prefersEphemeralWebBrowserSession = true

This mitigates the issue encountered earlier, where attempting to sign in immediately after running your app a second or third time results in no prompt to enter the user credentials. An ephemeral session means ASWebAuthenticationSession will not cache anything and will always ask the user for their credentials at the start of the session.

Build and run.

You’ll still be signed in since the app persists the access and refresh tokens. Tap Sign Out at the top to clear the tokens. Now, upon attempting to sign in, you’re asked to input your GitHub username and password. The app won’t cache anything from your previous login, as this is a private session.

Happy app character

Excellent! This might seem like a small issue, but security-wise, it’s of great benefit.

Where to Go From Here?

You can download the completed project by clicking the Download Materials button at the top or bottom of this tutorial.

Great work! In this tutorial, you got an introduction to OAuth and created your own third-party GitHub app using ASWebAuthenticationSession to authenticate. Some additional features you could try are enhancing the way you store the tokens by putting them in the keychain and adding more GitHub API requests.

If you’re interested in reading more about OAuth, visit IETF’s page, which contains the whole standard. It’ a bit dense, but a good reference.

For ASWebAuthenticationSession details and documentation, visit the Apple Developer Documentation.

For a comprehensive video course on networking, check out the Networking with URLSession.

If you have any questions or comments, don’t hesitate to reach out in the forum discussion below.