Implementing OAuth with ASWebAuthenticationSession

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

5 (8) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.