DEV Community

Ibrahima Ciss
Ibrahima Ciss

Posted on

Sign-in with Apple: from client to server-side validation

I was working on an upcoming app called Charabia , and I needed some ways to log the user into the app. But I wanted that experience to be really smooth; I don't want to pop up a signup or login form. So I thought that Sign-in with Apple would be a good fit for that, so I added it plus the option to sign in with Google.
The logic is pretty simple. Once you tap on one of these buttons, you authenticate via the client SDK. For Sign in with Apple (or SIWA, I like to call it 😉), it's the AuthenticationServices framework that takes care of that, and for Google, I've installed the Google Sign-In Swift package. Once authenticated, both SDK will send you a token or authorization code you can send to your backend server for verification. That's the tricky part because it requires a lot of configuration beforehand in order to get all the different elements necessary for the validation. After a successful verification, depending on the user's existence in the database, we might create the user from the decoded pieces of information contained in the JWT token. If he doesn't exist yet, we create the record in the user table and send him back to the client with an access token he can use for subsequent requests on the RESTful API.
Let's see how to do all that in this article. I've separated this guide into three main sections:

  1. Client side setup
  2. Keys creation
  3. Server-side validation

Excited? Let jump right into it without any further ado.

Client-side setup

Let's first start by creating the app. It's a single app that uses SwiftUI.
Xcode project creation
For the sake of simplicity, we'll use a single file, the ContentView, to host all of our code (and it shouldn't be long 😅). iOS 14 added a convenient way to sign in with Apple with a view named SignInWithAppleButton that is directly configurable via its initializer.

SignInWithAppleButton(
  label: SignInWithAppleButton.Label, 
  onRequest: (ASAuthorizationAppleIDRequest) -> Void,
  onCompletion: ((Result<ASAuthorization, Error>) -> Void)
)
Enter fullscreen mode Exit fullscreen mode

The onRequest and onCompletion arguments are both closures, the first to configure the request and the second to process the result. I'll put these methods in a ViewModel to decouple the view from the logic. It looks like this:

import SwiftUI
import Combine
import AuthenticationServices


final class ViewModel: ObservableObject {

  func handle(request: ASAuthorizationAppleIDRequest) {
  }

  func handle(completion result: Result<ASAuthorization, Error>) {
  }

}


struct ContentView: View {
  @StateObject private var viewModel = ViewModel()

    var body: some View {
      VStack {
        Spacer()
        Text("👋 Hello SIWA")
          .font(.title)
        Spacer()
        SignInWithAppleButton(
          .continue,
          onRequest: viewModel.handle(request:),
          onCompletion: viewModel.handle(completion:)
        )
        .frame(height: 48)
      }.padding()
    }
}
Enter fullscreen mode Exit fullscreen mode

For the authorization request, let ask for user email and the fullName.

func handle(request: ASAuthorizationAppleIDRequest) {
  request.requestedScopes = [.fullName, .email]
}
Enter fullscreen mode Exit fullscreen mode

Go to the project settings and select your main target; in "Signing & Capabilities" tab, add the "Sign in with Apple" capability.
Sign in with Apple capability
Now you can run the app. Note that you have to use a physical device to test SIWA; there is an unresolved bug since iOS13 simulators.
Anyway, it should look like this.
Sign in with Apple project demo
Once the button is tapped, you'll have the SIWA popup and you can either use your email or a private one that'll forward to your real email and set a name as well.
Be aware; this popup will appear only once before you accept it via Face-Id or Touch-Id; for subsequent login, you'll have another popup for authorization. Thus, if you want the user pieces of information like his name, you have to handle it the first time the popup is shown. But if you messed up, I will show you how you can later reset the popup and show it like it was the first time.
Now, once we accept (or deny), the handle completion method is fired, and we can send the authorization code provided to our backend for validation.

final class ViewModel: ObservableObject {

  func handle(request: ASAuthorizationAppleIDRequest) {
    request.requestedScopes = [.fullName, .email]
  }

  func handle(completion result: Result<ASAuthorization, Error>) {
    switch result {
      case .success(let authorization):
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
        let tokenData = credential.authorizationCode,
        let token = String(data: tokenData, encoding: .utf8)
        else { print("error"); return }
        send(token: token)
      case .failure(let error):
        print(error.localizedDescription)
    }
  }

  private func send(token: String) {
    guard let authData = try? JSONEncoder().encode(["token": token]) else {
      return
    }
    let url = URL(string: "https://yourbackend.example.com/tokensignin")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in
      // Handle response from your backend.
    }
    task.resume()
  }

}
Enter fullscreen mode Exit fullscreen mode

That's all we need to do for our client. That's pretty simple, right. Now let's handle the public and secret keys configuration that'll help us validate the token generated by the client.

Keys Creation and configuration

Now let's create the keys (public and secret) we need to verify the client's authorization code.

  1. Go to developer.apple.com and log into your account
  2. Click on “Certificates, IDs & Profiles” on the left sidebar
  3. Once the page loaded, go to the “Keys” menu to create a new key
  4. Click on the “+” icon, then put the name of the key, I am going to call it siwatut
  5. Check the “Sign in with Apple” checkbox and click on the “Configure” button
  6. It’ll redirect you to a key configuration page, and you’ll see a select textfield for picking the app you want to create key. Choose you app and click on the “Save” button.
  7. It’ll redirect you back to the key configuration page, now click on the “Continue” button
  8. You’ll see a summary of key creation, click on “Register” to create the key
  9. Now, step really important, you have to click on the “Download” button in order to save the key locally on your computer. I recommend to save it somewhere safe in your disk. Sign in with Appe - Private Key
  10. Rename the private key AuthKey_keyid.p8 to key.txt
  11. Now, open your terminal and go to the location of key.txt
  12. Install the JWT Gem with sudo gem install jwt
  13. Once done, create a file called client_secret.rb to process the private key and open it in your favorite text editor
  14. Put that content in it:
require 'jwt'

key_file = 'key.txt'
team_id = 'your-team-id'
client_id = 'dev.ibrahima.siwa-tut'
key_id = 'your-key-id'

ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file

headers = {
'kid' => key_id
}

claims = {
    'iss' => team_id,
    'iat' => Time.now.to_i,
    'exp' => Time.now.to_i + 86400*180,
    'aud' => 'https://appleid.apple.com',
    'sub' => client_id,
}

token = JWT.encode claims, ecdsa_key, 'ES256', headers

puts token
Enter fullscreen mode Exit fullscreen mode
  • team_id can be found on the top-right corner when logged into your Apple Developer account
  • client_id is our app’s bundle identifier
  • key_id is the private key identifier created at step 9 above
  • Save the file, go back to your terminal and type the command
ruby client_secret.rb
Enter fullscreen mode Exit fullscreen mode

If everything goes well, it should print a JWT token on the terminal.
Sign in with Appe - JWT Key
Copy and save it somewhere in the meantime, and we will use it in our backend.
Now that we have generated our JWT token, let's process it in our backend to extract the pieces of information we need.

Server-side validation

For the backend, I’m going to use Laravel PHP framework. You can, of course, use any server-side framework (Express, Django, Rails, etc.) of your choice. I already have a fresh installation of Laravel.
Now I'll copy the token we generate earlier in the .env, an environment file like so:

SIWA_CLIENT_ID=dev.ibrahima.siwa-tut # your xcode bundle identifer used to generate the token
SIWA_CLIENT_SECRET= # the jwt token generated earlier
SIWA_GRANT_TYPE=authorization_code # used in the validation request
Enter fullscreen mode Exit fullscreen mode

Now let's add an endpoint that the client'll hit. I'll go to the web.php route file, and I create a controller for handling the request.

// web.php
Route::post('tokensignin', [AuthController::class, 'handleSIWALogin']);
Enter fullscreen mode Exit fullscreen mode
// AuthController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
use GuzzleHttp\Exception\GuzzleException;


class AuthController extends Controller
{

    public function handleSIWALogin()
    {
        $authorizationCode = request()->input('token'); // 1
        $body = 'client_id=' . env('SIWA_CLIENT_ID') . '&client_secret=' . env('SIWA_CLIENT_SECRET') . '&code=' . $authorizationCode . '&grant_type=authorization_code';
        $client = new Client();
        $request = new Request("POST", "https://appleid.apple.com/auth/token", ["Content-Type" => "application/x-www-form-urlencoded"], $body); // 2
        try {
            $response = $client->send($request); // 3
            $data = json_decode($response->getBody(), true);
            $payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', explode('.', $data['id_token'])[1]))), true); // 4
            if ($payload['email']) return $this->createOrLogUser($payload); // 5
            return $this->respondWithError("Could not authenticate with this token");
        } catch (GuzzleException $e) {
            return $this->respondWithError($e->getMessage()); // 6
        }
    }

    private function createOrLogUser($payload): JsonResponse
    {
        // create the user if he's not yet in the database or return him with (if he already exists) with a token and send it back to the client
    }

    private function respondWithError($message): JsonResponse
    {
        // return a json response representing an error
    }
}

Enter fullscreen mode Exit fullscreen mode

Don't be intimidated by the code above, and it's pretty straightforward. Let's see the different steps needed to verify the token:

  1. We retrieve the token sent by the client
  2. We construct a request that'll be sent to Apple's server for authenticating the token. You'll find here the list of parameters
  3. We send the request
  4. If the request was successful, we extract the payload with the json_decode method in the id_token field
  5. With the email extracted, we try to log the user or create him in the database, then send the authentication token to the client
  6. If something is wrong, we return an error. The client should be logged in with the token generated by the backend that'll serve for authentication for the subsequent requests. You can print the authorization code on Xcode for testing purposes and make a post request to apple servers with all your credentials created earlier. The result should look like this: Token validation test

Resetting Sign in with Apple authorization popup.

As I told you before, the authorization popup where you give your name and decide to share or use a private email will show once. After accepting, there will be another popup for subsequent authorization. I know that you might want to retrieve all the user data like his name for testing, but if you haven't done that the first time, you have no chance to do it later. You can reset the Sign in with Apple authorization popup by following these steps.

  1. Go to the Settings app of your phone
  2. Select your iCloud profile at the top
  3. Go to "Password & Security"
  4. Go to "Apps using Apple ID"
  5. Select the concerned app and tap on "Stop using Apple ID" That's it. Next time you'll want to authorize, it'll bring you the original popup giving you the option to share or not your real email and to customize your name.

Final Thoughts

As you can see, validating the authorization code provided by Sign in with Apple isn’t difficult, it just requires some setup that can be tedious, I know 😔. For more informations, I recommend reading the docs about user verification and token validation from Apple docs:

Top comments (0)