Sign in with Apple Using Vapor 4

In this Vapor 4 tutorial, you’ll learn how to implement Sign in with Apple with an iOS companion app and a simple website. By Christian Weinberger.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Controlling Your Front End

Head back to your Vapor app in Xcode. To render your front end, you’ll implement renderSignIn(req:) in SIWAViewController.swift, which is located at Sources/App/Controllers/ViewControllers. You’ll also find an empty implementation for the callback, callback(req:), as well as the RouteCollection extension with the relevant routes.

Start by replacing the implementation of renderSignIn(req:):

func renderSignIn(req: Request) throws -> EventLoopFuture<View> {
  // 1
  let state = [UInt8].random(count: 32).base64
  /// 2
  req.session.data["state"] = state

  return req.view
    // 3
    .render(
      "Auth/siwa",
      SignInViewContext(
        clientID: ProjectConfig.SIWA.servicesIdentifier,
        scope: "name email",
        redirectURL: ProjectConfig.SIWA.redirectURL,
        state: state
      )
    )
}

With the function above you’re:

  1. Generating a random value for state. When Apple calls your callbackURL, it will provide the same value for state, so you can check that the response relates to this specific Sign in with Apple request.
  2. Adding state to the request’s session using the SessionsMiddleware.
  3. Rendering the template from Resources/Views/Auth/siwa and providing the SignInViewContext that’s defined at the beginning of the controller to resolve the Leaf template placeholders. Note that you omit Resources/Views and the file extension when providing the path to your Leaf template.
Note: When you run your project from Xcode, you have to configure the working directory. Otherwise, your back end won’t be able to locate the Leaf files. Do this by going to Product ▸ Schemes ▸ Edit Scheme in the menu bar, navigating to Run ▸ Options and checking Use custom working directory. Select the project folder here — for example, {path_to_your_projects}/siwa-vapor.

You need to set up two environment variables for ProjectConfig.SIWA.ServicesIdentifier and ProjectConfig.SIWA.redirectURL. To add two new environment variables to your Run scheme, go to Product ▸ Schemes ▸ Edit Scheme in the menu bar, locate the environment variables section in Run ▸ Arguments and add:

  • SIWA_SERVICES_IDENTIFIER: e.g. com.raywenderlich.siwa-vapor.services. You must replace this with your own identifier that’s unique to you.
  • SIWA_REDIRECT_URL: {your_ngrok_base_URL}/web/auth/siwa/callback, e.g. https://0f1ecb8f140a.ngrok.io/web/auth/siwa/callback

As defined in routes.swift and the RouteCollection extension of SIWAViewController, you can reach your sign-in front end under /web/auth/siwa/sign-in.

Build and run the project. In your browser navigate to the sign-in page, e.g. https://0f1ecb8f140a.ngrok.io/web/auth/siwa/sign-in.

You’ll now see the Sign in with Apple button:

Showing the front end with Apple's Sign in with Apple button.

Before you can actually use it, you’ll implement two more steps:

  1. Add the servicesIdentifier and redirectURL to Apple’s Developer Portal.
  2. Implement the /web/auth/siwa/callback endpoint.

Setting up the Services Identifier and Redirect URL

Sign in to your Apple Developer Portal and navigate to Certificates, Identifiers and Profiles. Then:

  • Go to Identifiers and add another Services ID. In this case, it’s com.raywenderlich.siwa-vapor.services.
  • Configure your Services ID by navigating to your newly-created Services ID, checking Sign in with Apple and clicking Configure.
  • Link it with the Primary App ID you created with your iOS app. In this case, it’s com.raywenderlich.siwa-vapor.
  • In Domains and Subdomains, add your ngrok domain without the scheme, e.g. 0f1ecb8f140a.ngrok.io
  • In Return URLs, add the full URL to your callback, e.g. https://0f1ecb8f140a.ngrok.io/web/auth/siwa/callback.
  • Click Next and then Done.
  • Confirm the changes by clicking Continue and Save.

Great! Now you can move on and implement the final missing piece!

Inspecting the Sign in with Apple Callback

Before you implement the callback, you have to understand what Apple is actually sending to it. To start, navigate to ngrok’s web interface at http://127.0.0.1:4040 and clear all requests.

Ensure your Vapor app is running and open the sign-in page again. Click the Sign in With Apple button and sign in with your Apple account. Watch the web interface of ngrok. There’s an entry for POST /web/auth/siwa/callback that you’ll inspect:

Inspecting the callback request in ngrok's web console.

Select the POST /web/auth/siwa/callback request. Here’s what’s displayed in ngrok:

  1. It shows the callback request from Apple you selected.
  2. The post body Apple sends to your callback. (Make sure the Summary tab is selected if you don’t see this.)
  3. The response is a 501 Not Implemented as the endpoint is not yet implemented.

Take a detailed look at the post body and you’ll see:

  • code: An authorization code used to get an access token from Apple.
  • id_token: Apple’s identity token, which is JWT encoded.
  • state: This should match with the value you provided to Apple and stored in the request’s session.
  • user: Contains a user’s email address, firstName and lastName, encoded as JSON
Note: This tutorial doesn’t cover the Sign in with Apple access token flow, as there is no real use case for it as of now. Apple wrote the following in its Sign in with Apple documentation: “(Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access.” Hence the Authorization Code is not important right now.

Implementing the Sign in with Apple Callback

To decode the post body, the starter project contains a type, AppleAuthorizationResponse, that matches the callback body. Look closer and you’ll see that custom decoding is required, as Apple encoded the user JSON object as a String.

Go back to SIWAViewController.swift in Xcode and replace callback(req:) with the following:

func callback(req: Request) throws -> EventLoopFuture<UserResponse> {
  // 1
  let auth = try req.content.decode(AppleAuthorizationResponse.self)
  // 2
  guard
    let sessionState = req.session.data["state"],
    !sessionState.isEmpty,
    sessionState == auth.state else {
      return req.eventLoop.makeFailedFuture(UserError.siwaInvalidState)
  }

  // 3
  return req.jwt.apple.verify(
    auth.idToken,
    applicationIdentifier: ProjectConfig.SIWA.servicesIdentifier
  ).flatMap { appleIdentityToken in
    User.findByAppleIdentifier(appleIdentityToken.subject.value, req: req) // 4
      .flatMap { user in
        if user == nil {
          return SIWAAPIController.signUp(
            appleIdentityToken: appleIdentityToken,
            firstName: auth.user?.name?.firstName,
            lastName: auth.user?.name?.lastName,
            req: req
          )
        } else {
          return SIWAAPIController.signIn(
            appleIdentityToken: appleIdentityToken,
            firstName: auth.user?.name?.firstName,
            lastName: auth.user?.name?.lastName,
            req: req
          )
        }
      }
  }
}

In the callback function above, you’re:

  1. Decoding the post body into AppleAuthorizationResponse.
  2. Validating that state is the same as the one stored in the session.
  3. Verifying the token returned by Apple. Note: this uses servicesIdentifier and not applicationIdentifier.
  4. Checking if the user exists and either logging them in or creating a new user.

As the route for the callback is already implemented there’s nothing more to do. Build and run your project, navigate to your login page again and sign in with Apple. You’ll see the result of a UserResponse containing your email address and an access token for your back end:

Showing the front end with the UserResponse.

Congratulations! You understand the fundamentals of Sign in with Apple and can offer this alternative authentication method to your users. :]

Swift bird celebrating