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
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:
- Install the Device Check Plugin
- Get the Device Token
- Send the Device Token to Our Backend
- Download the Device Check Key
- Verify the Device Token in the Backend
Install the Device Check Plugin
We need to install a plugin called @capacitor-community/device-check:
_10npm install @capacitor-community/device-check_10npx 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
:
_10import { DeviceCheck } from '@capacitor-community/device-check';_10..._10try {_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:
_10import { CapacitorHttp, HttpResponse } from '@capacitor/core';_10..._10// Send the token to our backend_10const 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 underKeys
click the+
button. - Provide a key name and check
DeviceCheck
. - Click
Continue
, then clickRegister
- Click
Download
and store the key securely (it will be saved with the filename formatAuthKey_[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.
_93import { SignJWT, importPKCS8 } from 'jose';_93import { v4 as uuid } from 'uuid';_93_93export interface Env {_93 AUTH_KEY: string;_93 TEAM_ID: string;_93 KEY_ID: string;_93}_93_93export 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_93async 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_93async 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 tofalse
during testing with Testflight/Production.
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
andbit1
to0
. 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".