7. Enable authentication


In this section, you'll add the ability to log in to the example server and obtain a token that your client can use to make identified requests.

Note: The way you log in to this particular server might differ from the way you log in with your own server. Login is often handled by middleware, or a layer totally separate from GraphQL.

Regardless of how you obtain the token from your server, you'll usually send it back to the server the same way as demonstrated in the next part of this tutorial.

Create a login mutation

A mutation is an operation that changes state on your server. In this case, the mutation changes back-end state by creating a session tied to a particular user of your client.

Open your Sandbox Explorer and click on the plus symbol to add a new tab. Next, click on the Schema icon to get back to looking at your schema, and select "Mutation" to look at your list of mutations:

The list of available mutations

Scroll down to take a look at the login mutation:

The definition of login in the schema

Click the play button to the right to open that mutation in the Explorer tab. When it opens, click the plus sign to add the operation:

The login mutation after initially being added

Sandbox Explorer tries to be helpful by adding the word Mutation after the word Login, but the iOS SDK also does that for you - so you'd wind up with a LoginMutationMutation. Rename the operation Login instead:

The renamed operation

Notice that there are no brackets after the call to the login mutation - this is because the type returned by this mutation is String, which is known as a scalar type. This means it won't have properties the way an object would, so you don't need to specify what properties you want returned to you.

You'll also notice that email wasn't automatically added as an argument even though it doesn't seem to have a default value: That's because email is of type String - which remember, in GraphQL, means that it's not required (although obviously you won't get too far without it).

Click the plus sign next to the email argument to have that argument added:

The operation with the email argument

You'll also notice that Sandbox Explorer has added a variable to your "Variables" section to match the login email.

Click the Submit Operation button your mutation. You'll see that since you sent null for the email address, you get back null for the login:

Results of passing a null email

Now, replace null in the Query Variables section with an actual email address:

JSON
1{ "loginEmail": "me@example.com" }

Press the Submit Operation button, and this time you'll get an actual response:

Results of passing an actual email

Next, copy the operation, either manually or using the meatball menu's "Copy operation" option.

Open Xcode, create a new empty file named Login.graphql, and paste the operation into it. Build your project to make sure the codegen adds the mutation to your API.swift file.

Define login logic

Now it's time to actually log the user in, using LoginViewController.swift. You'll notice in the IBAction for submitTapped, we're doing some very basic validation that the user has actually entered an email address before submitting it.

Still in submitTapped, replace the TODO with a call to perform the login mutation:

Swift
LoginViewController.swift
1 Network.shared.apollo.perform(mutation: LoginMutation(loginEmail: email)) { [weak self] result in
2  defer {
3    // Re-enable the submit button when this scope exits
4    self?.enableSubmitButton(true)
5  }
6
7  switch result {
8  case .failure(let error):
9    self?.showAlert(title: "Network Error",
10                    message: error.localizedDescription)
11  case .success(let graphQLResult):
12  
13    if let token = graphQLResult.data?.login {
14      // TODO: Store the token securely
15    }
16
17    if let errors = graphQLResult.errors {
18      let message = errors
19                     .map { $0.localizedDescription }
20                     .joined(separator: "\n")
21      self?.showAlert(title: "GraphQL Error(s)",
22                      message: message)
23    }
24  }
25}

Next, you need to store the login credential that's returned by the server. Login credentials should always be stored in the Keychain, but interacting with it directly is challenging, so you'll be using the KeychainSwift library which has already been added as a Swift Package to this project.

At the top of LoginViewController.swift, import the KeychainSwift library:

Swift
LoginViewController.swift
1import KeychainSwift

Next, note that there's a static let at the top of the view controller that will give you a value you can use to query the keychain for a login token. Because this is a static let, you can use it outside instances of LoginViewController (which you will do in a second).

Replace the TODO after unwrapping the token with the following:

Swift
LoginViewController.swift
1let keychain = KeychainSwift()
2keychain.set(token, forKey: LoginViewController.loginKeychainKey)
3self?.dismiss(animated: true)

With this code, once you log in successfully, the token will be stored in the keychain, and then the LoginViewController will automatically dismiss (if it still exists). This will send you back to the DetailViewController.

Display the login screen

Now, it's time to show the login view controller whenever someone attempts to book a trip without being logged in.

At the top of DetailViewController.swift, import the KeychainSwift library:

Swift
DetailViewController.swift
1import KeychainSwift

Then, find the isLoggedIn method and replace its contents with the following:

Swift
DetailViewController.swift
1private func isLoggedIn() -> Bool {
2  let keychain = KeychainSwift()
3  return keychain.get(LoginViewController.loginKeychainKey) != nil
4}

This code checks if there is any value stored in the keychain under the login keychain key. If there isn't, it determines the user isn't logged in. If there is, the user is logged in.

Find the bookOrCancelTapped method and start by determining what to do if the user is logged in or not:

Swift
DetailViewController.swift
1@IBAction private func bookOrCancelTapped() {
2  guard self.isLoggedIn() else {
3    self.performSegue(withIdentifier: "showLogin", sender: self)
4    return
5  }
6  
7  // TODO: Book or cancel the trip
8}

Then, replace the TODO in the above code with logic to figure out whether a trip on the current launched needs to be booked or cancelled:

Swift
DetailViewController.swift
1guard let launch = self.launch else {
2  // We don't have enough information yet to know
3  // if we're booking or cancelling, bail.
4  return
5}
6    
7if launch.isBooked {
8  print("Cancel trip!")
9} else {
10  print("Book trip!")
11}

Build and run the application, and select a launch from the list to view its detail screen. Now when you tap the "Book now!" button, the LoginViewController will appear. Use it to log yourself in.

When the DetailViewController reappears, if you tap the "Book now!" button again, you'll see the print statement you added logging happily to the console:

Console with book trip printed out

In the next step, you'll use your login token in a mutation to book yourself a trip !