React Native: Sign in with Apple

How to integrate Sign in with Apple for React Native apps

Sign in with Apple was released alongside iOS 13 and provides a simple and private way for users to authenticate with their apps and websites that support the service. With Sign in with Apple, users can essentially use their Apple ID as the trusted identifier for apps and websites, removing the need to create a new account with the app in question.

This article walks through the process of integrating Sign in with Apple in the context of React Native, and will unravel the code behind this seamless authentication mechanism. A couple of key packages will be used to facilitate integrating Sign in with Apple, namely react-native-apple-authentication for the React Native side and apple-signin-auth for the Node.js server side.

Before jumping into the code needed to get this service up and running in React Native, the next section breaks down exactly what to expect from the Sign in with Apple service from both a user standpoint and development standpoint. This will give us enough context to go ahead and integrate the service.

How Sign in with Apple Works

Users and developers will have very different perspectives on the service. The user experiences a quick and frictionless authentication process whereas us developers have the more challenging job of understanding the APIs, setting up the credentials needed, understanding the service workflow, and implement that workflow as React Native components and server-side logic with Node.js.

The above may sound like a lot of work, but in reality a clear understanding of the process will aid in a swift implementation — that is exactly what this article will enable.

The experience on the user end is rather simple and elegant if implemented correctly. All the user is required to do is tap a “Sign in with Apple” button (that follows Apple’s style guidelines) which then summons a Touch ID / Face ID prompt to authenticate. A successful authentication should then sign the user into the app without any further actions required. When a user is carrying out this process for the first time, they have the option to share or hide their name and email address to the app in question.

The follow diagram represents the stages of Sign in with Apple with an app that I personally develop. Tapping the Sign in with Apple button prompts authentication with Touch ID. Once successfully authenticated, the user signs in and is redirected to their dashboard:

The Sign in with Apple process. Tap sign in button -> authenticate -> persist updates and redirect to dashboard

As a way of finding middle ground between giving or hiding your email address, Sign in with Apple gives users the option to use a random apple-owned email address that is generated and associated with the user’s Apple ID. The benefit to doing this is that users can still receive communications from the app via email, only emails will be sent to this Apple managed address before they are forwarded to the user’s actual email address. These “private relay” email addresses come in the format of <random_string>@privaterelay.appleid.com.

It is our responsibility as developers to provide trusted domain names and email addresses to Apple in order to reach users from Apple’s private email relay addresses. All these details can be configured on the Developer Portal. We’ll briefly cover this process here, and also link to other sources that have outlined the process too.

From a developer perspective, the authentication process generates some key pieces of data that we need to process and verify server-side. An identity token is generated from Apple’s side along with an account identifier representing the user’s Apple ID when a user authenticates. This identifier is not the user’s Apple ID itself (the email address you use to sign in to your Apple account), but rather a randomised string that can be associated with the Apple account being authenticated. These pieces of data can be persisted in the app’s database for future authentications.

A different unique identity token is generated every time the user authenticates, but each one will be associated with the same account identifier.

So when an authentication happens in React Native, an identity token will be generated along with other metadata associated with it. What we’ll be doing in this walkthrough is then sending that data to the server-side where the identity token will be verified. When an identity token is successfully verified, the account identifier is returned in the verification response body which can then be used to either create the new account or fetch an existing account within your app database.

As an additional consideration, it is most likely that your app already has its own authentication mechanism in place and generating its own authentication tokens — and this is perfectly fine. In fact, what this article demonstrates is how to use Sign in with Apple alongside your existing authentication tokens.

Once an identity token is verified, your server can create its standard authentication token that’ll be used to authenticate further API requests from the user.

With this understanding we can now begin setting up the credentials and Xcode capabilities needed to get Sign in with Apple working.

Xcode and Developer Credentials Setup

The first port of call is to set up the needed credentials and Xcode capabilities to get Sign in with Apple working. The bulk of this setup happens within your Developer Portal; that is what this section will primarily focus on. There is also one capability needed to be added to your Xcode project.

Within Xcode, head to Signing and Capabilities tab of Xcode, press + Capability and add the Sign in with Apple capability. Save your Xcode project and head to your Developer Portal.

The Sign in with Apple capability in Xcode

There are a few Sign in with Apple related credentials that can be configured from the Developer Portal. Starting with the capability added from Xcode in the previous step, you can now visit the App ID Configuration settings from your Identifiers. Scroll down to the Sign in with Apple Capability and click Edit.

The Sign in with Apple Capability within App ID Capabilities

The resulting modal reveals that your App ID has been assigned as a Primary App ID, and you can use this App ID to sign in from any build of your app, whether that be for iOS, Mac or the web. The downside of doing this is that there will be no differentiation between those platforms, or no context of where the user is signing in from.

To resolve this issue, one can also group other App IDs (or Service IDs in the web app case) with a Primary App ID. Doing so will still only require one consent from the user for the entire group, rather than consenting to each of the grouped IDs.

Service IDs are also configured in your Identifiers section, and are used if you are configuring Sign in with Apple for a website. Apps simply use their App ID to associate themselves with Sign in with Apple. You may see some tutorials online directing you to set up a Service ID, but this is not needed if you’re solely configuring your app for Sign in with Apple. To view your Service IDs, click the App IDs dropdown at the top right of the page and select Service IDs.

In the context of Sign in with Apple, the Service ID and App ID is also referred to as the Client ID. You may see this terminology used in documentation online, so just know this is referring to either the App ID or Service ID that acts as the Sign in with Apple domain identifier.

For this piece we will only focus on the Primary App ID, so there will not be any further configuration required here. Although If the reader wishes to expand their ecosystem to the web or another platform, they can simply switch the newly created ID to Group with an existing primary App ID option, and choose a Primary App ID from the dropdown provided.

Also notice that a Server to Server Notification Endpoint URL can be configured here, where you can receive notification style events about Sign in with Apple activity. Again, this is not critical to getting the service working, but may be useful for analysing usage or verifying the service is working as expected.

Go to your Keys list and add a new key for Sign in with Apple. Add your (Primary) App ID to the key and finally Register the key. Be sure to download the private key and store it in a safe place.

The Keys section header of Certificates, Identifiers & Profiles

The last required setup step here is to configure email communication. This is solely for enabling communication with the private email relay service mentioned in the introduction (or when a user decides to hide their real Apple ID email address and use a randomly generated Apple owned email instead). Ideally, we as app developers still want to reach the user via email, especially for critical updates, so this added step is vital to maintain that communication.

Within your Certificates, Identifiers & Profiles, click More from the side menu and click Configure to open the email settings.

Where to configure Sign in with Apple for Email Communication within Developer Portal

Add the domains and email addresses you plan to use. For example, mydomain.com as the domain and no-reply@mydomain.com as an email address.

Also note that private email relay notifications are turned on by default. Toggle this setting from the Settings button.

These sources also need to be authenticated to prove their ownership, and the easiest way to do so is via SPF Authentication.

Apple outline instructions to do this for a range of mail service providers (SendGrid, MailChimp, etc) — expand Authenticating your Domain from the preceding link to see those details.

I used SendGrid as my app’s email service provider the when building the app related to this piece. To configure SPF authentication with SendGrid, simply add the following TXT record to your domain:

v=spf1 include:sendgrid.net ~all

Clicking Reverify SPF shortly after you’ve added the SPF record will successfully verify the source.

The above rule will enable SendGrid to send email from all subdomains of the record’s associated domain. There are plenty of articles online explaining how to configure SPF rules — search for “configuring SPF rules” to get the most up to date sources.

This setup certainly is not trivial, and requires understanding of how identifiers and keys are used together. If the reader found this section was difficult to follow, react-native-apple-authentication also includes instructions with screenshots in its docs/ folder — that can be viewed here.

We are now fully configured and ready for the React Native side of the implementation.

Signing in from React Native

This section will focus on implementing the Sign in with Apple button and its onPress handler that will generate the identity token along with its meta data. A Gist of the complete React component talked about here is included at the end of this section.

The package being used to facilitate this is @invertase/react-native-apple-authentication. Add it as a project dependency to gain access to its APIs:

yarn add @invertase/react-native-apple-authentication

The button display itself is straight forward; it is packaged in an AppleButton component and supports props to define its type and style. The button types provided change the button text, whereas the style themes the button, supporting light or dark themes, as well as an outline theme.

Define the Sign in button like so — we have also imported other members of react-native-apple-authentication as we’ll be using them in the button’s onPress handler function:

// displaying the Sign in with Apple button componentimport appleAuth, {
AppleButton,
AppleAuthError,
AppleAuthRequestScope,
AppleAuthRequestOperation,
} from '@invertase/react-native-apple-authentication'
const AppleAuth = (props) => { const onAppleButtonPress = async () => {
/* Sign in logic will go here */
}

return (
<AppleButton
buttonStyle={AppleButton.Style.WHITE}
buttonType={AppleButton.Type.SIGN_IN}
style={styles.appleButton}
onPress={() => onAppleButtonPress()}
/>
);
}

If you are targeting multiple platforms, use the appleAuth.isSupported boolean to determine whether to display the sign in button. This will ensure it will not be displayed on Android, for example.

Note that the button’s width and height are defined within the style prop. The button will also respond to margins and even support a shadow:

// button stylingappleButton: {
width: '100%',
height: 45,
shadowColor: '#555',
shadowOpacity: 0.5,
shadowOffset: {
width: 0,
height: 3
},
marginVertical: 15,
}

It is recommended to experiment with the styling of the button to match your existing theming as much as possible. Font size, border radius, and colours cannot be edited beyond the buttonStyle prop— this is most likely due to Apple’s styling guidelines. Where necessary, adjust your existing theming to match the Sign in button to make up for its lack of customisability.

Let’s turn our attention to the onPress handler function now, that we’ve named onAppleButtonPress. Firstly, all logic should be wrapped in a try catch block, where AppleAuthError exceptions can be handled. This is where logic pertaining to the user cancelling a sign in should be defined:

// try catch block to handle sign in exceptionsconst onAppleButtonPress = async () => {
try {

} catch (error) {
if (error.code === AppleAuthError.CANCELED) {
// user cancelled Apple Sign-in

} else {
// other unknown error
}
}
}

Now we can make a sign in request within the try block. This sign in request will invoke the sign in prompt built into iOS where the user can then authenticate using a method such as Touch ID or Face ID. In the event this prompt is cancelled, the exception we defined above will be called. A successful authentication on the other hand will return an identityToken, among other details such as the account identifier named appleUserId , a nonce associated with the identityToken, and other details.

The following example request includes two scopes for the user’s email address and name respectively, meaning these credentials will be requested if the user is signing in for the first time. Recall that there is no guarantee that the user will provide these details, and they can also choose the private email relay at this point. This stage is also called the consent stage.

In any case, make the asynchronous request like so:

// sign in request const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: AppleAuthRequestOperation.LOGIN,
requestedScopes: [
AppleAuthRequestScope.EMAIL,
AppleAuthRequestScope.FULL_NAME
],
});
const {
identityToken,
} = appleAuthRequestResponse;

We now have everything we need to continue the verification and sign-in process server side.

Although other metadata is returned in appleAuthRequestResponse, such as the user’s agreed scopes, we will only be interested in the identityToken to continue authentication in this walkthrough. As we’ll see, verifying the token server-side will also return key values such as the appleUserId, that’ll enabled an identityToken to be linked to an account.

If your app does not have a server-side API and is totally relying on Sign in with Apple exclusively, then this response is enough to verify the user successfully authenticated. However, this is definitely an edge case — just about all services will have account details persisted server-side and their own authentication mechanisms in place. To cater for the majority of use cases, we’ll be sending the identityToken (securely) server-side where it can then be verified and integrated into an existing authentication flow.

At this stage you may wish to use the other returned values, especially the email and fullName fields, to update your UI and component state or persist these values on-device. This article will continue its focus on the authentication flow, but acknowledges that further processing can be done at this stage.

The last thing onAppleButtonPress needs to do is make that request to your server and handle either a successful or failed verification response.

The following code sends the entire appleAuthRequestResponse to the server, but we will primarily be interested in using identityToken and appleUserId values for verification in the next section. Upon verification, your server should respond with a success or failure message:

// request to verify identity tokenif (identityToken) {  // sending entire auth response to server in `fetch` request
const { ack, response } = await authFetch({
uri: '/sign-in-with-apple',
body: {
...appleAuthRequestResponse,
}
});

if (ack === 'success') {
// successful request, process sign in
// state updates / Redux dispatches / etc
handleSignIn(response);
} else {
// failed to verify `identityToken`
handleFailedSignIn();
}
} else {
// no `identityToken`, also a failed sign in
handleFailedSignIn();
}

authFetch is a function I use in development that wraps a vanilla fetch request with an asynchronous function, providing simplified boilerplate.

Here is a Gist that collates this entire section in a React component:

The next section will document how to use another package in a Node.js Express environment to verify the identityToken and how to associate it along with the account identifier, appleUserId, to a user account.

Server-Side validation

Now let’s verify the identityToken. Another package is used here, apple-signin-auth. Go ahead and add this to your project:

yarn add apple-signin-auth

Let’s name the endpoint /sign-in-with-apple. We’ll firstly wrap its logic in a try catch block and reference the variables sent from the app:

// sign-in-with-apple endpointconst appleSignin = require('apple-signin-auth');router.post('/sign-in-with-apple', async function (req, res, next) {
try {
const { body } = req;
const {
email,
fullName,
identityToken
} = body;
} catch (e) {
next(e);
}
});

The appleSignin.verifyIdToken() method will be used to verify the identityToken. The following example will attempt to verify:

// verifying the provided identityTokentry { const clientId = <your_app_id>;  // verify token (will throw error if failure)
const { sub: userAppleId } = await
appleSignin.verifyIdToken(identityToken, {
audience: clientId,
ignoreExpiration: true, // ignore token expiry (never expires)
});
} catch (e) {
res.json({
ack: 'error',
message: 'failed to verify identityToken'
});
next(e);
}

Now we are getting somewhere. The verifyIdToken() method, if successful, will return the appleUserId associated with that identityToken. Here are the points of interest about this method:

  • We have supplied a clientId value to an audience property. This clientId is the App ID associated with Sign in with Apple configured earlier. The name clientId is used here to stay consistent with the official documentation of apple-signin-auth.
  • The ignoreExpiration property is set to true, which means this token will verify even if it has expired. This is useful when implementing Sign in with Apple in conjunction with your own authentication mechanisms, whereby you are not subject to mechanisms external to your authentication process.
  • We’ve defined an additional try catch block here to isolate verification errors, and send a fail response back to the app in the event it is not successfully verified.
  • As a side note, the official docs also outline a nonce property. A nonce was also generated on the React Native side and sent to the server as nonce, too. However, when providing this nonce value to verifyIdToken(), the token is not successfully verified. It may seem intuitive to provide this nonce value, but doing so does not seem to be supported when verifying an already-generated identityToken. Keep this in mind as to not be confused about this ambiguity.

With an appleUserId returned from a successful verification, one final check can be made relative to the appleUserId sent to the server, which should match the generated one:

// checking apple User IDs matchif(appleUserId === req.body.appleUserId) {
//details match, continue with sign in process
}

Note that the appleUserId will be consistent throughout subsequent sign ins, but the identityToken will not be.

You are now free to authenticate the user, and also create a new account if this is the first time the user is authenticating. The following details are now available to persist in a newly created account:

  • The fullName field, that provides the user’s full name and a nickname, if provided.
  • The email field, which could either be null, a private relay email or the user’s real Apple ID email, depending on how they chose to share it.
  • The appleUserId field, that can act as an identifier for a particular apple user. This can be seen as a username, and should be used as the account identifier over email that could indeed be null.

To conclude this section, you may wish to adhere to the following logic flow to complete your endpoint implementation:

// logic flow for verification and app sign in endpoint receives request
|_verify identityToken
|_ return failure if not successful
|_ check appleUserId's match
|_ return failure if they do not match
|_ check account with appleUserId exists
|_ create account if account does not exist
|_ generate authentication token via standard means
|_ persist token to user account record
|_ return token and account details to app

If you wish to read up on how to code your in-house authentication system to cater for standard email address and password with authentication token handling, check out my article dedicated to doing so: React Native: User Authentication Flow Explained.

Responding to Revoking Access

Also worth touching on briefly is an event listener that react-native-apple-authentication provides to respond to a user revoking access to your app. The following useEffect hook will initialise and unmount this event listener, allowing you to react to such a scenario with actions such as signing the user out of their account:

// responding to a user revoking accesslet authCredentialListener = null;
useEffect(() => {
authCredentialListener = appleAuth.onCredentialRevoked(async () => {
//user credentials have been revoked. Sign out of account
});
return (() => {
if (authCredentialListener.remove !== undefined) {
authCredentialListener.remove();
}
})
}, []);

In Summary

This piece has attempted to outline the process of integrating Sign in with Apple in the context of React Native. Although the process entails a somewhat cumbersome process of setting up all the credentials in the Developer Portal, the React Native and Node integration is relatively straight forward thanks to the simple APIs that react-native-apple-authentication and apple-signin-auth provide.

The end result of this integration is a drastically more simplified registration and sign in process for your end users, which will likely increase sign up rates for apps that require a sign up process to access content. The registration form is no longer needed for users who opt to use Sign in with Apple, that significantly decreases the barrier to your app content.

The downside to this approach is the lack of user information you can capture in this initial sign up process. This restriction can be alleviated by prompting the user for further info as and when the app needs it. This approach has certainly gained momentum since the advent of Sign in with Apple, suggesting users in general do have sign-up fatigue and appreciate less verbose authentication processes.

For queries and concerns, please feel free to post in the comments section and I will be sure to reply.

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS and Android.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store