DEV Community

Cover image for Complete Guide to Implementing Apple OAuth 2.0(Sign in with Apple) Authentication In a Node/Express Application
Daniel Okoronkwo
Daniel Okoronkwo

Posted on

Complete Guide to Implementing Apple OAuth 2.0(Sign in with Apple) Authentication In a Node/Express Application

What is OAuth?

OAuth, short for Open Authorization, is a standard that allows users to give third-party clients usually web applications, mobile applications, desktop applications, etc access to their basic data located on another server without giving up their sensitive data like passwords or any other encrypted data.

OAuth allows data to be exchanged between different servers at different locations provided that the user in question authorizes the data exchange. This protocol gives a faster on-boarding experience for users on other applications by transferring their already existing details from an identity provider to a third-party application that needs access to this information.

Here is a link to read more about OAuth.

How OAuth 2.0 Works:

Here is an example summary of how the OAuth 2.0 works

The third-party application e.g web, mobile or desktop application sends a request to the Identity Provider e.g Google, Apple, Facebook, Twitter, or GitHub specifying the redirect URL. The redirect URL is a webhook URL that the identity provider uses to securely transmit sensitive data to the third-party application after the user has authorized the identity provider.

  1. The third-party application e.g web, mobile or desktop application sends a request to the Identity Provider e.g Google, Apple, Facebook, Twitter, or GitHub specifying the redirect URL. The redirect URL is a webhook URL that the identity provider uses to securely transmit sensitive data to the third-party application after the user has authorized the identity provider.

  2. The Identity provider prompts the user to authorize the data transfer.

  3. The identity provider issues an Authorization Code to the third-party application.

  4. The third-party application exchanges the authorization code with the identity provider for an Identity token, a refresh token and an access token.

  5. The Identity token is a JWT string that can be decoded to access the user’s information.

  6. The third-party application can choose to decode and retrieve the user information(This is less secure as the signature of the JWT token may have been tampered with during the transmission) or utilize the access token to make further requests to the identity provider to retrieve the user’s information.

  7. The third-party application uses the access token to request the user’s data on behalf of the user.

While this procedure is pretty straightforward and vastly used by several identity providers, Apple’s OAuth2 implementation seems to be a lot different and quite challenging for a lot of developers to implement. I guess this is mostly because of Apple’s user privacy policy.

Before we proceed, let’s point out some of the differences that exist between Apple’s OAuth implementation and the widely used implementation by some of the other identity providers.

  • Apple gives its users the option to hide their email addresses while registering for certain services on third-party websites. If users choose to hide their email during the authorization process, Upon completion of the entire Authorization process, Apple issues a proxy email to the third-party website that redirects all the emails sent to the proxy email to the user’s real email address. Apple says that they do this to prevent spamming of their users.

    • Unlike most identity providers, at the time of writing there is no REST API endpoint to access the user’s profile information using a server-issued access token on behalf of the user, this makes it difficult to rely on the “Sign in with Apple” option as the only source of truth when onboarding new users in a third-party application.

Here is a link to Apple’s documentation on “Sign in with Apple”

Let’s get Started

Before we proceed, let’s consider how we want our application to work.

The client-side of the application(Web or Mobile) which for the sake of this tutorial we would not talk about in-depth, would initiate the entire authorization flow, get the access, refresh and identity token and then send a POST request to an API endpoint that we would define in our Node/Express backend server with either of the access token or the identity token.

For some identity providers like Google or Facebook, it would be best that the client sends the access token as this would allow our server to retrieve the user’s details on their behalf using an endpoint provided by Google and Facebook or any other identity provider.

In Apple’s case, though not explicitly specified in the developer documentation, as at the point of writing this article there is no endpoint provided by Apple to retrieve the user’s data on their behalf using the access token provided by Apple. With that said, as recommended by Apple on the developer documentation, we would be depending on the identity token(id_token) which I earlier stated is a JSON Web Token string containing some of the user information like email, sub etc. We would not just decode the token and retrieve the user information, that would not be nice since anyone with the technical knowledge can create a JWT string and also decode it.

In order to verify an Identity token issued by Apple, there are steps Apple recommends and they are all outlined in this part of the Apple developer documentation. One particular point I would like to highlight in this article which is also the approach we would be using in this article is to verify the JWS E256 signature using the server’s public key. Of course, this would sound confusing at first and at this point you may already be tempted to just decode the identity token and retrieve the data you need but that wouldn’t be a good approach and would also lead to a technical debt that could cause the company you work for huge amounts of money in the future.

The API endpoint that we would define in our Node/Express backend server would need the identity token to be passed in the request body when the request is to be sent and yes your guess is as good as mine, the request would be a POST request. The endpoint would also be responsible for making validating and verifying the JWS E256 signature of the identity token using the server’s public key in some kind of cryptographic manner and at the same time retrieving the user information from the token.

Enough of the boring talk, let’s write some code.

In order to follow along, you will need nodejs installed. Nodejs version >= 10.x would do. The example code for this tutorial would be based on Nodejs version 16.x.

Nodejs by default comes with NPM a package manager that allows developers to pull libraries and packages into their development workflow from the registry.

For this article, I would be using the yarn package manager.

Let’s set up a mini Nodejs/Express server with one endpoint that would allow our client-side applications to send a POST request with the id_token.

Create a folder and name it whatever you want to. Inside the folder for a start, we will install express and nodemon By running the command

npm install -–save express nodemon
Enter fullscreen mode Exit fullscreen mode

Or

yarn add express nodemon
Enter fullscreen mode Exit fullscreen mode

Create an index.js file at the root of the project folder and add the below snippet

const express = require("express")

const app = express();
const PORT = 3000;

app.post("/auth/apple", (req, res) => {
  const { id_token } = req.body
})

app.listen(PORT, () => console.log(`Server listening on port ${PORT}`))
Enter fullscreen mode Exit fullscreen mode

In the above snippet

  • We imported the express package we installed earlier
  • Created an instance of express
  • Created a route /auth/apple
  • We serve the app and listen on PORT 3000 for incoming request
  • We are also destructing the property id_token from the incoming request body just like earlier stated

Next, we need to verify the JWS E256 signature of the id_token using the server’s public key just as apple recommends.

First, what is the server public key and how do we fetch it?

The server public key is referred to by Apple as a JWK key set located in this part of the developer documentation.

According to the documentation, when we send a GET request to

https://appleid.apple.com/auth/keys we get a JSON keys response in the format below

https://appleid.apple.com/auth/keys response snippet

The response above is a JWKS key set. The JWKS key set contains information needed to get the Apple public key that would be used to verify the JWS E256 signature of the id_token. It is quite difficult to write specific detail on how to get the public key from this JWKS but in this article, we would use jwk-rsa, a nodejs package that converts a set of JWK keys into an equivalent Public key.

Install the package by running the command

npm install --save jwks-rsa
Enter fullscreen mode Exit fullscreen mode

Or

yarn add jwks-rsa
Enter fullscreen mode Exit fullscreen mode

To generate the public key with this package we would require a “kid” that matches one of the kid properties on the JWKS key set returned from the Apple server. From the attached image below, we can see that For every Apple issued Id_token, there exists a “kid” property on the header of the decoded id_token

Apple Issued id_token when decoded

In order to get the “kid” from the header of id_token, we would need to decode the token and get the header and then get the “kid” from the header. For this, we can use the j*sonwebtoken* nodejs package to decode the id_token.

npm install --save jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

Or

yarn add jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

Modify the initial code by adding the following line

const express = require("express")
const jwt = require("jsonwebtoken");


const app = express();
const PORT = 3000;



app.post("/auth/apple", (req, res) => {
  const { id_token } = req.body
  const { header } = jwt.decode(id_token, {
    complete: true
  })

  const kid = header.kid
})

app.listen(PORT, () => console.log(`Server listening on port ${PORT}`))

Enter fullscreen mode Exit fullscreen mode

The time we have all been waiting for.

In order to generate the public key modify the code to look like the one below

const express = require("express")
const jwksClient = require("jwks-rsa");
const jwt = require("jsonwebtoken");


const app = express();
const PORT = 3000;


async function key(kid) {
  const client = jwksClient({
    jwksUri: "https://appleid.apple.com/auth/keys",
    timeout: 30000
  });

  return await client.getSigningKey(kid);
}

app.post("/auth/apple", async (req, res) => {
  const { id_token } = req.body
  const { header } = jwt.decode(id_token, {
    complete: true
  })

  const kid = header.kid
  const publicKey = (await key(kid)).getPublicKey()
  console.log(publicKey)
})

app.listen(PORT, () => console.log(`Server listening on port ${PORT}`))

Enter fullscreen mode Exit fullscreen mode

When you send a POST request to http://localhost:3000/auth/apple **and pass the Apple issued id_token**, you will get a string in the following format in the console

-----BEGIN PUBLIC KEY----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvqNYBKQeFfPlSDq3kGxg
GtcMiCta7Tl/eirZ8T7knlEQomJjQN1z4p1rfhnA6m2dSh5/cnAo8MByRMlAO6DB
401k/A6YUxEqPjGoSnESQhfwL7MezjVDrHnhlnLTFT5a9MZx2PPJlNn+HSI5iKyz
AVBP+zrvnS1kbQE4G1nmpL/zS2ZYfvEWK2B7B1a14loBIT947Woy102yn1/E603l
T+lkNTIWbdhF85w4PNWqnfA7P51wpvtx1k3XURgZk6SMR6Slx53McKj0fho6Z0oK
nK2ov/0VeiKFwEyDf2zU5bdx/B+B/n+S84l1ypHg+gBNBN+wNWh4xZUHhcsZHpIL
mQIDAQAB
-----END PUBLIC KEY-----
Enter fullscreen mode Exit fullscreen mode

With the string above, we can then verify the JWS E256 signature of the Apple issued id_token by modifying the code a little.

const express = require("express")
const jwksClient = require("jwks-rsa");
const jwt = require("jsonwebtoken");


const app = express();
const PORT = 3000;


async function key(kid) {
  const client = jwksClient({
    jwksUri: "https://appleid.apple.com/auth/keys",
    timeout: 30000
  });

  return await client.getSigningKey(kid);
} 

app.post("/auth/apple", async (req, res) => {
  const { id_token } = req.body
  const { header } = jwt.decode(id_token, {
    complete: true
  })

  const kid = header.kid
  const publicKey = (await key(kid)).getPublicKey()
  console.log(publicKey)

  const { sub, email } = jwt.verify(id_token, publicKey);
  return { sub, email }
})

app.listen(PORT, () => console.log(`Server listening on port ${PORT}`))

Enter fullscreen mode Exit fullscreen mode

if everything works without errors, an Object of type JWTPayload containing the sub and email as well as other properties.

You can go ahead to store the email and sub in the database depending on your application needs.

Conclusion

In this article, our major focus has been on ensuring that we are able to verify the JWS signature of an Apple issued id_token.

While this is properly covered here, you may need to take certain steps before needing this article in the first place. For instance, how to get the necessary Apple credentials like Client Secret, Client ID. You will most likely spend a substantial amount of time in the Apple developer documentation before achieving your aim.

Here is a link to how you can get the above-mentioned credentials.

Top comments (0)