Adding Sign in with Apple to your iOS App

6 min read
Matheus C.
Matheus C.
Published May 1, 2020 Updated March 11, 2021

Since April 2020, all apps that use a third-party or social login service are required to offer Sign in with Apple if they want to be accepted in the App Store.

In a previous tutorial, we walked through the process of building a simple clone of Apple's iMessage. In this tutorial, we'll go through the steps of adding Sign in with Apple to that iMessage clone. If you have an existing app built with Stream's iOS Chat SDK and want to support this sign-in method, the steps will be similar, and the differences pointed out. If you're not building a messaging app, a good chunk of the information here will still apply, so stick around!

If you get lost during this tutorial, you can always check the completed project in this GitHub repo. If you run into any errors, there's a Troubleshooting section at the end of the repo's README that you can check on.

What is Sign in with Apple?

Sign in with Apple makes it easy for users to sign in to your apps and websites using their Apple ID. Instead of filling out forms, verifying email addresses, and choosing new passwords, they can use Sign in with Apple to set up an account and start using your app right away. All accounts are protected with two-factor authentication for superior security, and Apple will not track users' activity in your app or website.

Apple Developer Portal: Sign in with Apple

What you need

Step 1: Configuring the client project

If your app is still early in development or using the iMessage clone project, follow the steps in the image below. If your application is already in production, you can jump to the next image.

1.1: Select a device you have signed in with Apple ID and 2FA. 1.2: Select your team. 1.3: Choose a unique bundle id.

Now, you need to add the Sign in with Apple capability in the "Signing & Capabilities" tab of your target.

Screenshot of target settings highlighting the capability button and the Sign in with Apple capability

Step 2: Adding the sign-in button

To add the sign-in button, we'll use the AuthenticationServices framework, which provides all the functionality needed, including UI elements.

We also need to add the authentication screen, which will appear before the contacts screen. Let's create an AuthenticationViewController.swift file and paste in the following code:

import UIKit
import AuthenticationServices

class AuthViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupSignInButton()
    }
    
    func setupSignInButton() {
        let button = ASAuthorizationAppleIDButton()
        button.center = view.center
        view.addSubview(button)
        button.addTarget(self, action: #selector(handleSignInPress), for: .touchUpInside)
    }

Now, let's add this new screen as the first screen in the storyboard. If you're using the iMessage clone project, it should be the root view controller of the navigation controller. To finish it up, set up a Segue from it to the contacts screen with id kAuthToContactsSegueId:

Storyboard with the auth screen with a segue to the contacts screen

Now, run the project, and you should have a button that, when pressed, leads to the contacts screen.

Animation of the Sign in with Apple button being pressed and the contacts screen showing up

Of course, there is no real authentication happening here yet. We'll look into that in the next steps.

Step 3: Setting up the backend

Before we build real authentication into the client, we'll need a backend that can generate a Stream token when given the Apple ID credentials. If you already have a backend, you can use one of Stream's server-side chat libraries to set it up similarly.

To keep things short, we'll build a simple Node.js/Express backend with one endpoint: /authenticate. We'll also use the node-persist package to persist new user data the first time they authenticate. If you're not interested in building the backend yourself or are having problems, you can get the complete code in the repository.

In the terminal, run the following commands:

$ mkdir backend; cd backend
$ npm init --yes
$ npm install stream-chat apple-auth express node-persist jsonwebtoken --save
$ touch index.js

The following code snippets can be copied in sequence into the index.js.

Let's start coding our index.js by importing the objects we need:

const StreamChat = require('stream-chat').StreamChat;
const AppleAuth = requite('apple-auth');
const express = require('express');
const storage = require('node-persist');
const jwt = require('jsonwebtoken');

Now, configure the Stream Chat client with the credentials that you get in your Stream dashboard:

const apiKey = '[api_key]'
const serverKey = '[server_key]'
const client = new StreamChat(apiKey, serverKey);

Configure the Apple Auth client:

const appleAuth = new AppleAuth({
  client_id: "[client_id]", // eg: my.unique.bundle.id.iMessageClone
  team_id: "[team_id]", // eg: FWD9Q5VYJ2
  key_id: "[key_id]", // eg: 8L3ZMA7M3V
  scope: "name email"
}, './config/AuthKey.p8');

For details on filling those parameters, read apple-auth's SETUP.md.

If you're using something other than Node.js for your backend, there are versions of this library for other languages, such as Go. If you can't find it for your preferred backend stack, you'll have to read through apple-auth's source code and Sign in with Apple's API specifications to implement something similar from scratch.

Initialize express and the node-persist storage:

var app = express();
app.use(express.json());

storage.init();
Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Now, let's start building the handler for the /authenticate endpoint by extracting the parameters we need from the request:

app.post('/authenticate', async (req, res) => {
  const {appleUid, appleAuthCode, name} = req.body;
  console.log(`[INFO] Sign in attempt with request: ${JSON.stringify(req.body)}`)

Now, we verify the appleAuthCode with Apple's servers using apple-auth and extract the user's email address from the response:

  let email;
  try {
    const response = await appleAuth.accessToken(appleAuthCode);
    email = jwt.decode(response.id_token).email;
    console.log(`[INFO] User identity confirmed by Apple.`)
  } catch {
    console.log(`[ERROR] Could not confirm user identity with Apple.`);
    res.sendStatus(401);
    return;
  }

If we have an email, and a name was supplied in the authentication request we need to store that information in our persistent storage:

  if(email && name) {
    const streamId = Buffer.from(email).toString('base64').replace(/=/g, '@');
    const userData = {streamId, email, name}
    await storage.set(appleUid, userData);
    console.log(`[INFO] User registered with email: ${email}. Derived streamId: ${streamId}`)
  }

Information other than Apple Uid and auth code is only guaranteed to be given by Apple the first time the user tries to sign in. We use this fact to determine when to register the user, and it's why we need to save the information and resupply it in every authentication response.

Now, let's finish the handler by fetching the user data from local storage and relaying it in the response:


  const userData = await storage.get(appleUid);

  if (!userData) {
    console.log(`[ERROR] User not found in persistent storage.`);
    res.sendStatus(404);
    return;
  }

  const response = {
    apiKey,
    streamId: userData.streamId,
    streamToken: streamClient.createToken(userData.streamId),
    email: userData.email,
    name: userData.name

Finally, let's configure the express app to listen on port 4000:

const port = process.env.PORT || 4000;
app.listen(port);
console.log(`Running on port ${port}`);

Now, we can close index.js and leave the backend running with the following command:

$ node index.js

If this is working, opening localhost:4000/authenticate in your browser will display Cannot GET /authenticate, which is OK, because we'll use POST in the client application.

Step 4: Request Authorization with Apple ID

Let's go back to Xcode.

First, we need to code the function to interface with the /authenticate endpoint we just created. Let's create a file named Authentication.swift and start by defining the request and response structures.


import Foundation

struct AuthRequest: Codable {
    let appleUid: String
    let appleAuthCode: String
    let name: String?
    
    func encoded() -> Data {
        try! JSONEncoder().encode(self)
    }
}

struct AuthResponse: Codable {
    let apiKey: String

Thanks to Codable, we can easily transform those objects into and from JSON, which comes in very useful as we define the actual function to interface with /authenticate by sending AuthRequest and receiving AuthResponse:

func authenticate(request: AuthRequest, 
                  completion: @escaping (AuthResponse) -> Void) {
    var urlReq = URLRequest(url: URL(string: "http://[your local ip]:4000/authenticate")!)
    urlReq.httpBody = request.encoded()
    urlReq.httpMethod = "POST"
    urlReq.addValue("application/json", forHTTPHeaderField: "Content-Type")
    urlReq.addValue("application/json", forHTTPHeaderField: "Accept")
    
    URLSession.shared.dataTask(with: urlReq) { data, response, error in
        completion(AuthResponse(data: data!))
    }.resume()
}

Reminder: Sign in with Apple will only work on a real device. Make sure to replace [your local ip] with your Mac's local network IP address, which is not localhost nor 127.0.0.1. To find it out, run the following command on a terminal in your Mac:

$ ipconfig getifaddr en0
192.168.0.11 // example output

Now, to authenticate the user with Apple, let's go back to AuthViewController.swift and edit the button's press handler to add this behavior:

@objc func handleSignInPress() {
    let provider = ASAuthorizationAppleIDProvider()
    let request = provider.createRequest()
    request.requestedScopes = [.fullName, .email]

    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

This implementation will make pressing the Sign in with Apple button trigger the Sign in with Apple screen.

Animation showing the Sign in with Apple button being pressed and the Sign in with Apple modal showing up

However, it won't compile yet, because we haven't conformed AuthViewController to a couple of required protocols.

We need conform AuthViewController to ASAuthorizationControllerPresentationContextProviding to tell Sign in with Apple which window to render the flow in:

extension AuthViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return self.view.window!
    }
}

Most importantly, we also need the conformance to ASAuthorizationControllerDelegate, which will let us receive the Apple credentials after the user completes the sign-in flow, which we can then use to authenticate, then configure our Stream client and move to the contacts screen:

extension AuthViewController: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        let cred = authorization.credential as! ASAuthorizationAppleIDCredential
        let code = String(data: cred.authorizationCode!, encoding: .utf8)!
        
        var name: String? = nil
        if let fullName = cred.fullName {
            name = PersonNameComponentsFormatter().string(from: fullName)
        }
        
        let request = AuthRequest(appleUid: cred.user, appleAuthCode: code, name: name)

        authenticate(request: request) { [weak self] res, error in 
            DispatchQueue.main.async {
                guard let res = res else {

Finally, we have a functioning Sign in with Apple implementation:

Animation showing user authenticating successfully with Sign in with Apple and being moved to the contacts screen

Since it's a new user, we don't have any contacts showing.

The backend logs should look similar to this:

[INFO] Sign in attempt with request: {"name":"Matheus Cardoso","appleUid":"001482.30f24b627a403ee4837b27a403ee6a22.1758","appleAuthCode":"ce73969641ba34969a5e69641ba349697.0.nruys.ElBakUhUlBakUhUZMB-xJQ"}
[INFO] User identity confirmed by Apple.
[INFO] User registered with email: matheus@cardo.so. Derived streamId: bWF0aGV1c0BjYXJkby5zbw@@
[INFO] User signed in successfully with response: {"apiKey":"zgtb7uugmvx3","streamId":"bWF0aGV1c0BjYXJkby5zbw@@","streamToken":"eyJhbGciOiJ5cCI6IkpXVInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiY1c2VyX2lkFjMEJqWVhKa2J5NXpid0BAIn0.1FqhMbQU70EB-i837w7oKcWLeon2FqhMbQU707cdSP8","email":"matheus@cardo.so","name":"Matheus Cardoso"}.

Wrapping up

Congratulations! You can now build Sign in with Apple authentication into any iOS app. Make sure to read Sign in with Apple's guidelines and documentation to keep up-to-date with the requirements and announcements. Also, for your apps that need social media or messaging features, check out Stream's documentation for activity feeds and chat messaging.

Thanks for reading and happy coding!

decorative lines
Integrating Video With Your App?
We've built an audio and video solution just for you. Launch in days with our new APIs & SDKs!
Check out the BETA!