In this episode, you learn how to authenticate a user using basic authentication, or basic auth for short, in Swift. We use the URLSession API in this episode, but the concepts we discuss apply to any networking library or framework that can send requests over HTTP(S).

What Is Basic Authentication?

Basic authentication is also known as basic auth or basic access authentication. It is a simple method to authenticate a user over HTTP(S). 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 the user has access to.

As the name suggests, basic authentication is a simple, straightforward method to authenticate a user over HTTP(S). It requires a username or email address that identifies the user and a password to prove that the user is who they claim to be.

I won't cover the security concerns of basic authentication in this episode. You are here because you want to learn how to authenticate a user using basic authentication in Swift.

Starter Project

Open the starter project of this episode if you would like to follow along. The application shows a view with a sign in form. The sign in view is driven by a view model, an instance of the SignInViewModel class.

import SwiftUI

struct SignInView: View {

    // MARK: - Properties

    @ObservedObject var viewModel: SignInViewModel

    // MARK: - View

    var body: some View {
        HStack {
            Spacer()

            VStack {
                VStack(alignment: .leading) {
                    Text("Email")
                    TextField("Email", text: $viewModel.email)
                        .autocapitalization(.none)
                        .keyboardType(.emailAddress)
                        .disableAutocorrection(true)
                    Text("Password")
                    SecureField("Password", text: $viewModel.password)
                }
                .textFieldStyle(.roundedBorder)
                .disabled(viewModel.isSigningIn)

                if viewModel.isSigningIn {
                    ProgressView()
                        .progressViewStyle(.circular)
                } else {
                    Button("Sign In") {
                        viewModel.signIn()
                    }
                }

                Spacer()
            }
            .padding()
            .frame(maxWidth: 400.0)

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

}

To sign in, the user enters their credentials, email and password, and taps the Sign In button at the bottom of the form. That triggers the view model's signIn() method. Let's take a look at the implementation of the SignInViewModel class. SignInViewModel conforms to the ObservableObject protocol and defines a number of publishers. That is how the view model receives the values of the sign in form.

import Combine
import Foundation

final class SignInViewModel: ObservableObject {

    // MARK: - Properties

    @Published var email = ""
    @Published var password = ""

    // MARK: -

    @Published var hasError = false

    @Published var isSigningIn = false

    // MARK: -

    var canSignIn: Bool {
        !email.isEmpty && !password.isEmpty
    }

    // MARK: - Public API

    func signIn() {
        guard !email.isEmpty && !password.isEmpty else {
            return
        }
    }

}

Let's focus on the implementation of the signIn() method. The method returns early if the user didn't provide an email or password. How do we implement basic authentication in Swift using the URLSession API?

We start by creating a mutable URLRequest object, passing in the URL of the API endpoint we use to authenticate the user. As you can see, I use a local server for this example.

func signIn() {
    guard !email.isEmpty && !password.isEmpty else {
        return
    }

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

We set the httpMethod property to POST. You could set it to GET, which is the default. I prefer to use POST whenever authentication is involved. I won't discuss why that is in this episode.

func signIn() {
    guard !email.isEmpty && !password.isEmpty else {
        return
    }

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

    request.httpMethod = "POST"
}

The next step is what you are here for, basic authentication. The specification that describes basic authentication is lengthy, but the gist isn't complex. Basic authentication requires the request to have an Authorization header that contains the user's credentials. Let me show you how that works.

We create a string that concatenates the email and the password, separated by a colon. That string needs to be base 64 encoded. We do that by converting the string to a Data object. The view model base 64 encodes the user's credentials by invoking the base64EncodedString() method on the Data object. That was the most complex section of this episode.

func signIn() {
    guard !email.isEmpty && !password.isEmpty 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()
}

To add the Authorization header to the request, we invoke addValue(_:forHTTPHeaderField:) on the URLRequest object, passing in the encoded credentials and the name of the header, Authorization. Notice that the value of the header is the word Basic followed by the base 64 encoded credentials. Don't overlook this detail.

func signIn() {
    guard !email.isEmpty && !password.isEmpty 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")
}

The rest of the implementation isn't difficult. We set isSigningIn to true to show a progress view to the user and create a data task to send the request to the server. We ask the shared URL session for a data task, passing in the URLRequest object. In the completion handler we inspect the response of the data task.

func signIn() {
    guard !email.isEmpty && !password.isEmpty 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()
}

In the completion handler, we update one of the view model's publishers. Because the publisher is used by the sign in view, we handle the response on the main thread using Grand Central Dispatch also known as GCD. This is necessary because the completion handler of the data task is executed on a background thread.

func signIn() {
    guard !email.isEmpty && !password.isEmpty 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
        DispatchQueue.main.async {
        	
        }
    }.resume()
}

We start by setting isSigningIn to false to indicate to the user that the request completed, successfully or unsuccessfully.

func signIn() {
    guard !email.isEmpty && !password.isEmpty 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
        DispatchQueue.main.async {
            self?.isSigningIn = false
        }
    }.resume()
}

We keep error handling simple. If error isn't equal to nil or the status code of the response isn't 200, something went wrong. The request to sign the user in failed. If that happens, the view model's hasError property is set to false. The sign in view shows an alert to the user if that happens.

func signIn() {
    guard !email.isEmpty && !password.isEmpty 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
        DispatchQueue.main.async {
            if error != nil || (response as! HTTPURLResponse).statusCode != 200 {
                self?.hasError = true
            } else if let data = data {
            	
            }

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

We use an else if clause to safely unwrap the Data object. In this example, the response of a successful request contains an access token the client can use to sign requests. We define a simple struct, SignInResponse, in SignInViewModel.swift. We declare SignInResponse as fileprivate and conform it to Decodable. It defines a single property, accessToken of type String.

fileprivate struct SignInResponse: Decodable {

    // MARK: - Properties

    let accessToken: String

}

In the completion handler of the data task, we use a JSONDecoder instance to convert the Data object into a SignInResponse object. The access token can be cached in the keychain for later use.

func signIn() {
    guard !email.isEmpty && !password.isEmpty 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
        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)

                    // TODO: Cache Access Token in Keychain
                } catch {
                    print("Unable to Decode Response \(error)")
                }
            }

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

What's Next?

That's it. This is what it takes to adopt basic authentication in Swift using the URLSession API. You can use any networking library or framework, such as Alamofire, if you find the URLSession API too verbose.