ELI5: “Sign in with Apple” for React Native using Expo SDK35 & Node.js

appandflow

App & Flow

Posted on January 23, 2020

ELI5: “Sign in with Apple” for React Native using Expo SDK35 & Node.js

alt text

Thanks to Vincent de Lafontaine, without his help this article wouldn’t exist.

We too usually skip to the code snippets, so we won’t judge if you do. Just keep in mind that there’s a bunch of configuration to do on the Apple Developer side of things.

The Expo documentation regarding “Sign in with Apple” is pretty straightforward : you create a button, it calls a function, the user puts in their Apple info, and la dee da you are authentified.

Here’s the button almost exactly as what Expo shows you

<AppleAuthentication.AppleAuthenticationButton
  buttonType={
    AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN
  }
  buttonStyle={
    AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
  }
  cornerRadius={5}
  style={{
    width: 150,
    height: 40,
    marginRight: 20,
  }}
  onPress={this._loginWithApple}
/>

note that you’ll have to pass it a width and a height, otherwise the button won’t show up (that’s the official workaround for now)

When you press the button, it should call this :


applePayload = await AppleAuthentication.signInAsync({
  requestedScopes: [
    AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
    AppleAuthentication.AppleAuthenticationScope.EMAIL,
  ],
});

signInAsync returns a promise that resolves to an object containing an authorizationCode(among other things). This is what you want to send to your backend.

But what then? What do you have to do on the backend to handle this information?

You know the drill : your route gets an access token from the provider, you send the access token to their API, if valid it returns an object containing some info about the user such as name, email, etc. that you can use in your login or signup flow. You’ve already done it for Google, Facebook, Twitter, Myspace (yeah, no).

It should be easy.

Well it ain’t. Apple said “screw this, it’s not good enough for us” and did things a bit differently. You know Apple, they always need to do things differently.

Here are a few things to know about the apple sign in before we start :

  • If you want to validate your authorizationCode with Apple, you need to be enrolled in the Apple Developer Program (💰).

  • The user can decide to “Sign in with Apple” and choose not to share their email address, in which case you’ll receive a proxy email instead.

  • Apple will only send you back the user’s email (or proxy email) the first time a specific user authentifies themselves. This means that if the user logs out and logs back in, you won’t get their e-mail the second time around. So keep in mind that if you need the user’s email, you should persist it.

  • The whole frontend part of this flow will only work on standalone builds. It will fail if you try it within Expo.

  • “Sign in with Apple” only works for iOS 13, and the button will only appear if that condition is met.

To give you a bigger picture, these will be the steps to follow (all these points will be explained in detail later) :

Pre step : Do all the configuration on the Apple Developer portal

Step 1 : Create a route on your backend that will receive the payload
obtained from the Expo AppleAuthentification module (as shown previously in the signInAsync example). This payload contains an authorizationCode

Step 2 : Create your own json web token (jwt) which will be your clientSecret

Step 3 : Authenticate your authorizationCode

If successful, Apple will send you back a tokenResponse.

Pre step : Start with all the Apple Developer config stuff

  • Login on the Apple Developer portal

  • Go to Certificates, Identifiers & Profiles

  • Select Identifiers, choose your app and check the “Sign in with Apple” capability. If it’s your first time doing this, you’ll need to create a new app identifier. Make sure to use the same bundle id as in your app.json

  • Go to keys and create a new key, give it a pertinent name and make sure to check “Sign in with Apple”. Download the key when you’ll have the opportunity and keep it somewhere safe

Devs who implement “Sign in with Apple” on web have a few more steps to follow, but we don’t need to worry about them #mobileDevMasterrace (just kidding.)

You should then have the following items :

  • Team Id : you can find it if you go to Membership details, but it’s also on the app’s Identifier page

alt text

  • Key Id : You can get it in the Keys section you just created

alt text

  • A secret key file that has a “.p8” extension

  • Not related to the Apple Developer stuff, but make sure you also have a bundle id (the same as in your app.json file).

Step 1 : Your backend receives a payload from your mobile app

Not much to do here other than get the attribute authorizationCode out of the payload.

const { authorizationCode } = req.body.applePayload;

Step 2 : Create a clientSecret jwt

This part draws inspiration directly from this article (thanks Arjun Komath!)

You’ll need your key id, your team id and your bundle id, as well as your secret key file.

In the following code snippet, I’ll be using fs-extra as a drop-in replacement for the module fs from node. The advantage of fs-extra over fs is that readFile is already promisified and can be awaited.

import fs from 'fs-extra';
import jwt from 'jsonwebtoken';

const getClientSecret = async () => {
  const privateKey = await fs.readFile(process.env.PRIVATE_KEY_FILE_PATH);
  const headers = {
    kid: process.env.APPLE_KEY_ID,
    typ: undefined,
    alg: 'ES256',
  };
  const claims = {
    iss: process.env.APPLE_TEAM_ID,
    aud: 'https://appleid.apple.com',
    sub: process.env.APPLE_BUNDLE_ID,
  };
  const token = jwt.sign(claims, privateKey, {
    algorithm: 'ES256',
    header: headers,
    expiresIn: '24h',
  });
  return token;
};

Step 3 : Validate your authorization code

This part is flexible, feel free to fetch in whichever way you prefer. The idea is just to pass it authorizationCode, clientSecret and clientId (=== your bundle id). Also grant_type=authorization_code as we’re validating the authorization code.

const urlBody = `code=${authorizationCode}&client_secret=${clientSecret}&client_id=${clientId}&grant_type=authorization_code`;

const res = await fetch(`https://appleid.apple.com/auth/token`, {
  method: 'POST',
  body: urlBody,
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
});

That’s it. You either get a 200 and a tokenResponse or a 400.

“But if Apple doesn’t give me the user’s email every time, how do I keep track of users?”

Both the payload sent from your mobile app and the token sent by Apple contain an identity token (the expo module calls it identityToken while Apple calls it id_token). These tokens can be decoded using jwt.decode. Once decoded, they contain a sub attribute which is unique to each user. So you can simply add a sub field to your user, and validate them that way.

Further reading

Most helpful post on the subject : How to setup sign-in with apple — Arjun Komath

For more info regarding the Apple Developer set-up part : What the heck is sign in with Apple — Aaron Parecki

Apple documentation on validating tokens

💖 💪 🙅 🚩
appandflow
App & Flow

Posted on January 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related