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

23. GitHub 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 chapter, you learned how to authenticate users using Google. In this chapter, you’ll see how to build upon this and allow users to log in with their GitHub accounts.

Setting up your application with GitHub

To be able to use GitHub OAuth in your application, you must first register the application with GitHub. In your browser, go to https://github.com/settings/developers. Click Register a new application:

Note: You must have a GitHub account to complete this chapter. If you don’t have one, visit https://github.com/join to create one. This chapter also assumes you added Imperial as a dependency to your project in the previous chapter.

Fill in the form with an appropriate name, e.g. Vapor TIL. Set the Homepage URL to http://localhost:8080 for this application and provide a sensible description. Set the Authorization callback URL to http://localhost:8080/oauth/github. This is the URL that GitHub redirects back to once users have allowed your application access to their data:

Click Register application. After it creates the application, the site takes you back to the application’s information page. That page provides the client ID. Click Generate a new client secret to get a client secret:

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

Integrating with Imperial

Now that you’ve registered your application with GitHub, you can start integrating Imperial. First, open Package.swift in Xcode and replace:

.product(name: "ImperialGoogle", package: "Imperial")
.product(name: "ImperialGoogle", package: "Imperial"),
.product(name: "ImperialGitHub", package: "Imperial")
import ImperialGitHub
func processGitHubLogin(request: Request, token: String)
  throws -> EventLoopFuture<ResponseEncodable> {
    return request.eventLoop.future(request.redirect(to: "/"))
  }
guard let githubCallbackURL =
  Environment.get("GITHUB_CALLBACK_URL") else {
    fatalError("GitHub callback URL not set")
}
try routes.oAuth(
  from: GitHub.self,
  authenticate: "login-github",
  callback: githubCallbackURL,
  completion: processGitHubLogin)
GITHUB_CALLBACK_URL=http://localhost:8080/oauth/github
GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID>
GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET>

Integrating with web authentication

As in the previous chapter, it’s important to match the experience for a regular login. Again, you’ll create a new user when a user logs in with GitHub for the first time. You can use GitHub’s API with the user’s OAuth token.

struct GitHubUserInfo: Content {
  let name: String
  let login: String
}
extension GitHub {
  // 1
  static func getUser(on request: Request)
    throws -> EventLoopFuture<GitHubUserInfo> {
      // 2
      var headers = HTTPHeaders()
      try headers.add(
        name: .authorization, 
        value: "token \(request.accessToken())")
      headers.add(name: .userAgent, value: "vapor")

      // 3
      let githubUserAPIURL: URI = "https://api.github.com/user"
      // 4
      return request
        .client
        .get(githubUserAPIURL, headers: headers)
        .flatMapThrowing { response in
          // 5
          guard response.status == .ok else {
            // 6
            if response.status == .unauthorized {
              throw Abort.redirect(to: "/login-github")
            } else {
              throw Abort(.internalServerError)
            }
          }
          // 7
          return try response.content
            .decode(GitHubUserInfo.self)
      }
  }
}
// 1
return try GitHub
  .getUser(on: request)
  .flatMap { userInfo in
    // 2
    return User
      .query(on: request.db)
      .filter(\.$username == userInfo.login)
      .first()
      .flatMap { foundUser in
        guard let existingUser = foundUser else {
          // 3
          let user = User(
            name: userInfo.name,
            username: userInfo.login,
            password: UUID().uuidString)
          // 4
          return user
            .save(on: request.db)
            .flatMap {
              // 5
              request.session.authenticate(user)
              return generateRedirect(on: request, for: user)
          }
        }
        // 6
        request.session.authenticate(existingUser)
        return generateRedirect(on: request, for: existingUser)
    }
}
<a href="/login-github">
  <img class="mt-3" src="/images/sign-in-with-github.png"
   alt="Sign In With GitHub">
</a>

Integrating with iOS

Just like signing in with Google, you should offer the ability to sign in with GitHub on iOS as well. You’ve already done most of this work in the previous chapter :]

func iOSGitHubLogin(_ req: Request) -> Response {
  // 1
  req.session.data["oauth_login"] = "iOS"
  // 2
  return req.redirect(to: "/login-github")
}
routes.get("iOS", "login-github", use: iOSGitHubLogin)
// 1
guard let githubAuthURL =
  URL(string: "http://localhost:8080/iOS/login-github") 
else {
  return
}
// 2
let scheme = "tilapp"
// 3
let session = ASWebAuthenticationSession(
  url: githubAuthURL, 
  callbackURLScheme: scheme) { callbackURL, error in
  // 4
  guard 
    error == nil, 
    let callbackURL = callbackURL 
  else { 
    return 
  }

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

Where to go from here?

In this chapter, you learned how to integrate GitHub login into your website using Imperial and OAuth. This complements the Google and first-party sign in experiences and allows your users to choose a range of options for authentication.

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