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

At WWDC19, Apple announced Sign in with Apple (SiwA), a new single sign-on (SSO) solution that allows users to set up an account with an app and website using their Apple ID, similar to the way you can log in with Google or Facebook.

Offering an SSO authentication method to your users will improve the first-time user experience, as a user won’t have to create yet another password to remember. Sign in with Apple even goes a step further: It allows users to hide their email addresses from apps and use relay email addresses instead. This is great for privacy!

In this tutorial, you’ll:

  • Learn how to validate an Apple identity token, provided by an iOS app, with your Vapor 4 back end.
  • Use this information to authenticate an existing user or create a new account for them.
  • Create a tiny website that allows users to sign in with Apple to achieve the same result without an iOS app.
Note: You’ll need the following for this project:
  • Xcode 11 and Swift 5.2
  • A paid iOS developer account for setting up the required profiles, keys and certificates
  • An iOS 13 device, as the simulator does not always work properly
  • The ngrok CLI and a free account to connect your Sign in with Apple back end with your iOS app and to Apple’s Sign in with Apple services without deploying it.
  • A REST client to run requests against your back end. This tutorial uses Insomnia Core, but Postman, Paw, RESTed or even curl work fine.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Look in the starter directory and you’ll notice there are two folders with projects inside.

  • siwa-vapor: This is your Vapor 4 back end. You’ll use it to implement Sign in with Apple endpoints and the website.
  • siwa-ios: This is your iOS companion app. It already contains everything necessary to get an identity token from Apple. You’ll connect it to your Vapor 4 back end by adding its URL.

Now it’s time to inspect the project.

Looking at the Vapor Project

Open the Vapor app in Xcode by double-clicking the Package.swift file in siwa-vapor. While you wait for the Swift Package Manager (SPM) to resolve dependencies, check out the existing project in /Sources/App:

The project structure of the Vapor project.

There are a handful of things to pay particular attention to:

  1. siwa.leaf: This is the leaf template you’ll use to implement the Sign in with Apple front end.
  2. ProjectConfig.swift: This contains a couple of project-wide configurations. Your Sign in with Apple-related variables are loaded from environment variables, which you’ll set up later.
  3. Controllers: You’ll see two subdirectories — one for API controllers and one for view controllers. UserAPIController.swift allows you to retrieve the profile of an authenticated user. You’ll implement SIWAAPIController and SIWAViewController later.
  4. Migrations: You’ll find the migrations for User and Token here.
  5. Models: This contains various models used in the project.
  6. configure.swift: Here’s where anything required for this tutorial — such as Sessions, Leaf, Databases and Migrations — are set up.
  7. routes.swift: All routes required for your project are already set up. You’ll find anything user-related under /api/users. The Sign in with Apple endpoints will be under /api/auth/siwa, and you’ll find the Sign in with Apple front end at /web/auth/siwa.

A starter project with Bearer authentication is available in the project. You’ll implement an authentication flow using Sign in with Apple, in which you’ll return a Bearer token to your users that they can use for authentication.

Note: If you want to learn more about Bearer authentication, please read Vapor 4 Authentication: Getting Started by Natan Rolnik.

Running the Vapor Project

When SwiftPM finishes downloading all dependencies, select the Run scheme and My Mac as your platform. Then build and run.

Now switch to Insomnia, or your preferred REST client, and call GET http://localhost:8080/api/users/me:

Insomnia REST Client displaying the request to /api/users/me

You’ll notice your server returns 401 Unauthorized because the token-protected route expects a Bearer token in the authorization header. Also, there are no users in your database yet.

Setting up ngrok

To make your local back end available to both your iOS companion app and Apple — keeping in mind that web authentication requires a reachable callback — you’ll use ngrok. It will create an instant and secure URL that connects to your localhost server.

First, download the client from ngrok.com and move it to your /Applications folder.

Then, if you don’t yet have an account, register for free on ngrok’s signup page. Once you have an account, sign in and go to your dashboard to grab your authtoken from Authentication ▸ Your Authtoken.

Switch to Terminal and configure ngrok with your authttoken:

$ /Applications/ngrok authtoken {your auth token}

You successfully configured ngrok. Now run:

$ /Applications/ngrok http 8080

This will start an HTTP tunnel forwarding to your local port, 8080. In Terminal, you’ll see something similar to:

The running ngrok in Terminal.

Note: ngrok allows you to inspect incoming requests by clicking on the Web Interface link.

In Insomnia, replace localhost:8080 with the highlighted URL. If your server still runs, you’ll see the same response:

Insomnia REST Client displaying the request to /api/users/me, this time using the ngrok base URL.

Note: If you’re on the free tier of ngrok, the URL will change whenever you restart ngrok. Leave ngrok running throughout the tutorial, as both your iOS app and Apple’s callback for web authentication will use this URL.

Looking at the iOS App

Open the iOS app source by double-clicking SignInWithApple.xcodeproj in siwa-ios. You’ll find this structure:

Project structure of the iOS starter project.

There are only a few relevant files:

  1. ContentView.swift: This contains the code for showing the Sign in with Apple button. It’s capable of displaying errors and user profiles in an alert.
  2. SignInWithAppleDelegates.swift: This handles the callbacks from Apple’s ASAuthorizationController and calls the WebAPI.
  3. WebAPI directory: This contains the UserProfile model, as well as a small wrapper to make calls to your Vapor back end. The implementation for getting a profile is already provided. You’ll implement authorizeUsingSIWA(identityToken:email:firstName:lastName:completion:) later.
Note: The iOS starter project comes from our article, Sign in with Apple Using SwiftUI by Scott Grosch. Check it out if you want to understand more.

Running the iOS App

Before you can build and run the iOS app, you need to fill in the URL placeholder. Navigate to WebAPI.swift and update baseURL with the URL you obtained from ngrok, e.g. https://0f1ecb8f140a.ngrok.io.

Important: Make sure you copy the HTTPS URL, otherwise your iOS app will return an error when trying to sign in.

Now, you’ll add the Sign in with Apple capability to your App ID.

In Xcode, go to the SignInWithApple target and select Signing & Capabilities. Select your own team and change the bundle identifier to match your team — it must be unique. Click on + Capability and add Sign in with Apple.

GIF showing how to add the Sign in with Apple capability

Select the SignInWithApple target and run it on your device.

Signing in with Apple should work, but you’ll see an error indicating the operation can’t be completed:

An error showing the operation could not be completed

This error occurs because you haven’t implemented the required code. But before moving on to the implementation, it’s important to understand the fundamentals of Sign in with Apple and how you’ll implement it.

Sign in with Apple Authentication Flow

As you can see in the starter project, your back end already has its own way of authenticating users — Bearer token authentication – in the token-protected routes.

When using third-party authentication flows, such as Sign in with Apple or Google, you still want use your own authentication tokens for your API. This provides a unified API for your clients, e.g. the iOS app. The third-party service replaces the need for a username and password.

Here’s an overview of the flow you’ll implement:

A diagram outlining the Sign in with Apple flow.

  1. Using ASAuthorizationAppleIDButton, ask your users to Sign in with Apple from the iOS app. Apple will validate the credentials and provide your app with an ASAuthorizationAppleIDCredential.
  2. Take the provided Apple identity token and send it to your Vapor server for authentication. The Vapor app will validate the identity token using Apple’s public keys.
  3. Using the Apple identifier, you check if this is a new or existing user. If the Apple identifier is unknown, create a new user.
  4. Create a new Bearer token, which the app will use to authenticate the user.
  5. Use the Bearer token to get the user’s profile using the GET /api/users/me endpoint, which returns 401 at the moment.
  6. The back end will validate the token and return the profile. The app will display the profile to the user.

Remember, you’re using Sign in with Apple to authenticate a user, as shown in steps 1 and 2. However, you provide your user with your own Bearer token, which you’ll use for authentication moving forward.

Now that you know what you need to implement, it’s time to begin!

Sign in with Apple Authentication With iOS & Vapor

Go back to your Vapor project and switch to SIWAAPIController.swift in Sources/App/APIControllers. You’ll find RouteCollection with a single route defined and an empty implementation for authHandler(req:).

First, decode the request body that the iOS app will send. Replace the implementation of SIWAAPIController with the following:

struct SIWAAPIController {
  // 1
  struct SIWARequestBody: Content {
    let firstName: String?
    let lastName: String?
    let appleIdentityToken: String
  }

  func authHandler(req: Request) throws -> EventLoopFuture<UserResponse> {
    // 2
    let userBody = try req.content.decode(SIWARequestBody.self)
  }
}

With the code above you’re:

  1. Creating a struct for decoding the request body.
  2. Trying to decode the request body into the SIWARequestBody.

Now, look at the SIWARequestBody to understand what will be sent to your endpoint:

  • appleIdentityToken, which is a JWT encoded as String
  • firstName and lastName which may or may not be present

Go back to the flow chart and you’ll notice you need to validate the JWT first. Luckily, Vapor 4 has built-in functionality that does this for you. Everything you need is shipped with JWT so that you can use Request.JWT.Apple.verify(applicationIdentifier:).

Now, update authHandler(req:). Below let userBody = try req.content.decode(SIWARequestBody.self), add the following:

// 1
return req.jwt.apple.verify(
  userBody.appleIdentityToken,
  applicationIdentifier: ProjectConfig.SIWA.applicationIdentifier
).flatMap { appleIdentityToken in    
  // 2
  User.findByAppleIdentifier(appleIdentityToken.subject.value, req: req)
    // 3
    .flatMap { user in
      if user == nil {
        // TODO 1: create a new user, return bearer token
      } else {
        // TODO 2: sign in existing user, return bearer token
      }
      // 4
      fatalError()
    }
}

A couple of things are happening in the code above:

  1. You’re using Vapor’s JWT implementation to verify the JWT-encoded Apple identity token provided by the iOS app. Note that you have to provide your iOS app’s bundle ID here. To do so, go to Product ▸ Schemes ▸ Edit Scheme in the menu bar, locate the Environment Variables section in Run ▸ Arguments and add an environment variable with the key SIWA_APPLICATION_IDENTIFIER and the value of your bundle ID you created earlier.
  2. verify(_:applicationIdentifier:) returns a future type, AppleIdentityToken, that you can use to grab the Apple user identifier under AppleIdentityToken.subject.value. You’re using the appleUserIdentifier to check if a user with this identifier already exists.
  3. If a user is not returned, register a new user and return a Bearer token; otherwise, return a Bearer token.
  4. The fatalError() is here to make the code compile without errors. You’ll replace it with an actual implementation in a moment.

Registering a User

Now, you’ll create the actual method for signing up a user. You’ll need the AppleIdentityToken as well as firstName and lastName. In the AppleIdentityToken, you’ll find an email key, which you can use as well.

Note: email , firstName and lastName are usually only provided the first time a user authenticates via Sign in with Apple. It might be a good idea to cache values on the iOS side and adjust your request body to also accept an email field, in case your server isn’t responding when the iOS app wants to authenticate for the first time.

For testing, you can go to the Apple ID page and sign in, then revoke your app from Security ▸ Apps & Websites using Apple ID. Then, the next time you authenticate with Sign in with Apple, the email field will be included.

Revoking Sign in with Apple permissions for your app

Add this below authHandler(req:) in SIWAAPIController.swift:

// 1
static func signUp(
  appleIdentityToken: AppleIdentityToken,
  firstName: String? = nil,
  lastName: String? = nil,
  req: Request
) -> EventLoopFuture<UserResponse> {
  // 2
  guard let email = appleIdentityToken.email else {
    return req.eventLoop.makeFailedFuture(UserError.siwaEmailMissing)
  }
  // 3
  return User.assertUniqueEmail(email, req: req).flatMap {
    // 4
    let user = User(
      email: email,
      firstName: firstName,
      lastName: lastName,
      appleUserIdentifier: appleIdentityToken.subject.value
    )
    // 5
    return user.save(on: req.db)
      .flatMap {
        // 6
        guard let accessToken = try? user.createAccessToken(req: req) else {
          return req.eventLoop.future(error: Abort(.internalServerError))
        }
        // 7
        return accessToken.save(on: req.db).flatMapThrowing { 
          // 8
          try .init(accessToken: accessToken, user: user) 
        } 
    }
  }
}

Here’s what the code above does:

  1. As discussed, your method takes an appleIdentityToken, firstName and lastName and returns an EventLoopFuture<UserResponse>.
  2. If no email is provided in appleIdentityToken, the method returns a failing future with an error (you’ll add this in a second).
  3. User.assertUniqueEmail(_:req:) checks for duplicates and returns a failing future, in case a user with this email already exists.
  4. Creates a new User.
  5. Stores the user in your database.
  6. Creates a new access token for this user.
  7. Stores the new access token into your database.
  8. Returns a UserResponse containing the new user and the access token.

Build and run. Your project should build without errors now.

Logging in a User

Before you connect the new method in your authHandler(req:), you need to add your implementation for the sign-in flow. Add this below signUp(appleIdentityToken:firstName:lastName:req:) in SIWAAPIController.swift:

// 1
static func signIn(
  appleIdentityToken: AppleIdentityToken,
  firstName: String? = nil,
  lastName: String? = nil,
  req: Request
) -> EventLoopFuture<UserResponse> {
  // 2
  User.findByAppleIdentifier(appleIdentityToken.subject.value, req: req)
    // 3
    .unwrap(or: Abort(.notFound))
    .flatMap { user -> EventLoopFuture<User> in
      // 4
      if let email = appleIdentityToken.email {
        user.email = email
        user.firstName = firstName
        user.lastName = lastName
        return user.update(on: req.db).transform(to: user)
      } else {
        return req.eventLoop.future(user)
      }
    }
    // 5
    .flatMap { user in
      guard let accessToken = try? user.createAccessToken(req: req) else {
        return req.eventLoop.future(error: Abort(.internalServerError))
      }
      return accessToken.save(on: req.db).flatMapThrowing { 
        // 6
        try .init(accessToken: accessToken, user: user) 
      }
  }
}

This looks similar to previous steps:

  1. You take the same input and produce the same output as in signUp(appleIdentityToken:firstName:lastName:req:).
  2. Check if a user with the provided Apple identifier (stored in AppleIdentityToken.subject.value) exists.
  3. If a user isn’t found, return a 404 error — .notFound.
  4. If the data from Apple has changed, update a user’s email, first name and last name.
  5. Create and save an access token for your user.
  6. Return a UserResponse containing the user and the access token.

Finishing the Sign in with Apple Authentication Handler

You’ve now finished both implementations for registering and logging in a user using Sign in with Apple. Next, you’ll invoke the new methods. Replace the closure for the final flatMap in authHandler(req:) with the following:

if user == nil {
  // 1
  return SIWAAPIController.signUp(
    appleIdentityToken: appleIdentityToken,
    firstName: userBody.firstName,
    lastName: userBody.lastName,
    req: req
  )
} else {
  return SIWAAPIController.signIn(
    appleIdentityToken: appleIdentityToken,
    firstName: userBody.firstName,
    lastName: userBody.lastName,
    req: req
  )
  // 3
}

To complete this function’s implementation, you:

  1. Call signUp(appleIdentityToken:firstName:lastName:req:) if no user is found.
  2. If a user is found, invoke signIn(appleIdentityToken:firstName:lastName:req:) instead.
  3. Remove fatalError() as you no longer need it

You’ve now implemented authHandler(req:), which is already registered to POST /api/auth/siwa. Build and run your Vapor app to ensure it’s running the latest code.

Connecting the iOS App to Your Back End

To test if your implementation works, you’ll connect the iOS app to your back end. So navigate to your iOS project and open WebAPI.swift.

Go to authorizeUsingSIWA(identityToken:email:firstName:lastName:completion:) and remove the current implementation. Replace it with the following to create a SIWAAuthRequestBody:

// 1
guard let identityToken = identityToken else {
  completion(.failure(WebAPIError.identityTokenMissing))
  return
}

// 2
guard let identityTokenString = String(data: identityToken, encoding: .utf8) else {
  completion(.failure(WebAPIError.unableToDecodeIdentityToken))
  return
}

// 3
let body = SIWAAuthRequestBody(
  firstName: firstName,
  lastName: lastName,
  appleIdentityToken: identityTokenString
)

// 4
guard let jsonBody = try? JSONEncoder().encode(body) else {
  completion(.failure(WebAPIError.unableToEncodeJSONData))
  return
}

With the request body above, you’re:

  1. Checking the response for an identityToken, and if it’s not there, returning an error.
  2. Converting the identityToken into a String and returning an error if the decoding fails.
  3. Initializing the SIWAAuthRequestBody.
  4. Encoding SIWAAuthRequestBody into JSON and returning an error if the encoding fails.

Now, you’ll create the request. Similar to the implementation for getting a user’s profile, you’ll use URLSession and URLRequest for this. Append this to your method body:

// 1
let session = URLSession.shared
let url = URL(string: "\(baseURL)/api/auth/siwa")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

// 2
session.uploadTask(with: request, from: jsonBody) { (data, response, error) in
  // 3
  do {
    let userResponse: UserResponse = 
      try parseResponse(response, data: data, error: error)
    // 4
    accessToken = userResponse.accessToken

    // 5
    completion(.success(userResponse.user))
  } catch {
    completion(.failure(error))
  }
}.resume()

Here’s what’s going on with the code above:

  1. It creates a URLSession and a URLRequest with url, httpMethod and a proper Content-Type.
  2. URLSession sends the request to your back end.
  3. It tries to parse the response into a UserResponse model.
  4. The accessToken is stored statically so it’s available for any subsequent calls to getProfile(completion:).
  5. It then returns either the user profile or an error.

Make sure your Vapor app is running. Then run your iOS app as well and test Sign in with Apple again.

GIF showing the successful implementation of Sign in with Apple.

You did it! You implemented Sign in with Apple using an iOS app and a Vapor back end.

Celebrate your success

Sign in with Apple Authentication on Web

Now, it’s time to implement Sign in with Apple on the web.

Sign in with Apple Web Authentication Flow

Before you jump in to the implementation, take a quick look at the Sign in with Apple web authentication flow below:

A diagram outlining the Sign in with Apple web flow.

Apple provides a JavaScript component to render a Sign in with Apple button, and it also handles the authentication. You’re providing a callback URL, which Apple will call once the authentication flow is complete.

  1. You’ll use the Sign in with Apple JavaScript component provided by Apple. Initialize it with scope, clientID, redirectURL and state. The sign-in process is completely wrapped and managed by Apple.
  2. Upon successful sign in, Apple will call your callback URL from its JavaScript component. It includes anything you need to validate the authenticity, like the Apple identity token.
  3. From now on, the flow is mostly the same as it was in your previous implementation.
  4. Once completed, you’ll return a UserResponse to the front end and the JSON response will render in the browser.

Implementing the Leaf Template

Swift and Leaf

In Vapor, you can use Leaf as a templating engine to render your front end. It’s already configured in configure.swift, and there’s a prepared Leaf template for you as well. You’ll find it at Resources/Views/Auth/siwa.leaf:

<!DOCTYPE html>

<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- 1 -->
  <style>
      \#appleid-signin {
          width: 240px;
          height: 40px;
      }
      \#appleid-signin:hover {
          cursor: pointer;
      }
      \#appleid-signin > div {
          outline: none;
      }
  </style>
</head>
<body>
  <!-- 2 -->
  <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
  <div id="appleid-signin" data-color="black" data-border="false" data-type="sign in"></div>
  <!-- 3 -->
  <script type="text/javascript">
    AppleID.auth.init({
      clientId : '#(clientID)',
      scope : '#(scope)',
      redirectURI : '#(redirectURL)',
      state : '#(state)',
      usePopup : false
    });
  </script>
</body>
</html>

This template contains everything you need to display the Sign in with Apple button:

  1. CSS: There customizes the appearance of the Sign in with Apple button. There are problems with rendering it properly unless you provide a fixed width or height. There are also some cosmetic changes to ensure a proper hover state and to get rid of the outline.
  2. JavaScript: This is the implementation, as outlined by Apple in its documentation. Apple’s CDN (Content Delivery Network) loads and renders the JavaScript.
  3. Leaf variables: For Sign in with Apple, you provide information, such as clientID, scope, redirectURL and state. You provide these in your context when you render your Leaf template.

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

Where to Go From Here?

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

In this article, you learned how to validate an Apple identity token with your Vapor 4 back end. You used this information to authenticate an existing user or create a new account for them. Finally, you used Leaf to create a small front-end website that allows users to sign in with Apple.

If you’re looking for a challenge beyond the scope of this article, here are a few things you can try:

  • Allow signing in using Sign in with Apple with an existing user by matching the email address.
  • Allow the user to sign up and sign in using a username and password.
  • Add a profile page to your front end that’s protected by your access token.

If you want to learn more, you’ll find the Vapor 4 Authentication: Getting Started and Sign in with Apple using SwiftUI tutorials helpful. Additionally, Apple’s Sign in with Apple and Communicating Using the Private Email Relay Service documentation has useful information.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!