Server-Side Sign in with Apple

Nov 15 2022 · Swift 5.6, macOS 12, iOS 15, Xcode 13.3

Part 2: Add Sign in with Apple to a Website

07. Add the Sign in with Apple Button to a Website

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 06. Setting up Sign in with Apple for the Web Next episode: 08. Handle the Sign in with Apple Callback

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 07. Add the Sign in with Apple Button to a Website

It’s now time to add Sign in with Apple to the website.

Open the Vapor app in Xcode and go to WebsiteController.swift. Each page that displays the Sign in with Apple button needs a few bits of data in order for it to work. At the bottom of the file, create a new type called SIWAContext to hold this data:

struct SIWAContext: Encodable {
  let clientID: String
  let scopes: String
  let redirectURI: String
  let state: String
}

The client ID matches the client you created in the Apple developer portal. The scopes define the data you request from the user and will match the scopes requested in the iOS app. The redirect URL defines the URL Apple will redirect to once the user has completed the Sign in with Apple flow. Finally, the state is a random piece of data used to ensure that the Sign in with Apple attempt is the one initiated from your server.

At the bottom of WebsiteController create a new function called buildSIWAContext(on:) to create this for requests. You’ll display the button on the register page and the log in page so this allows you to build the context in one place. The function will return a SIWAContext:

private func buildSIWAContext(on req: Request) throws -> SIWAContext {

}

First, create the state by generating some random data and define the scopes as the name and email:

let state = [UInt8].random(count: 32).base64
let scopes = "name email"

Then, get the client ID and redirect URL from the environment variables you defined in the previous video:

guard let clientID = Environment.get("WEBSITE_APPLICATION_IDENTIFIER") else {
    req.logger.error("WEBSITE_APPLICATION_IDENTIFIER not set")
    throw Abort(.internalServerError)
}
guard let redirectURI = Environment.get("SIWA_REDIRECT_URL") else {
    req.logger.error("SIWA_REDIRECT_URL not set")
    throw Abort(.internalServerError)
}

Then, finally, create and return the context:

let siwa = SIWAContext(clientID: clientID, scopes: scopes, redirectURI: redirectURI, state: state)
return siwa

Next, change RegisterContext to take a SIWAContext:

struct RegisterContext: Encodable {
  let title = "Register"
  let message: String?
  let siwaContext: SIWAContext
  
  init(message: String? = nil, siwaContext: SIWAContext) {
    self.message = message
    self.siwaContext = siwaContext
  }
}

Navigate to the registerHandler(_:). First, create the context and pass it to the two instances of RegisterContext:

let siwaContext = try buildSIWAContext(on: req)
let context: RegisterContext
if let message = req.query[String.self, at: "message"] {
    context = RegisterContext(message: message, siwaContext: siwaContext)
} else {
    context = RegisterContext(siwaContext: siwaContext)
}

Then, before returning the response, create a cookie to store the state in and set it on the response:

let expiryDate = Date().addingTimeInterval(300)
let cookie = HTTPCookies.Value(string: siwaContext.state, expires: expiryDate, maxAge: 300, isHTTPOnly: true, sameSite: HTTPCookies.SameSitePolicy.none)
response.cookies["SIWA_STATE"] = cookie

This sets the expiry date for the cookie to 5 minutes - this is the amount of time the user has to complete the Sign in with Apple flow. When building the cookie you must set the same site to .none - this ensures that the cookie can be read when Apple redirect back to your website.

Then, do the same with the log in page. Change the LoginContext to take a SIWAContext:

struct LoginContext: Encodable {
  let title: String
  let siwaContext: SIWAContext
}

Then in loginHandler(_:) create the context and pass it to LoginContext:

let siwaContext = try buildSIWAContext(on: req)
let context = LoginContext(title: "Log In", siwaContext: siwaContext)

Then, create a cookie:

let expiryDate = Date().addingTimeInterval(300)
let cookie = HTTPCookies.Value(string: siwaContext.state, expires: expiryDate, maxAge: 300, isHTTPOnly: true, sameSite: HTTPCookies.SameSitePolicy.none)

Change the loginHandler(_:) to return a response:

func loginHandler(_ req: Request) async throws -> Response

And generate a response by rendering the view:

let response: Response = try await req.view.render("login", context).encodeResponse(for: req)

Set the cookie on the response and return it:

response.cookies["SIWA_STATE"] = cookie
return response

Finally, you need to fix the loginPostHandler(_:). Again, create the context and pass it to LoginContext:

let siwaContext = try buildSIWAContext(on: req)
let context = LoginContext(title: "Log In", siwaContext: SIWAContext)

And create a cookie for the state and pass it to the response:

let expiryDate = Date().addingTimeInterval(300)
let cookie = HTTPCookies.Value(string: siwaContext.state, expires: expiryDate, maxAge: 300, isHTTPOnly: true, sameSite: HTTPCookies.SameSitePolicy.none)
let response: Response = try await req.view.render("login", context).encodeResponse(for: req)
response.cookies["SIWA_STATE"] = cookie
return response

The Vapor app now has everything to display the Sign in with Apple buttons. Open login.leaf. Below the login form, create a new <div> with the ID apple-signin - Apple’s JavaScript searches for this when the page loads:

<div id="appleid-signin" class="signin-button" data-color="black" data-border="true" data-type="sign in"></div>

Then, load the Sign in with Apple script:

<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>

Finally, create an AppleID instance, passing in the necessary data from the page context:

<script type="text/javascript">
  AppleID.auth.init({
    clientId : '#(siwaContext.clientID)',
    scope : '#(siwaContext.scopes)',
    redirectURI : '#(siwaContext.redirectURI)',
    state : '#(siwaContext.state)',
    usePopup : false
  });
</script>

Open register.leaf and add the same code below the register form:

<div id="appleid-signin" class="signin-button" data-color="black" data-border="true" data-type="sign in"></div>
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
<script type="text/javascript">
  AppleID.auth.init({
    clientId : '#(siwaContext.clientID)',
    scope : '#(siwaContext.scopes)',
    redirectURI : '#(siwaContext.redirectURI)',
    state : '#(siwaContext.state)',
    usePopup : false
  });
</script>

Save the file and build and run the app to make sure everything compiles. Go to the page via Ngrok, using the URL from the previous video and click Register. You’ll see the Sign in with Apple button.

Note that if you stop and start Ngrok on the free plan you’ll get a new domain so you’ll need to update the environment variable and the redirect URL in the Apple developer portal and your .env file.