With the view and the view model in place, we can authenticate the user. In this episode, you learn how to authenticate a user using the URLSession API and basic authentication.

Development Setup

When you need to integrate an application with a remote API, it is often faster and more convenient to run a local instance of the API during development. This isn't always possible, but it is worth the investment.

For this episode, I am running a Vapor application on a local server. The Vapor application defines an endpoint to authenticate users. The endpoint uses basic authentication, basic auth for short, to authenticate the user. It returns an access token and a refresh token on success.

It is important to make a distinction between authentication and authorization. Authentication is the process of identifying a user. Authorization is the process of verifying which resources a user has access to. In this episode, we focus on authenticating the user using an email and password combination.

Performing a Network Request

Open SignInViewModel.swift, navigate to the signIn() method, and remove the asyncAfter(deadline:qos:flags:execute:) method call. Even though the Sign In button is disabled if either of the form fields is empty, we use a guard statement to return early if email or password is empty. This is a detail, but an important one. The view model shouldn't assume the user isn't able to sign in if either of the form fields is empty. Remember from the previous episode that validating user input is a responsibility of the view model, not the view.

func signIn() {
    guard canSignIn else {
        return
    }

    isSigningIn = true
}

Before setting isSigningIn to true, we create a mutable URLRequest object. The initializer accepts the URL of the API endpoint as an argument. It is fine to forced unwrap the URL because that should never fail. We improve the implementation later.

func signIn() {
    guard canSignIn else {
        return
    }

    var request = URLRequest(url: URL(string: "http://localhost:8080/api/v1/signin")!)

    isSigningIn = true
}

Even though basic authentication doesn't define the HTTP verb or method of the request, it is safer to use POST for requests that involve authentication. Why that is is a separate discussion. We set the httpMethod property of the request to POST.

func signIn() {
    guard canSignIn else {
        return
    }

    var request = URLRequest(url: URL(string: "http://localhost:8080/api/v1/signin")!)

    request.httpMethod = "POST"

    isSigningIn = true
}

Basic authentication is easy to implement, but there are a few details you need to be aware of. We add a header field with name Authorization. The value of the header field contains the user's credentials prefixed with the word Basic. Let me show you how that works.

We create a string by concatenating the user's email and password, separating email and password with a colon. The resulting string needs to be base 64 encoded. To do that, we convert the string to a Data object using UTF-8 encoding. To create a base 64 encoded string from the Data object, we invoke the base64EncodedString() method. It is safe to forced unwrap the result of the String to Data conversion since we use Unicode encoding. That conversion should never fail.

func signIn() {
    guard canSignIn else {
        return
    }

    var request = URLRequest(url: URL(string: "http://localhost:8080/api/v1/signin")!)

    request.httpMethod = "POST"

    let authData = (email + ":" + password).data(using: .utf8)!.base64EncodedString()

    isSigningIn = true
}

We invoke the addValue(_:forHTTPHeaderField:) method on the request to add the Authorization header. Remember that the value of the header field is the base 64 encoded string prefixed with the word Basic.

func signIn() {
    guard canSignIn else {
        return
    }

    var request = URLRequest(url: URL(string: "http://localhost:8080/api/v1/signin")!)

    request.httpMethod = "POST"

    let authData = (email + ":" + password).data(using: .utf8)!.base64EncodedString()
    request.addValue("Basic \(authData)", forHTTPHeaderField: "Authorization")

    isSigningIn = true
}

To send the request, we ask the shared URL session singleton for a data task, passing in the request as the first argument and a completion handler as the second argument. The completion handler accepts three arguments, an optional Data object, an optional URLResponse object, and an optional Error object. To send the request, we invoke resume() on the data task.

func signIn() {
    guard canSignIn else {
        return
    }

    var request = URLRequest(url: URL(string: "http://localhost:8080/api/v1/signin")!)

    request.httpMethod = "POST"

    let authData = (email + ":" + password).data(using: .utf8)!.base64EncodedString()
    request.addValue("Basic \(authData)", forHTTPHeaderField: "Authorization")

    isSigningIn = true

    URLSession.shared.dataTask(with: request) { [weak self] data, response, error in

    }.resume()
}

The completion handler of the data task is executed on a background thread. Because the result of the authentication request is reflected in the user interface, we need to make sure the response is handled on the main thread. This is straightforward using Grand Central Dispatch. Also note that self, the view model, is weakly captured by the completion handler. While this isn't strictly necessary in this example, I tend to weakly reference self to prevent the completion handler from holding on to self for too long.

URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
    DispatchQueue.main.async {

    }
}.resume()

In the completion handler, we set isSigningIn to false to communicate to the user that the request completed, successfully or unsuccessfully.

URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
    DispatchQueue.main.async {
        self?.isSigningIn = false
    }
}.resume()

Because we are authenticating the user, I am interested in the Error object and the status code of the response. If error has a value or the status code of the response isn't equal to 200, authenticating the user was unsuccessful and we need to notify the user. We declare a variable property, hasError, of type Bool and prefix it with the Published property wrapper.

import Foundation

final class SignInViewModel: ObservableObject {

    // MARK: - Properties

    @Published var email = ""
    @Published var password = ""
    @Published var isSigningIn = false

    @Published var hasError = false
	
	...

}

If authenticating the user was unsuccessful, we set hasError to true.

URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
    DispatchQueue.main.async {
        if error != nil || (response as? HTTPURLResponse)?.statusCode != 200 {
            self?.hasError = true
        }

        self?.isSigningIn = false
    }
}.resume()

We use an else if clause to safely unwrap the Data object, the body of the response.

URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
    DispatchQueue.main.async {
        if error != nil || (response as? HTTPURLResponse)?.statusCode != 200 {
            self?.hasError = true
        } else if let data = data {

        }

        self?.isSigningIn = false
    }
}.resume()

The response of a successful request contains an access token and a refresh token. Let's create a model for the response of the authentication request. Add a Swift file and name it SignInResponse.swift. Define a struct, SignInResponse, that conforms to the Decodable protocol. The struct declares two properties of type String, accessToken and refreshToken.

import Foundation

struct SignInResponse: Decodable {

    // MARK: - Properties

    let accessToken: String
    let refreshToken: String

}

Head back to SignInViewModel.swift. Because decoding the response is a throwing operation, we wrap it in a do-catch statement. We store the result of the decoding operation in a constant, signInResponse. We create a JSONDecoder instance and invoke its decode(_:from:) method, passing in the type of the value to decode and the Data object. We add a print statement to print the value of signInResponse. In the catch clause, we set the hasError property to true to notify the user and print the error that is thrown.

URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
    DispatchQueue.main.async {
        if error != nil || (response as? HTTPURLResponse)?.statusCode != 200 {
            self?.hasError = true
        } else if let data = data {
            do {
                let signInResponse = try JSONDecoder().decode(SignInResponse.self, from: data)

                print(signInResponse)
            } catch {
            	self?.hasError = true
                print("Unable to Decode Response \(error)")
            }
        }

        self?.isSigningIn = false
    }
}.resume()

If authenticating the user was unsuccessful, the application should notify the user by displaying an alert. Revisit SignInView.swift and add an alert modifier to the outer HStack. We pass the hasError binding as the first parameter and an Alert object as the second parameter. We keep it simple for now with a title and a message.

var body: some View {
    HStack {
        ...
    }
    .alert(isPresented: $viewModel.hasError) {
        Alert(
            title: Text("Sign In Failed"),
            message: Text("The email/password combination is invalid.")
        )
    }
}

As you may know, it is important to keep the error message generic to avoid giving away why the authentication attempt failed. In this example, we state that the email and password combination the user entered was not valid. We don't tell the user that the password was incorrect or that there isn't a user with that email address. By keeping the error message generic, we don't give people with less good intentions information they shouldn't have.

You can test the implementation by running a local instance of an API that responds to the request with a 200 response. The advantage of running a local instance of an API is the ability to easily mock responses, such as failures. This is what it looks like if the authentication attempt fails.

Authenticating the User

What's Next?

As a developer, you learn to spot code that is smelly or can be improved. The sign in form is functional, but there is room for improvement. In the next episode, we make a number of changes to improve the implementation.