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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sign in with Apple Using Vapor 4
35 mins
- Getting Started
- Looking at the Vapor Project
- Running the Vapor Project
- Setting up ngrok
- Looking at the iOS App
- Running the iOS App
- Sign in with Apple Authentication Flow
- Sign in with Apple Authentication With iOS & Vapor
- Registering a User
- Logging in a User
- Finishing the Sign in with Apple Authentication Handler
- Connecting the iOS App to Your Back End
- Sign in with Apple Authentication on Web
- Sign in with Apple Web Authentication Flow
- Implementing the Leaf Template
- Controlling Your Front End
- Setting up the Services Identifier and Redirect URL
- Inspecting the Sign in with Apple Callback
- Implementing the Sign in with Apple Callback
- Where to Go From Here?
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.
- 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:
There are a handful of things to pay particular attention to:
- siwa.leaf: This is the leaf template you’ll use to implement the Sign in with Apple front end.
- 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.
-
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
andSIWAViewController
later. -
Migrations: You’ll find the migrations for
User
andToken
here. - Models: This contains various models used in the project.
- configure.swift: Here’s where anything required for this tutorial — such as Sessions, Leaf, Databases and Migrations — are set up.
- 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.
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
:
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:
In Insomnia, replace localhost:8080
with the highlighted URL. If your server still runs, you’ll see the same response:
Looking at the iOS App
Open the iOS app source by double-clicking SignInWithApple.xcodeproj in siwa-ios. You’ll find this structure:
There are only a few relevant files:
- 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.
-
SignInWithAppleDelegates.swift: This handles the callbacks from Apple’s
ASAuthorizationController
and calls theWebAPI
. -
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 implementauthorizeUsingSIWA(identityToken:email:firstName:lastName:completion:)
later.
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
.
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.
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:
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:
- Using
ASAuthorizationAppleIDButton
, ask your users to Sign in with Apple from the iOS app. Apple will validate the credentials and provide your app with anASAuthorizationAppleIDCredential
. - 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.
- Using the Apple identifier, you check if this is a new or existing user. If the Apple identifier is unknown, create a new user.
- Create a new Bearer token, which the app will use to authenticate the user.
- Use the Bearer token to get the user’s profile using the
GET /api/users/me
endpoint, which returns 401 at the moment. - 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:
- Creating a
struct
for decoding the request body. - 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:
- 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. -
verify(_:applicationIdentifier:)
returns a future type,AppleIdentityToken
, that you can use to grab the Apple user identifier underAppleIdentityToken.subject.value
. You’re using theappleUserIdentifier
to check if a user with this identifier already exists. - If a user is not returned, register a new user and return a Bearer token; otherwise, return a Bearer token.
- 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.
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.
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:
- As discussed, your method takes an
appleIdentityToken
,firstName
andlastName
and returns anEventLoopFuture<UserResponse>
. - If no
email
is provided inappleIdentityToken
, the method returns a failing future with an error (you’ll add this in a second). -
User.assertUniqueEmail(_:req:)
checks for duplicates and returns a failing future, in case a user with this email already exists. - Creates a new
User
. - Stores the user in your database.
- Creates a new access token for this user.
- Stores the new access token into your database.
- 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:
- You take the same input and produce the same output as in
signUp(appleIdentityToken:firstName:lastName:req:)
. - Check if a user with the provided Apple identifier (stored in
AppleIdentityToken.subject.value
) exists. - If a user isn’t found, return a 404 error —
.notFound
. - If the data from Apple has changed, update a user’s email, first name and last name.
- Create and save an access token for your user.
- 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:
- Call
signUp(appleIdentityToken:firstName:lastName:req:)
if no user is found. - If a user is found, invoke
signIn(appleIdentityToken:firstName:lastName:req:)
instead. - 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:
- Checking the response for an
identityToken
, and if it’s not there, returning an error. - Converting the
identityToken
into aString
and returning an error if the decoding fails. - Initializing the
SIWAAuthRequestBody
. - 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:
- It creates a
URLSession
and aURLRequest
withurl
,httpMethod
and a properContent-Type
. -
URLSession
sends the request to your back end. - It tries to parse the response into a
UserResponse
model. - The
accessToken
is stored statically so it’s available for any subsequent calls togetProfile(completion:)
. - 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.
You did it! You implemented Sign in with Apple using an iOS app and a Vapor back end.
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:
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.
- You’ll use the Sign in with Apple JavaScript component provided by Apple. Initialize it with
scope
,clientID
,redirectURL
andstate
. The sign-in process is completely wrapped and managed by Apple. - 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.
- From now on, the flow is mostly the same as it was in your previous implementation.
- Once completed, you’ll return a
UserResponse
to the front end and the JSON response will render in the browser.
Implementing the Leaf Template
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:
- 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.
- JavaScript: This is the implementation, as outlined by Apple in its documentation. Apple’s CDN (Content Delivery Network) loads and renders the JavaScript.
-
Leaf variables: For Sign in with Apple, you provide information, such as
clientID
,scope
,redirectURL
andstate
. 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:
- Generating a random value for
state
. When Apple calls yourcallbackURL
, it will provide the same value forstate
, so you can check that the response relates to this specific Sign in with Apple request. - Adding
state
to the request’s session using theSessionsMiddleware
. - 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.
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:
Before you can actually use it, you’ll implement two more steps:
- Add the
servicesIdentifier
andredirectURL
to Apple’s Developer Portal. - 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:
Select the POST /web/auth/siwa/callback
request. Here’s what’s displayed in ngrok:
- It shows the callback request from Apple you selected.
- The post body Apple sends to your callback. (Make sure the Summary tab is selected if you don’t see this.)
- 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
andlastName
, encoded as JSON
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:
- Decoding the post body into
AppleAuthorizationResponse
. - Validating that
state
is the same as the one stored in the session. - Verifying the token returned by Apple. Note: this uses
servicesIdentifier
and notapplicationIdentifier
. - 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:
Congratulations! You understand the fundamentals of Sign in with Apple and can offer this alternative authentication method to your users. :]
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!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more