Skip to main content

Device Check

On an iOS device you can use Device Check to help reduce fraudulent use of your services by managing:

Device State

This tutorial covers how to use the DCDevice API to:

  • Identify devices while preserving privacy.
  • Store state about the device (2 binary digits per device).

This can be used to:

  • Flag a device that you've determined to be fraudulent.
  • Identify devices that have taken advantage of a promotional offer.
  • Store some other state that needs to be specific to the device.

The API would typically be used in combination with Asserting App Integrity.

Asserting App Integrity

warning

This tutorial DOES NOT cover the topic of App Integrity. App Integrity uses the

DCAppAttestService

API.

If you are interested in this topic it is worth noting that it requires a backend service with database requirements to store multiple key and receipt pairs for each user of a device. It also requires a native plugin in a Capacitor project.

Implementing Device State

This tutorial describes how you use the Device State feature in a Capacitor application by following these steps:

  1. Install the Device Check Plugin
  2. Get the Device Token
  3. Send the Device Token to Our Backend
  4. Download the Device Check Key
  5. Verify the Device Token in the Backend

Install the Device Check Plugin

We need to install a plugin called @capacitor-community/device-check:


_10
npm install @capacitor-community/device-check
_10
npx cap sync

The plugin is used to get a Device Token which we will send back to our backend.

Get the Device Token

In our application, we make a call to generateToken:


_10
import { DeviceCheck } from '@capacitor-community/device-check';
_10
...
_10
try {
_10
const result = await DeviceCheck.generateToken();
_10
// Send the token to our backend
_10
} catch (err) {
_10
// Log the error, but it is likely ok to continue running the app
_10
}

The Device token is now in the variable result.token and we can send it to our backend.

Send the Device Token to Our Backend

We'll send the Device Token to our backend using Capacitor HTTP:


_10
import { CapacitorHttp, HttpResponse } from '@capacitor/core';
_10
...
_10
// Send the token to our backend
_10
const response: HttpResponse = await CapacitorHttp.post({
_10
url: 'https://myserver',
_10
headers: { 'Content-Type': 'application/json' },
_10
data: { token: result.token }
_10
});
_10
// Add your response handling here

You can continue running your app regardless of the response but you may want to handle the scenario of a response that indicates the device has been flagged as fraudulent by alerting the users. Other potential responses may include Apples servers being down or is error due to App being run from XCode.

Download Device Check Key

For our next steps we'll need to download a DeviceCheck Key from the Apple Developer Portal:

  • Login to your Apple Developer Account
  • Visit Certificates, Identifiers & Profiles and under Keys click the + button.
  • Provide a key name and check DeviceCheck.
  • Click Continue, then click Register
  • Click Download and store the key securely (it will be saved with the filename format AuthKey_[key ID].p8)

Accept Token in the Backend

You can use whatever backend technology you like, however in this tutorial I'll describe how to create a backend with a Cloudflare Worker.

You can find the sample Cloudflare Worker project here.

Creating a Cloudflare Worker

  • Run npm create cloudflare and choose the hello world example.
  • Install a JWT library by running npm install jose
  • Install a unique ID generator by running npm install uuid
  • Install its types by running npm install --save-dev @types/uuid

Verify the Token

The following code:

  • Accepts the Device Token from our app.
  • Generates a JWT from our Private Key.
  • Calls the Apple server to validate the Device Token.
  • Returns a success response to our app.

_93
import { SignJWT, importPKCS8 } from 'jose';
_93
import { v4 as uuid } from 'uuid';
_93
_93
export interface Env {
_93
AUTH_KEY: string;
_93
TEAM_ID: string;
_93
KEY_ID: string;
_93
}
_93
_93
export default {
_93
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
_93
if (request.method != 'POST') {
_93
return new Response('', { status: 404 });
_93
}
_93
return await validateDeviceCheckToken(request, env);
_93
},
_93
};
_93
_93
async function validateDeviceCheckToken(request: Request, env: Env): Promise<Response> {
_93
try {
_93
const deviceToken = await getDeviceToken(request);
_93
const isDevelopment = request.url.includes('development');
_93
_93
if (!deviceToken) {
_93
return new Response('', { status: 404 });
_93
}
_93
_93
let privateKey;
_93
try {
_93
privateKey = await importPKCS8(env.AUTH_KEY, 'ES256');
_93
} catch (e) {
_93
console.error(`Unable to create private key`, e);
_93
return new Response('KeyError', { status: 401 });
_93
}
_93
_93
const jwt = await new SignJWT({ iss: env.TEAM_ID })
_93
.setProtectedHeader({ alg: 'ES256', kid: env.KEY_ID, typ: 'JWT' })
_93
.setIssuedAt()
_93
.setExpirationTime('12h')
_93
.sign(privateKey);
_93
_93
// In production this should be set to false
_93
const environment = isDevelopment ? 'api.development' : 'api';
_93
console.log(`POST https://${environment}.devicecheck.apple.com/v1/validate_device_token`);
_93
console.log(`Authorization: Bearer ${jwt}`);
_93
_93
const body = JSON.stringify({
_93
device_token: deviceToken, // The Device Token from our Capacitor App
_93
transaction_id: uuid(), // A unique transaction id
_93
timestamp: Date.now(),
_93
});
_93
_93
// Send the request to Apple
_93
const res = await fetch(`https://${environment}.devicecheck.apple.com/v1/validate_device_token`, {
_93
method: 'POST',
_93
headers: {
_93
Authorization: `Bearer ${jwt}`,
_93
'Content-Type': `application/json`,
_93
},
_93
body,
_93
});
_93
if (res.status == 200) {
_93
const data = await res.text();
_93
console.log(`Success`, data);
_93
return new Response('Awesome!');
_93
} else {
_93
const text = await res.text();
_93
if (text == 'Missing or badly formatted authorization token') {
_93
// If you deployed your app to a device with Xcode this error may occur
_93
// You need to deploy to testflight and test that way
_93
}
_93
console.error(`Failure ${res.status}`, await res.text());
_93
return new Response('Error', { status: 401 });
_93
}
_93
} catch (err) {
_93
console.error(`Exception`, err);
_93
return new Response('Error', { status: 401 });
_93
}
_93
}
_93
_93
async function getDeviceToken(request: Request): Promise<string | undefined> {
_93
try {
_93
const body: any = await request.json();
_93
const deviceToken = body.token;
_93
if (!deviceToken) {
_93
return undefined;
_93
}
_93
return deviceToken;
_93
} catch {
_93
console.error(`Request is missing a JSON body`);
_93
return undefined;
_93
}
_93
}

These Environment Variables need to be set in the above code:

  • AUTH_KEY - This is the contents of the Device Check Key we downloaded from Apple in the previous step.
  • TEAM_ID - This is your Team ID from your Apple Developer Account.
  • KEY_ID - This is the Key ID that is associated with the Device Check Key from your Apple Developer Account.
  • isDevelopment - This is a boolean value used to specify which Apple API to use. Set it to false during testing with Testflight/Production.
warning

A typical error response you may receive from Apple's API is status code 400 with the text Missing or badly formatted authorization token. This may have been caused by the app being deployed to the device using XCode. You must deploy your app to Testflight (or the Store) to get a 200 response code from Apple.

Summary

This tutorial has covered using the Device Check API to confirm that our App was recognized by Apple as having been installed from the App Store. We have not verfied that the app is untampered with. Doing so requires that we assert App Integrity, which is a topic that is not covered by this tutorial.

Next Steps

  • App Integrity - To verify that app is not altered or distributed outside the App Store we need to use the DCAppAttestService class. Reading the detailed documentation from Apple will give a good sense of how much work is required to implement this.

  • Accessing Device State - Apple can store information about the device which you can use for anything (eg is the device fraudulent). These are additional calls you can make to Apple in your backend. You should read about how to do this in Apple's Documentation.

Common Questions

  • What are other responses that could be returned from Apple? - See Apples documentation

  • Does Device State survive uninstalling the App? - Yes, so be sure to store only relevant information. For example, maybe your App has a purchase for a paid plan per device. You can use a bit flag to indicate that the user paid on this device.

  • Is there a way to remove data from Device State? - As only 2 bit values can be stored it isn't necessary to clear this data, but you can set these values for bit0 and bit1 to 0. For information on how, see the Apple Documentation.

  • If the user installs my App on another device does the Device State transfer? - No, the device state is for the particular unique device.

  • During development I'm getting an error response from Apple? - This is likely because you are not deploying the app using TestFlight or the App Store. A Device Token that is generated by an App that was deployed with Xcode will return a status of 400 and message "Missing or badly formatted authorization token".