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?
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. :]