Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

22. Google Authentication
Written by Tim Condon

In the previous chapters, you learned how to add authentication to the TIL web site. However, sometimes users don’t want to create extra accounts for an application and would prefer to use their existing accounts.

In this chapter, you’ll learn how to use OAuth 2.0 to delegate authentication to Google, so users can log in with their Google accounts instead.

OAuth 2.0

OAuth 2.0 (https://tools.ietf.org/html/rfc6749) is an authorization framework that allows third-party applications to access resources on behalf of a user. Whenever you log in to a website with your Google account, you’re using OAuth.

When you click Login with Google, Google is the site that authenticates you. You then authorize the application to have access to your Google data, such as your email. Once you’ve allowed the application access, Google gives the application a token. The app uses this token to authenticate requests to Google APIs. You’ll implement this technique in this chapter.

Note: You must have a Google account to complete this chapter. If you don’t have one, visit https://accounts.google.com/SignUp to create one.

Imperial

Writing all the necessary scaffolding to interact with Google’s OAuth system and get a token is a time-consuming job!

There’s a community package called Imperial, https://github.com/vapor-community/Imperial, that does the heavy lifting for you. It has integrations for Google, Facebook and GitHub and several more.

Adding to your project

Open Package.swift in Xcode to add the new dependency. Replace:

.package(
  url: "https://github.com/vapor/leaf.git",
  from: "4.0.0")

with the following:

.package(
  url: "https://github.com/vapor/leaf.git", 
  from: "4.0.0"),
.package(
  url: "https://github.com/vapor-community/Imperial.git",
  from: "1.0.0")

Next, add the dependency to your App target’s dependency array. Replace:

.product(name: "Leaf", package: "leaf")

with the following:

.product(name: "Leaf", package: "leaf"),
.product(name: "ImperialGoogle", package: "Imperial")

Next, create a file for a new controller to manage Imperial’s routes. In Sources/App/Controllers create a file called ImperialController.swift. Open the new file and create a new empty controller:

import ImperialGoogle
import Vapor
import Fluent

struct ImperialController: RouteCollection {
  func boot(routes: RoutesBuilder) throws {
  }
}

This creates a new type, ImperialController, that conforms to RouteCollection, implementing the required boot(routes:).

Finally, open routes.swift and add the controller to your application at the bottom of routes(_:):

let imperialController = ImperialController()
try app.register(collection: imperialController)

Setting up your application with Google

To be able to use Google OAuth in your application, you must first register the application with Google. In your browser, go to https://console.developers.google.com/apis/credentials.

If this is the first time you’ve used Google’s credentials, the site prompts you to create a project:

Click Create Project to create a project for the TIL application. Fill in the form with an appropriate name, e.g. Vapor TIL:

After it creates the project, the site takes you back to the Google credentials page for the newly created project. This time, click Create Credentials to create credentials for the TIL app and choose OAuth client ID:

Next, click Configure consent screen to set up the page Google presents to users, so they can allow your application access to their details.

Choose External for the user type and click Create.

Add an app name and select the user support email.

At the bottom of the page, add your developer contact information. Click Save and Continue.

On the next screen, you configure the scopes for your application. These are the permissions you want to request from users, such as their email address. Click Add or remove scopes and select both /auth/userinfo.email and /auth/userinfo.profile. This gives you access to the user’s email and profile which you need to create an account in the TIL app.

Once you’ve selected the scopes, click Update and then Save and continue. Next, you need to select the users you’ll use for testing. Click Add Users and add any users you want to be able to log in. If you publish your app, you can verify your domain and app to remove this limitation. Click Save and continue.

You’ve completed the OAuth consent screen so click Back to dashboard. Click the Credentials page again and click Create Credentials once more and choose OAuth client ID. When creating a client ID, choose Web application. Add a redirect URI for your application for testing — http://localhost:8080/oauth/google. This is the URL that Google redirects back to once users have allowed your application access to their data.

If you want to deploy your application to the internet, such as with AWS or Heroku, add another redirect for the URL for that site — e.g., https://rw-til-vapor.herokuapp.com/oauth/google:

Click Create and the site gives you your client ID and client secret:

Note: You must keep these safe and secure. Your secret allows you access to Google’s APIs, and you should not share or check the secret into source control. You should treat it like a password.

Setting up the integration

Now that you’ve registered your application with Google, you can start integrating Imperial. Open ImperialController.swift and add the following under boot(routes:):

func processGoogleLogin(request: Request, token: String) 
  throws -> EventLoopFuture<ResponseEncodable> {
    request.eventLoop.future(request.redirect(to: "/"))
  }

This defines a method to handle the Google login. The handler simply redirects the user to the home page — the same way that the regular login works. Imperial uses this method as the final callback once it has handled the Google redirect. Notice the use of eventLoop.future(_:) to create a future from request.redirect(to:). This is because the method that Imperial uses requires an EventLoopFuture.

Next, set up the Imperial routes by adding the following in boot(routes:):

guard let googleCallbackURL =
  Environment.get("GOOGLE_CALLBACK_URL") else {
    fatalError("Google callback URL not set")
}
try routes.oAuth(
  from: Google.self,
  authenticate: "login-google",
  callback: googleCallbackURL,
  scope: ["profile", "email"],
  completion: processGoogleLogin)

Here’s what this does:

  • Get the callback URL for Google from an environment variable — this is the URL you set up in the Google console.
  • Register Imperial’s Google OAuth router with your app’s router.
  • Tell Imperial to use the Google handlers.
  • Set up the /login-google route as the route that triggers the OAuth flow. This is the route the application uses to allow users to log in via Google.
  • Provide the callback URL to Imperial.
  • Request the profile and email scopes from Google — this matches the scopes you set when creating your application earlier.
  • Set the completion handler to processGoogleLogin(request:token:) - the method you created above.

In order for Imperial to work, you need to provide it the client ID and client secret that Google gave you. You provide these to Imperial using environment variables. There are a number of ways to do this but Vapor has built in support for .env files. This allows you to define environment variables in a file that Vapor reads. This works from both the command line and Xcode. Note: .env files rely on you setting the custom working directory when running in Xcode. See Chapter 14, “Templating with Leaf” if you need more information about how to do this. Create a new file in your project directory called .env and open it in your favorite text editor. Insert the following:

GOOGLE_CALLBACK_URL=http://localhost:8080/oauth/google
GOOGLE_CLIENT_ID=<THE_CLIENT_ID_FROM_GOOGLE>
GOOGLE_CLIENT_SECRET=<THE_CLIENT_SECRET_FROM_GOOGLE>

Insert your client ID and client secret provided by Google.

Note: It’s good practice to add .env files to .gitignore so you don’t check secrets into source control.

Integrating with web authentication

It’s important to provide a seamless experience for users and match the experience for the regular login. To do this, you need to create a new user when a user logs in with Google for the first time. To create a user, you can use Google’s API to get the necessary details using the OAuth token.

Sending requests to third-party APIs

At the bottom of ImperialController.swift, add a new type to decode the data from Google’s API:

struct GoogleUserInfo: Content {
  let email: String
  let name: String
}

The request to Google’s API returns many fields. However, you only care about the email, which becomes the username, and the name.

Next, under GoogleUserInfo, add the following:

extension Google {
  // 1
  static func getUser(on request: Request)
    throws -> EventLoopFuture<GoogleUserInfo> {
      // 2
      var headers = HTTPHeaders()
      headers.bearerAuthorization =
        try BearerAuthorization(token: request.accessToken())

      // 3
      let googleAPIURL: URI =
        "https://www.googleapis.com/oauth2/v1/userinfo?alt=json"
      // 4
      return request
        .client
        .get(googleAPIURL, headers: headers)
        .flatMapThrowing { response in
        // 5
        guard response.status == .ok else {
          // 6
          if response.status == .unauthorized {
            throw Abort.redirect(to: "/login-google")
          } else {
            throw Abort(.internalServerError)
          }
        }
        // 7
        return try response.content
          .decode(GoogleUserInfo.self)
      }
  }
}

Here’s what this does:

  1. Add a new method to Imperial’s Google service that gets a user’s details from the Google API.
  2. Set the headers for the request by adding the OAuth token to the authorization header.
  3. Set the URL for the request — this is Google’s API to get the user’s information. This uses Vapor’s URI type, which Client requires.
  4. Use request.client to send the request to Google. get() sends an HTTP GET request to the URL provided. Unwrap the returned future response.
  5. Ensure the response status is 200 OK.
  6. Otherwise, return to the login page if the response was 401 Unauthorized or return an error.
  7. Decode the data from the response to GoogleUserInfo and return the result.

Next, replace the contents of processGoogleLogin(request:token:) with the following:

// 1
try Google
  .getUser(on: request)
  .flatMap { userInfo in
    // 2
    User
      .query(on: request.db)
      .filter(\.$username == userInfo.email)
      .first()
      .flatMap { foundUser in
        guard let existingUser = foundUser else {
          // 3
          let user = User(
            name: userInfo.name,
            username: userInfo.email,
            password: UUID().uuidString)
          // 4
          return user
            .save(on: request.db)
            .map {
              // 5
              request.session.authenticate(user)
              return request.redirect(to: "/")
            }
        }
        // 6
        request.session.authenticate(existingUser)
        return request.eventLoop
          .future(request.redirect(to: "/"))
      }
  }

Here’s what the new code does:

  1. Get the user information from Google.
  2. See if the user exists in the database by looking up the email as the username.
  3. If the user doesn’t exist, create a new User using the name and email from the user information from Google. Set the password to a UUID string, since you don’t need it. This ensures that no one can login to this account via a normal password login.
  4. Save the user and unwrap the returned future.
  5. Call session.authenticate(_:) to save the created user in the session so the website allows access. Redirect back to the home page.
  6. If the user already exists, authenticate the user in the session and redirect to the home page.

Note: In a real world application, you may want to consider using a flag to separate out users registered on your site vs. logging in with OAuth.

The final thing to do is to add a button on the website to allow users to make use of the new functionality! Open login.leaf and, under </form>, add the following:

<a href="/login-google">
  <img class="mt-3" src="/images/sign-in-with-google.png"
   alt="Sign In With Google">
</a>

The sample project for this chapter contains a new, Google-provided image, sign-in-with-google.png, to display a Sign in with Google button. This adds the image as a link to /login-google — the route provided to Imperial to start the login.

Save the Leaf template and build and run the application in Xcode. Remember to set the custom working directory before running. Visit http://localhost:8080 in your browser.

Click Create An Acronym and the application takes you to the login page. You’ll see the new Sign in with Google button:

Click the new button and the application takes you to a Google page to allow the TIL application access to your information:

Select the account you want to use and the application redirects you back to the home page. Go to the All Users screen and you’ll see your new user account. If you create an acronym, the application also uses that new user.

Integrating with iOS

You’ve integrated Imperial with the TIL website to allow users to sign in with Google. However, you also have another client — the iOS app. You can reuse most of the existing code to allow users to sign in to the iOS app with Google as well! In ImperialController.swift add a new route handler below processGoogleLogin(_:):

func iOSGoogleLogin(_ req: Request) -> Response {
  // 1
  req.session.data["oauth_login"] = "iOS"
  // 2
  return req.redirect(to: "/login-google")
}

Here’s what the new route does:

  1. Add an entry to the request’s session, noting that this OAuth login attempt came from iOS.
  2. Redirect to the URL you created earlier to start the OAuth flow for logging in to the website using Google.

Register the new route at the bottom of boot(routes:):

routes.get("iOS", "login-google", use: iOSGoogleLogin)

This routes a GET request to /iOS/login-google to iOSGoogleLogin(_:). Then, below iOSGoogleLogin(_:), add a new method to create the redirect for logging in:

// 1
func generateRedirect(on req: Request, for user: User) 
  -> EventLoopFuture<ResponseEncodable> {
    let redirectURL: EventLoopFuture<String>
    // 2
    if req.session.data["oauth_login"] == "iOS" {
      do {
        // 3
        let token = try Token.generate(for: user)
        // 4
        redirectURL = token.save(on: req.db).map {
          "tilapp://auth?token=\(token.value)"
        }
      // 5
      } catch {
        return req.eventLoop.future(error: error)
      }
    } else {
      // 6
      redirectURL = req.eventLoop.future("/")
    }
    // 7
    req.session.data["oauth_login"] = nil
    // 8
    return redirectURL.map { url in
      req.redirect(to: url)
    }
}

Here’s what the new code does:

  1. Define a new method that takes both Request and User to generate a redirect. This new method returns EventLoopFuture<ResponseEncodable>.
  2. Check the request’s session data for the oauth_login flag to see if it matches the flag set in iOSGoogleLogin(_:).
  3. If the request is from iOS, generate a token for the user.
  4. Save the token, resolve the returned future and return a redirect. This uses the tilapp scheme and returns the token as a query parameter. You’ll use this in the iOS app.
  5. Catch any errors thrown by generating the token and return a failed future.
  6. If the request is not from iOS, create a future string for the original redirect URL.
  7. Reset the oauth_login flag for the next session.
  8. Resolve the future and return a redirect using the returned string.

Next, in processGoogleLogin(request:token:), replace:

return user.save(on: request.db).map {
  request.session.authenticate(user)
  return request.redirect(to: "/")
}

with the following:

return user.save(on: request.db).flatMap {
  request.session.authenticate(user)
  return generateRedirect(on: request, for: user)
}

This returns a generated redirect for the new user instead of the hard-coded /. It also replaces map with flatMap as the closure now returns a future.

Finally, replace:

return request.eventLoop
  .future(request.redirect(to: "/"))

with the following:

return generateRedirect(on: request, for: existingUser)

This returns a redirect generated for the existing user. Build and run the app, then open the TILiOS starter project. The iOS project is similar to the final project from Chapter 19, “API Authentication, Part 2”. The login screen now contains a new button for signing in with Google.

Open LoginTableViewController.swift. The Sign in with Google button triggers signInWithGoogleButtonTapped(_:) when tapped. This doesn’t do anything at the moment. At the top of the file, below import UIKit add:

import AuthenticationServices

This imports the Authentication Services framework which you’ll use for signing in. Then, in signInWithGoogleButtonTapped(_:), add the following:

// 1
guard let googleAuthURL = URL(
  string: "http://localhost:8080/iOS/login-google") 
else {
  return
}
// 2
let scheme = "tilapp"
// 3
let session = ASWebAuthenticationSession(
  url: googleAuthURL, 
  callbackURLScheme: scheme) { callbackURL, error in
}

Here’s what this does:

  1. Create a URL that matches the route you created in TILApp for signing in with Google earlier.
  2. Define the scheme to use. This matches the scheme of the redirect the TILApp you set earlier.
  3. Create an instance of ASWebAuthenticationSession. This allows the user to authenticate with the TIL app using existing credentials from Safari.

Next, at the bottom of the file, add the following extension:

extension LoginTableViewController: ASWebAuthenticationPresentationContextProviding {
  func presentationAnchor(
    for session: ASWebAuthenticationSession
  ) -> ASPresentationAnchor {
    guard let window = view.window else {
      fatalError("No window found in view")
    }
    return window
  }
}

This conforms the view controller to ASWebAuthenticationPresentationContextProviding and implements presentationAnchor(for:) as required by the protocol. Then, in the callback for ASWebAuthenticationSession(url:callbackURLScheme:) add the following:

// 1
guard 
  error == nil, 
  let callbackURL = callbackURL 
else { 
  return 
}

// 2
let queryItems = 
  URLComponents(string: callbackURL.absoluteString)?.queryItems
// 3
let token = queryItems?.first { $0.name == "token" }?.value
// 4
Auth().token = token
// 5
DispatchQueue.main.async {
  let appDelegate = 
    UIApplication.shared.delegate as? AppDelegate
  appDelegate?.window?.rootViewController =
    UIStoryboard(name: "Main", bundle: Bundle.main)
      .instantiateInitialViewController()
}

Here’s what’s going on:

  1. Ensure there’s no error and a callback URL is set.
  2. Get the query items from the callback URL.
  3. Extract the token from the URL. This is the token provided in the redirect you set up earlier.
  4. Set the token on the Auth instance.
  5. Replace the root view controller to complete the log in process.

Finally, below ASWebAuthenticationSession(url:callbackURLScheme:) add the following:

session.presentationContextProvider = self
session.start()

This sets the session’s presentationContextProvider to the current view controller. This allows iOS to know where to launch the browser from. It then starts the session to start the log in flow.

Build and run the app and log out if necessary in the Users tab. You’ll see the new Sign in with Google button:

Tap the button and you’ll get a prompt to allow the app to access the TIL website to log in:

Click Continue and the app redirects you to Google to sign in or select an account to use. Complete the log in process and select an account and the app logs you in.

Where to go from here?

In this chapter, you learned how to integrate Google login into your website using Imperial and OAuth. This allows users to sign in with their existing Google accounts!

The next chapter shows you how to integrate another popular OAuth provider: GitHub.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.