DEV Community

Cover image for HOW-TO: Sign in with Apple on React and React-Native using Node
Sunim
Sunim

Posted on • Updated on

HOW-TO: Sign in with Apple on React and React-Native using Node

Dealing with Apple is a pain, ask any devs around. So I'll be writing a quick guide to setup Apple Sign in a couple of minutes (depending on Apple 🤷‍♂️)

Apple Sign-in Gif

├── Setting Up Developer Portal
├── Setting Up React.js
├── Setting Up Node
├── Setting Up React-Native

Setting Up Developer Portal:

First, sign in to the Apple Developer Portal and enroll if you have not already, and then click on Certificates, Identifiers and Profiles.

Certificates

Setting up App ID

From the sidebar, choose Identifiers then click the blue plus icon.

Identifiers

PS. If you have already set up your react-native-app you'll see a different screen. Choose the right Bundle ID only and go to Adding Sign-in Capability.

Select Previous AppID

Choose App ID

Now, Choose App IDs in this first step.

App IDs

Now, choose a description and Bundle ID for the App ID. It is best when it’s a reverse-dns style string. So my suggestion, if www.example.com is your web-app URL, com.example.app can be your Bundle ID.

Bundle ID

Add Capability

You’ll also want to scroll down through the list of capabilities and check the box next to Sign In with Apple and Enable it as Primary App ID.

Enable Sign-In

Setting up Service ID

Now Go ahead and create a new identifier and choose Services IDs.

Services IDs

In the next step, you’ll define the name of the app that the user will see during the login flow, as well as define the identifier which becomes the OAuth client_id.
You can set it as app.example.com as com.example.app is used for Bundle ID.

PS. Make sure to also check the Sign In with Apple checkbox.

check the Sign In

You’ll also need to click the Configure button next to Sign In with Apple in this step. This is where you’ll define the domain your app is running on, as well as define the redirect URLs used during the OAuth flow.

Redirect URLs

PS. Apple doesn’t allow localhost URLs in this step. You have to use a real domain here or use Ngrok.

Go ahead and click Save and then Continue and Register until this step is all confirmed.

Setting up Private Key

You actually have more steps to do generate Key. Either follow the guide by Aaron Parecki or hop on:
Back in the main Certificates, Identifiers & Profiles screen, choose Keys from the side navigation.

Keys

Click the blue plus icon to register a new key. Give your key a name, and check the Sign In with Apple checkbox.

Sign-in checkbox

Click the Configure button and select the primary App ID you created earlier.

Configure

Apple will generate a new private key for you and let you download it only once. Make sure you save this file because you won’t be able to get it back again later. Then, press Done.

Download Key

Setting Up React.js:

Damn, long process right? Bare with me, now is the easier part. The first thing you need to do is add the apple script. We use scriptjs for that.

Init AppleID

scriptjs.get('https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js', () => {
  const params = {
    clientId: 'app.example.com',
    redirectURI: 'https://www.example.com/apple/callback',
    scope: 'name email',
  };
  window.AppleID.auth.init(params);
});
Enter fullscreen mode Exit fullscreen mode

SignIn AppleID

Next, create a button with onClick function <button onClick={() => window.AppleID.auth.signIn()}/>

App Sign-in Button

It'll open the Apple View, it's different on Safari however, but I'll be attaching the Screenshot of other browsers here:

Sign-In Screen

Apple Permission Screen

Now, after continuing, it'll forward a POST request 🤦‍♂️. We know to handle it inside React/Next is a task on its own.

Setting Up Node:

So, we define the route /apple/callback on the backend and redirect to /app route being handled in the frontend.

app.use('/apple/callback', function(req, res) {
  console.log(req.body);
});
Enter fullscreen mode Exit fullscreen mode

Handling JWT

We have the id_token and code inside body. If you want to validate the user using code, please follow the guide by Arjun Komath
cause we'll only be using id_token.

You have two ways of getting the user. Since the id_token is just a jwt we can:

  • Extract the JWT
  • Validate the JWT

Extract JWT

const getAppleUserId = async token => {
  const parts = token.split('.');
  try {
    return await JSON.parse(new Buffer(parts[1], 'base64').toString('ascii'));
  } catch (e) {
    return null;
  }
};

app.use('/apple/callback', function(req, res) {
  const user = await getAppleUserId(req.body.id_token);
  console.log(user);
});
Enter fullscreen mode Exit fullscreen mode

Validate the JWT

import axios from 'axios';
import NodeRSA from 'node-rsa';
import jsonwebtoken from 'jsonwebtoken';

async function _getApplePublicKeys() {
  return axios
    .request({
      method: 'GET',
      url: 'https://appleid.apple.com/auth/keys',
    })
    .then(response => response.data.keys);
}

const getAppleUserId = async token => {
  const keys = await _getApplePublicKeys();
  const decodedToken = jsonwebtoken.decode(token, { complete: true });
  const kid = decodedToken.header.kid;
  const key = keys.find(k => k.kid === kid);

  const pubKey = new NodeRSA();
  pubKey.importKey(
    { n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') },
    'components-public'
  );
  const userKey = pubKey.exportKey(['public']);

  return jsonwebtoken.verify(token, userKey, {
    algorithms: 'RS256',
  });
};

app.use('/apple/callback', function(req, res) {
  const user = await getAppleUserId(req.body.id_token);
  console.log(user);
});
Enter fullscreen mode Exit fullscreen mode

Where,

  • _getApplePublicKeys is just fetching the multiple keys from https://appleid.apple.com/auth/keys.
  • Decode the id_token to extract the kid and extract the exact key which matches the kid of id_token.
  • Build the RSA and verify the jwt.

Forwarding to Frontend

Then, you can forward the user details by sending the data as:

app.use('/apple/callback', function(req, res) {
  const user = await getAppleUserId(req.body.id_token);
  res.redirect(303, 'https://www.example.com/app?user=${JSON.stringify(req.body.id_token)}');
});
Enter fullscreen mode Exit fullscreen mode

Now, define a route /app inside react.js and then, on useEffect or componentDidMount just use query-string to get the user.

const user = queryString.parse(location.search).user;
console.log(JSON.parse(user));
Enter fullscreen mode Exit fullscreen mode

PS. You can also set the cookie on the backend and parse it using react-cookie.

Setting Up React-Native:

It's fairly simple on the react-native though with the introduction of react-native-apple-authentication package.

You can just follow the Initial-Setup Guide or hop on:

Add Sign-in capability

Taking into consideration that you already have Target setup on XCode with bundle id com.example.app, just add the Sign In with Apple capability inside:

Sign In with Apple capability

PS. You need a valid team, however, once you set it you will see a screen similar to this:

Capability Success

Now, if you have not followed the guide from above you need to have your AppID setup in Identifiers. Follow the guide above for AppID only and come back here.

Note: Enable the APP ID as a primary if not already and click the Save button.

Now that you have everything set up, just add the package:

yarn add @invertase/react-native-apple-authentication
Enter fullscreen mode Exit fullscreen mode

And then use the AppleButton where you can add custom-styles as well. On pressing the button, we call a function to extract the identityToken with scope of for email and name.

import { Platform } from 'react-native';
import appleAuth, {
  AppleButton,
  AppleAuthRequestScope,
  AppleAuthRequestOperation,
} from '@invertase/react-native-apple-authentication';

const appleAuth = () => {
  const token = appleAuth
    .performRequest({
      requestedOperation: AppleAuthRequestOperation.LOGIN,
      requestedScopes: [
        AppleAuthRequestScope.EMAIL,
        AppleAuthRequestScope.FULL_NAME,
      ],
    })
    .then(
      res => {
        return res.identityToken;
      },
      error => {
        console.log(error);
      }
    );
  console.log(token);
  // TODO: Send the token to backend
};

export const appleButton = () => {
  return (
    <>
      {Platform.OS === 'ios' && (
        <AppleButton
          buttonType={AppleButton.Type.CONTINUE}
          onPress={() => appleAuth()}
        />
      )}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now that we have the token, we send it to our node-backend. However, above we are handling for the condition of callback_url and POST Request. Here, we'll use the same logic of getAppleUserId, so we'll just send the token to /apple/login backend route as:

import HttpStatus from 'http-status-codes';

app.use('/apple/login', function(req, res) {
  const user = await getAppleUserId(req.body.token);
  res.status(HttpStatus.OK).json({ user });
});
Enter fullscreen mode Exit fullscreen mode

Where,

  • getAppleUserId is defined above. Just have a look at it where we either extract or validate the jwt and extract the data.

Next, we forward the user back to react-native-client.

Conclusion

Setting it up is definitely not easy and requires patience. It took a lot of time for us as well and we intend to solve it for you. There might be some grammatical mistakes or logic issues here and there, do let me know if you find it.

Thanks for giving this a read. We will continue to keep you posted on the updates and cool stuff.

Until next time 👋

Sunim - https://prep.surf/blog/new-landing-page

Top comments (7)

Collapse
 
talha131 profile image
Talha Mansoor

Great write up. Thank you, @aryaminus .

I will add a tad more information. When the Apple Sign In was introduced, developers had the option of receiving id_token and code via URL fragment or query, besides form_post on the redirect URL.

But then Apple has changed it. Now, id_token and code are only sent via form_post to the redirect URL.

Another tidbit is that you can get a "Sign in with Apple" button, which conforms to the Apple human interaction guidelines by adding a div with id "appleid-signin". Apple's JS will automatically replace it with a button.

Collapse
 
jackyew profile image
jacky-ew

Hi how to handle the form_post ?
i try to fetch the redirect URL, but nothing is shown on console

Collapse
 
danielpdev profile image
danielpdev

Wow. Great content. Thanks for taking your time to post it!

Collapse
 
anh999 profile image
Jack

you made my day! Thanks!

Collapse
 
bagchi_mihir profile image
Mihir Bagchi

Hey is there a way to handle this in a popup rather than going to their site ?, I need to handle some state and this logic won't help me :((

Collapse
 
julisalis profile image
Julián Salischiker

Hi Mihir!

You have to add usePopup: true inside the params object passed to AppleID.auth.init.
If the signIn function started the authorization process, the function returns a promise object that resolves if the authorization succeeds, or rejected if fails.

try {
const data = await AppleID.auth.signIn()
} catch ( error ) {
//handle error.
}

Hope this helps!

Collapse
 
naveenhubino profile image
Naveenhubino

Nice article @aryaminus .
As I need to know is there any whole project you posted anywhere because it will help to know more about the execution flow. If yes please share it.