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.