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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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!

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")
.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")
.product(name: "Leaf", package: "leaf")
.product(name: "Leaf", package: "leaf"),
.product(name: "ImperialGoogle", package: "Imperial")
import ImperialGoogle
import Vapor
import Fluent

struct ImperialController: RouteCollection {
  func boot(routes: RoutesBuilder) throws {
  }
}
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.

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: "/"))
  }
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)
GOOGLE_CALLBACK_URL=http://localhost:8080/oauth/google
GOOGLE_CLIENT_ID=<THE_CLIENT_ID_FROM_GOOGLE>
GOOGLE_CLIENT_SECRET=<THE_CLIENT_SECRET_FROM_GOOGLE>

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
}
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)
      }
  }
}
// 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: "/"))
      }
  }
<a href="/login-google">
  <img class="mt-3" src="/images/sign-in-with-google.png"
   alt="Sign In With Google">
</a>

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")
}
routes.get("iOS", "login-google", use: iOSGoogleLogin)
// 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)
    }
}
return user.save(on: request.db).map {
  request.session.authenticate(user)
  return request.redirect(to: "/")
}
return user.save(on: request.db).flatMap {
  request.session.authenticate(user)
  return generateRedirect(on: request, for: user)
}
return request.eventLoop
  .future(request.redirect(to: "/"))
return generateRedirect(on: request, for: existingUser)
import AuthenticationServices
// 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
}
extension LoginTableViewController: ASWebAuthenticationPresentationContextProviding {
  func presentationAnchor(
    for session: ASWebAuthenticationSession
  ) -> ASPresentationAnchor {
    guard let window = view.window else {
      fatalError("No window found in view")
    }
    return window
  }
}
// 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()
}
session.presentationContextProvider = self
session.start()

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!

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now