adjoe Engineers’ Blog
 /  Fighting Fraud on iOS with Apple DeviceCheck

Fighting Fraud on iOS with Apple DeviceCheck

At adjoe, we use Apple’s DeviceCheck to ensure that users engaging with our Playtime feature are genuine and not attempting to spoof activity or manipulate rewards.

This matters because once an iOS app is released, it can be reverse-engineered or modified to bypass ads, unlock premium features, or cheat in gameplay.

This article gives an overview of the client-side protections we use to combat fraudulent behavior on iOS.  We’ll walk you through how we use Apple’s DeviceCheck, both Device Identification and App Attest to verify real devices. You can detect manipulation and strengthen your iOS fraud defenses.

Let’s briefly cover DeviceCheck before diving into implementation.

What is DeviceCheck?

First introduced with iOS 11, it consists of a mobile framework and Apple server interface that  developers need to access from their own server, which can make integration harder as it requires both client- and backend-side work.

This article provides code samples for both the client and the server, implemented in Swift; these samples can be used as a helpful starting point. You can download the whole source code here. Refer to the README to set up the project locally. The code snippets used in this article are simplified excerpts from this sample project.

DeviceCheck consists of two independent components:

  • A device identification mechanism, which enables developers to associate two bits of data with a particular device.
  • App Attest, which allows for verifying an app’s integrity, preventing access for compromised app instances.

Device Identification

You can associate two bits of data with a particular device in a privacy-respectful way. It means there’s no need to receive any identifiers that could expose a user’s identity outside your apps, or Apple isn’t aware of exactly how you use these bits. 

It’s up to you to decide what those bits mean. Here are a few possible examples:

  • A free trial has been used on this device.
  • The user has already received a promotional gift.
  • Check if the same device has been used to register multiple accounts.

The bits are unique per device per Apple developer team. Meaning all your apps installed on a device will share the same bit of info. 

Changing an account on the device or wiping the device’s data won’t reset the bits you associated with it. Users who resell their devices might as well transfer their data, so design your business logic accordingly.

How does it work?

The mechanism works through an encrypted token that must be generated on the device and should be communicated to your server. Here, Apple’s DeviceCheck web API must be called with the token in the request. The article covers the integration in both the client and server code.

Let’s look into the integration in both the client and server code.

  1. Register API Key to support DeviceCheck

You need to provide the authentication key to interact with Apple’s API. You can create that in Certificates, Identifiers & Profiles.

  1. On the page, click the Add button (+).
  2. Type in a name and optionally a description for your key.
  3. Select Key Services. You need to select at least DeviceCheck for this feature to work with the key.
  4. After confirming the configuration, download the key. It’s saved as a .p8 file that can be opened with any text editor. Save it in a secure place, as you won’t be able to re-download it.
Prevent Fraud on iOS with Apple DeviceCheck and App Attest

Each HTTP request to Apple’s server must include an authorization header. 

The value of this header should be a Base 64 URL-encoded JSON web token (JWT). You can find the sample code to generate this token later in the article.

Implement Device Identification on iOS

The client API to support this feature is simple. Take a look at the code snippet below.

import DeviceCheck // 1

if DCDevice.current.isSupported { // 2

    do {

        let tokenData = try await DCDevice.current.generateToken() // 3

    } catch {

        // 4

    }

} else {

   // 5

}

1. Import DeviceCheck to the files that use the API of this framework.

2. Check if the API is available. Not every target or destination supports this API.

In Xcode, a target is the specific product your project builds, such as an iOS/iPadOS app, or app extensions (iMessage, App Intents, etc.). 

A destination is the device or environment where the built target can run, such as a physical iPhone, iPad, Mac (via Mac Catalyst), or a simulator.

According to Apple’s documentation, only iOS/iPadOS app targets (excluding simulator and Mac Catalyst) and watchOS extension targets are supported for the DeviceCheck API.

3. Generate the token data. This operation is performed locally and can succeed even without the internet. Note that the token is ephemeral and you shouldn’t store it once it’s sent to your server.

4. Handle potential errors through the DCError type. For device identification, you may encounter either featureUnsupported or unknownSystemFailure. In the latter case, the recommended approach is to retry after some time and log the error for diagnostic purposes.

5. Implement a fallback strategy if DeviceCheck is completely not supported. A recommendation from Apple is that you don’t block the critical functionality of your app for such failures, but only possibly limit some sensitive features.

Send the token to your server once it’s generated. Here’s a simplified code snippet to send it in an HTTP request:

struct DeviceCheckRequest: Codable { let deviceToken: String }

let body = try JSONEncoder().encode(DeviceCheckRequest(deviceToken: token.base64EncodedString()))

var req = URLRequest(url: URL(string: "https://yourserver.com/devicecheck/verify")!)

req.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")

req.httpMethod = "POST"

req.httpBody = body

_ = try await URLSession.shared.data(for: req)

Implement Device Identification on Backend

On your server, you should receive the token and provide it further to Apple’s endpoints as HTTP Post commands. 

There are three endpoints available, they are well-explained in the official documentation

This article only provides a general overview. Use the sample project to explore a possible server implementation with Swift and VAPOR.

For development, use the test base URL which is https://api.development.devicecheck.apple.com. 

For production, use https://api.devicecheck.apple.com.

JWT Authentication

Before we start with the actual requests, it’s important to understand the authentication mechanism for Apple’s API endpoints which is JSON web token (JWT).

Each request must include an authorization header containing the authentication key. 

It must use the ES256 algorithm and be in the Base 64 URL–encoded JSON web token format. Failure to provide the correct token will lead to the error BAD_AUTHENTICATION_TOKEN.

The code samples below use JWTKit to generate a token.

  1. Create a payload. You need to provide your Team ID as the Issuer (iss) and Bundle ID as the Subject (sub), as well as dates of issue and expiration of the token.
import Foundation

import JWT

struct AppleJWT: JWTPayload {

    let iss: IssuerClaim        // Your Apple Team ID

    let iat: IssuedAtClaim      // Token issued at

    let exp: ExpirationClaim    // Token expiry

    let sub: SubjectClaim       // Your App's bundle ID

    func verify(using signer: JWTSigner) throws {

        try exp.verifyNotExpired()

    }

}

let now = Date()

let expiry = now.addingTimeInterval(10 * 60)

let teamId = "YOUR_TEAM_ID"

let bundleId = "YOUR_BUNDLE_ID"

// Create JWT payload

let payload = AppleJWT(

    iss: IssuerClaim(value: teamId),

    iat: IssuedAtClaim(value: now),

    exp: ExpirationClaim(value: expiry),

    sub: SubjectClaim(value: bundleId)

)
  1. Create headers. Include your Key ID that you created earlier:
let keyId = "YOUR_KEY_ID"

var headers = JWTHeader()

headers.kid = JWKIdentifier(string: keyId)
  1. Create a signer using the private key you obtained earlier and specify ES256 encryption. Then generate a JWT with the payloads and headers created above.
let privateKey = "YOUR_PRIVATE KEY"

let signer = try JWTSigner.es256(privateKey: Data(privateKey.utf8))

let jwt = try signer.sign(payload, headers: headers)

Include the generated token in the following header in all your HTTP requests to Apple's API endpoints:

authorization: bearer <TOKEN>

DeviceCheck API Endpoints

1. Query the bits

The request allows you to receive the bits associated with a given token.

Share the device token along with a transaction ID that your server should generate and a timestamp. You must provide the last two fields as that’s how Apple implements its infrastructure to process developers’ API requests.

There’s flexibility in how you generate the transaction ID: some teams debate whether to reuse the same ID for all requests associated with a given token or to generate a new one for each call. 

In practice, Apple doesn’t enforce a specific approach, so you can choose whichever strategy best fits your system’s design.

Here’s an example of a payload for the request which you send to the path v1/query_two_bits:

{

    "device_token" : "wlkCDA2Hy/CfrMqVAShs1BAR/0sAiuRIUm5jQg0a...",

    "transaction_id" : "5b737ca6-a4c7-488e-b928-8452960c4be9",

    "timestamp" : 1487716472000

}

And that's the response you'd expect:

{

"bit0": false,

"bit1": false,

"last_update_time": "2025-10"

}

Pay attention that the value format for the field last_update_time is YYYY_MM, is different from the timestamp you must provide in the request. It should be UTC timestamp in milliseconds since the Unix epoch.

If the bits have never been updated for the provided device token, you’ll get a 200 status code response and the following string body:

Failed to find bit state

2. Update the bits

The request to update the bits (v1/update_two_bits) is very similar and only contains the bit information in addition:

{

    "bit0": true,

    "bit1": true,

    "device_token" : "wlkCDA2Hy/CfrMqVAShs1BAR/0sAiuRIUm5jQg0a...",

    "transaction_id" : "5b737ca6-a4c7-488e-b928-8452960c4be9",

    "timestamp" : 1487716472000

}

If the response is 200, the bits are updated. So the next time you query them, you’ll see the date when you performed the update last time.

3. Validate device

Apple provides an endpoint (v1/validate_device_token) that verifies whether a given device token is still valid. 

This request uses the same payload structure as the “query bits” request. A successful 200 response confirms that Apple recognizes the token and that it can be used with the DeviceCheck service.

A device token might become invalid if it was never issued by Apple, if it has expired, or if its signature no longer matches the app’s current signing identity. Validation can also fail if the device is using an improperly configured runtime environment (for example, a tampered or revoked app installation).

Although this endpoint is still available, Apple now recommends using App Attest for stronger device integrity guarantees. Let’s cover App Attest in the next section.

App Attest App in the DeviceCheck Framework

App Attest is a feature introduced as part of DeviceCheck framework with iOS 14. It enables you to verify that the requests to your server are sent from a legitimate version of the application. 

How does the App Attest mechanism work?

The mechanism is powered by a cryptographic key-pair with the private key stored in the Secure Enclave. Apple servers are used to verify the validity of the key (once per app install), and then you can use it to generate assertions for all sensitive requests to your backend.

Reliability Considerations

Note that sometimes a client’s request to Apple’s servers might fail, that’s why it’s recommended only as an optional security measure that doesn’t block users from the main functionality of the app. 

You might want to restrict certain features of your app, like downloading premium content, or silently collect data, like the user’s achievements, but only sync it with the server once App Attest succeeds.

App Attest Limitations

App Attest ensures that the application that generates the request is signed by you and hasn’t been modified in any way. But it has some limits. 

For example, the mechanism can’t guarantee that a legitimate version of your app is run on a system that is not jailbroken, which could enable attackers to circumvent your restrictions.

Setting Up App Attest  

The following steps guide you through setting up App Attest in your project and preparing your app for secure communication.

How to Implement App Attest on iOS?

1. Add App Attest Capability

As the first step, you need to enable App Attest in Capabilities of your project:

  1. Select your project in Project Navigator.
  2. Select your app’s target.
  3. Navigate to the Signing & Capabilities tab.
  4. Click on the Plus button and add App Attest.
How to Mitigate Fraud on iOS with Apple DeviceCheck and App Attest

Adding this capability will automatically create an Entitlements File if your project doesn’t have any. 

In this file, you can specify App Attest Environment, which could be either production or development. 

Note that after distributing your app through TestFlight, the App Store, or the Apple Developer Enterprise Program, your app ignores the entitlement you set and uses the production environment. Ensure that your backend corresponds to the selected environment.

For example, when sending a sandbox-generated attestation object to your server, the verification procedure differs slightly. 

  • The authenticator data’s aaguid field will contain appattestsandbox instead of appattest. 

  • Your server-to-server call to obtain the fraud metric must use https://data-development.appattest.apple.com as the base URL, instead of the production URL https://data.appattest.apple.com.

2. Generate a Key Pair

After enabling the capability for App Attest, use the following method to generate a key pair. 

You can use both completion-handler or async method versions.

import DeviceCheck // 1

if DCDevice.current.isSupported { // 2

    do {

        let keyID = try await DCAppAttestService.shared.generateKey() // 3

    } catch {

        // 4

    }

} else {

   // 4

}
  1. Import DeviceCheck, as the feature is part of the same framework.
  2. Check that DeviceCheck is supported, the same as for Device Identification.
  3. Use the API to generate a cryptographic key pair. This operation is performed on the device, and you get a key ID as a successful response. 

This ID is a hash of your private key, you store it and reuse it for the same user unless they reinstall the app. The private key itself is stored in Secure Enclave and is never exposed to you directly.

  1. Handle cases when DeviceCheck is not supported or an error during key generation occurred. 

3. Attest the key

Before calling the client API, your server should issue a challenge to prevent man-in-the-middle and replay attacks. It can be any unique, random, one-time value. You can combine it with other data available on the client to generate a nonce hash that will be provided to App Attest.

Use the following method to attest the key:

let challenge = try await myServer.getChallenge()

let nonceHash = Data(SHA256.hash(data: challenge))

let attestation = try await DCAppAttestService.shared.attestKey(keyID, clientDataHash: nonceHash)

This method will use the private key to create a hardware attestation request for the device. It will submit the request to Apple servers for verification, which means it can fail if there’s no internet connection (error DCError.serverUnavailable). 

It’s also the reason why Apple recommends gradual roll out of this feature in your app. From within a day for a few million users up to 30 days for a billion.

Once verified, Apple will return an anonymous attestation object to your app. You need to send this object to your server for further validation. The structure of this object and the ways to validate it are described in the Backend implementation section below.

You should call this method once per app installation, unless another account is used on the same app. In which case a new key should be re-generated and re-attested.

It’s possible that you get DCError.invalidInput or DCError.invalidKey thrown. It might happen, among other reasons, if you attest the same key twice. The possible solution for such cases is to regenerate the key.

4. Generate an assertion

Once you have the attested key, you can call the generateAssertion API to secure communication between your app and server. 

All assertions are generated on-device and validated on your server, so Apple servers are no longer involved. 

Assertions allow to verify that the app possesses the private key that was previously attested when performing a sensitive request to your server.

Here’s how you’d call this method:

let challenge = try await myServer.getChallenge()

let nonceHash = Data(SHA256.hash(data: challenge))

let assertion = try await DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: nonceHash)

It is essential to know the meaning of nonce here. It might be a challenge that your app requests from your server before each sensitive request. It might be some predictably built data, like the timestamp of the request or the digest of the payload. 

What’s important is that your server’s able to reconstruct it for a particular request to verify the assertion. It makes you know when and why the assertion was created, eliminating room for replay attacks.

Take into account that since assertion is a cryptographic operation, it will add some latency to the server requests that your app performs.

Implement App Attest on Backend

The challenging part about App Attest is that it requires some backend work that may not be familiar to a regular iOS developer. You should:

  • Be familiar with the CBOR data format which is used for attestation and assertion objects.
  • Know the Web Authentication specification and recognize where Apple deviates from it.
  • Particularly, have a solid understanding of X.509 certificates and how to verify the validity of a certificate chain.

It’s up to you how exactly you verify attestations and assertions, there’s no Apple backend API for that. As is the case with the DeviceCheck mechanism for Device Identification that was covered earlier in this article. However, this allows you to have more control on your side.

This article outlines backend concepts at a high-level. Check the sample project if you want to explore a full implementation.

1. Generate challenge

The ideal way is that your client can request a challenge before key attestation and every assertion generation. A basic challenge is a random and unique byte sequence that is incorporated into attestation and assertion objects to prevent several attacks like a replay attack.

That’s how you can generate a challenge using CryptoKit to guarantee that the result is cryptographically secure random data:

import Foundation

import CryptoKit

func generateChallengeData() -> Data {

    var data = Data(count: 32)

    let _ = data.withUnsafeMutableBytes {

        SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)

    }

    return data

}

Store the challenge on your server. It’s a good practice to make it short-lived as well, so that it’ll expire in, say, 5 minutes.

2. Verify attestation

When the client attests the key pair it has generated, it should send the resulting attestation object along with a key identifier to your server. Here are the most important components of that object:

Reduce Fraud on iOS with App Attest - Apple DeviceCheck Complete Guide 2026

Verifying the certificates ensures that the public key has been attested by Apple. So it’s a cryptographic confirmation that the key pair was created on a real device and as a response to your challenge. 

You should verify the validity of both certificates using Apple’s App Attest root certificate. Using the challenge you provided, reconstruct the nonce and verify that it corresponds to the one in the leaf certificate. 

Extract and store the public key from the credCert field of the leaf certificate and verify that its hash matches the key ID provided along with the attestation object from your client.

Authenticator data contains a field RP ID, whose value is a hash for your app’s ID (a 10-digit team ID and bundle ID separated by a dot). It guarantees that the app instance was signed by you. 

The field credentialId should contain the hash of the public key, and you need to compare it to the key ID.

Risk metric receipt could be additionally used to retrieve the number of key attests performed on a given device for your app in the last 30 days. 

Use a specific Apple API endpoint to retrieve the metric from the receipt, and periodically refresh the metric by submitting the most recent receipt. 

// Note that a number of attests could be more than 1 in harmless cases, because App Attest key pair doesn’t survive over an app re-installation. But a higher number could indicate that the same device is used for multiple app-installations, possibly serving modified versions of your app if the operating system is jailbroken. Evaluate the risk based on your case.

You can find the code that parses the attestation object in the sample project.

3. Verify assertion

The structure of an assertion object is more lightweight:

 Fraud on iOS with DeviceCheck and App Attest - Apple DeviceCheck Complete Guide 2026


Use the public key extracted from the attestation object to verify the signature in the assertion. Reconstruct the nonce that you provided for assertion on device and compare it with the one in the signature. Check that authenticator data contains the hash of your app ID.

Limitations of Apple’s Anti-Fraud Solutions

Even though DeviceCheck offers solid protection, it has important limitations. 

Specifically, it cannot:

  • Determine the integrity of the device and the OS running on it. If a device is jailbroken, DeviceCheck functionality might be circumvented.

  • Completely protect you from man-in-the-middle attacks. Implement techniques like an SSL certificate pinning to make it harder to spoof the network requests that your app performs.

  • Prevent the app from being debugged, and parts of it substituted with techniques like method swizzling.

  • Hide secrets that your app stores. Always use techniques to obfuscate secure data like API access tokens or premium resources. Take into account that they might be compromised and extracted.

Another thing to keep in mind is that this integration isn’t free from an engineering perspective. It requires backend work, and the effort your team invests will only protect clients on iOS.

EndNote

While no solution can provide complete client protection, DeviceCheck significantly raises the barrier for attackers and is a valuable addition to any mobile fraud-prevention stack. Its native iOS support and proven impact make it well worth considering for strengthening the security of your mobile clients.

Our team relies on Apple’s DeviceCheck to block thousands of fraudulent attempts daily, from simple reinstall abuse to illegitimate app traffic. 

Want more in-depth technical insights? Check out the adjoe engineer’s blog.

Build products that move markets