Marcus Kida's Blog

Hey! 👋 I'm Marcus, a Freelance Senior iOS Developer 📱 CTO-as-a-Service and CEO @Bearologics. I'm solving problems through code. Want to hire me? 👉 Let's talk!

Rationale

Often teams are using static, hard coded secrets or API keys to try to secure their backend services. While this might seem like a good idea on the first glance, the problem lies in the fact that it's pretty easy to exfiltrate those keys from your app, especially if it lies in some form of plain string inside the string resources. Otherwise there's probably still a good chance to snoop it by going MITM on your App's HTTP traffic.

By using DeviceCheck you can drop your propriertary API key and use a short-lived, Apple issued, token which you can authenticate server-side before answering calls to your iOS App.

This technology is applicable to Android, as well as other FaaS (Function-as-a-Service) such as AWS Lambda, Vercel (prev now.sh) or Google Cloud Functions. However we've been mostly using Cloudflare Workers and will use their service, as well as the iOS Platform, as an example implementation for security via Apple DeviceCheck.

Prerequisites

We're expecting you to have knowledge about iOS development as well as Xcode up and running, for developing the Worker you'll also need Node.js and a basic knowledge of Cloudflare Workers and their Wrangler CLI installed.

What is Apple DeviceCheck

As per Apple's documentation, DeviceCheck is a framework to 👉

Reduce fraudulent use of your services by managing device state and asserting app integrity. Source

In our case we'll leverage it to assert on HTTP requests agains one of our Worker, which will only allow traffic from our own app, DeviceCheck also let's you flip two bit's per device, which you can use as desired, but we'll not cover this feature as it's not relevant for our use-case.

The DeviceCheck API

Apple is providing documentation on how to query the DeviceCheck API from your Server, or Worker in our case, here.

Basically we're going to send a simple JSON payload via POST to the Apple DeviceCheck Server, if we're providing a valid device token, the request weill succeed, otherwise it will fail, the payload sent to the DeviceCheck Server looks like this:

{
   "device_token" : "wlkCDA2Hy/CfrMqVAShs1BAR/0sAiuRIUm5jQg0a..."
   "transaction_id" : "5b737ca6-a4c7-488e-b928-8452960c4be9",
   "timestamp" : 1487716472000
}

(Note that you won't have to construct and send this payload yourself, but we'll use a Node module which we've open-sourced for your to use, more on this later.)

To authenticate against Apple's DeviceCheck API, you'll need to create new private key in the Apple Developer Portal under Certificates, Identifiers & Profiles -> Keys.

Once you've created a new Authentication Key there, you'll be able to download it. It's a P8 file containing the key in the private key format, we'll use this later so please keep it in a safe place, also don't share it with others or check it in via source control, as it's a private secret 🤫.

Writing the Client-side code

Generating a DeviceCheck token, as of writing this, only works on a real iOS / iPadOS device. You should import and use the DeviceCheck.framework for this.

We'll now generate a device token and send it to our Worker using a custom header, which is called X-Apple-Device-Token, this header is parsed by the Worker and sent to the Apple Server for verification.

When using a development certificate, you also want to set the header X-Apple-Device-Development: true for the Worker to target Apple's Development Server rather than the production one.

A sample implementation looks like the following, assuming your Worker is deployed to https://my-worker.my-handle.workers.dev but of course you can use any networking client you like, as long as you can set custom header values.

import DeviceCheck

guard DCDevice.current.isSupported else {
  fatalError("Device not supported") // todo: handle error
}

DCDevice.current.generateToken { data, error in
  guard let data = data else {
    fatalError("Could not generate device token") // todo: handle error
  }

  let tokenString = data.base64EncodedString() // going to use this in our header

  let request = URLRequest(url: "https://my-worker.my-handle.workers.dev")
  request.setValue(tokenString, forHTTPHeaderField: "X-Apple-Device-Token")

  // optional when signing using a development certificate
  // this will use Apple's development server
  #if DEBUG
  request.setValue("true", forHTTPHeaderField: "X-Apple-Device-Development")
  #endif

  let config = URLSessionConfiguration.default
  let session = URLSession(configuration: config)

  let task = session.dataTask(with: request, completionHandler: { data, response, error in
    // handle result
    // work with your response as usual, in case it didn't pass the DeviceCheck validation, the response will carry a 401 status code
  }

  task.resume()
}

As you can tell there's not a lot of client side involved to secure your API, in fact you're only adding one or two headers.

A Node module to connect to Apple's DeviceCheck Server

We've published a Node Module on Npmjs.org to help you easily get started with this, it only has one dependency and can be easily used with Cloudflare Workers.

You can find the source code on GitHub and happily contribute to it.

Writing the Worker

First create a new worker project using Wrangler CLI and go into the Worker's directory:

wrangler generate my-worker && cd my-worker

We have to store your secrets, the private key from your AuthKey_XXXX.p8 file, the Key ID (which is the XXXX in the .p8 filename) as well as your Issues ID (your Apple Development Team ID) securely so your Worker can use them, for this we're going to use Cloudflare Worker Secrets.

Now let's go ahead and pipe this key to wrangler to store it as a Cloudflare Secret (please replace the XXXX with your actual Key ID) in a secret named APPLE_JWT_PRIVATE_KEY:

cat AuthKey_XXXX.p8 | wrangler secret put APPLE_JWT_PRIVATE_KEY

Now what we'll also need is the actual Key ID, that's the last bit in the key's filename after AuthKey_, you may also find this in the Apple Developer Portal when opening up the corresponding Key entry.

Let's store it in another secret named APPLE_JWT_KID:

wrangler secret put APPLE_JWT_KID

The last secret we need to store is the issuer, this is your Apple Developer Teams identifier, you can find it e.g on your Apple Developer Membership page under Team ID.

Let's store it as APPLE_JWT_ISS:

wrangler secret put APPLE_JWT_ISS

Those three things will be used by the @bearologics/devicecheck Node module to create and sign a JWT which is required to authenticate with the Apple DeviceCheck API.

Then install the DeviceCheck module:

npm i -S @bearologics/devicecheck

Our previously created secrets will automatically be made available to the Worker, so all you need to do now is to provide your Request object to the exported method of the module, it's an async function an it will return either a true or false depending on whether the DeviceCheck was successful or not.

const deviceCheck = require("@bearologics/devicecheck");

addEventListener("fetch", function (event) {
  const { request } = event;
  const response = handleRequest(request);
  event.respondWith(response);
});

async function handleRequest(request) {
  const { url } = request;
  const { pathname, searchParams } = new URL(url);

  const deviceCheckPassed = await deviceCheck(request, {
    iss: APPLE_JWT_ISS,
    kid: APPLE_JWT_KID,
    privateKeyPEM: APPLE_JWT_PRIVATE_KEY,
  });

  if (!deviceCheckPassed) {
    return new Response("Unauthorized", { status: 401 });
  }

  return new Response("OK", { status: 200 });
}

Don't foget to specify your Cloudflare account_id inside your Worker's wrangler.toml. Now your almost good to go. Publish your worker and try it out:

wrangler publish

Now run your App on an iOS / iPadOS device and see if the request is successful.

If you've followed all the steps above and are still getting a 401 - Unauthenticated response, please make sure that APPLE_JWT_KID, APPLE_JWT_ISS and APPLE_JWT_PRIVATE_KEY are set to the correct values, if you're unsure check them again.

We'd be curious to hear your opinion on this, so please follow and engage on Twitter or send us an Email.

If you're having problems or spotting something odd, please let us know. Also if you need help improving or shipping your own product, we're available for hire! Let's chat 👉 inquiries@bearologics.com.

Tagged with: