How can you trust that your app is running on a genuine, unmodified Android device? This is a critical question for any app that handles sensitive data, offers premium content, or has a competitive element. Without a way to verify device integrity, your app is vulnerable to cheating, content piracy, and fraud.
For years, Google's SafetyNet Attestation API was the answer. Now, it has been deprecated and fully replaced by the more powerful and unified Play Integrity API. This guide will walk you through why you need this protection, how to migrate from the old API, and how to implement the Play Integrity API correctly, including the crucial backend verification step.
Why Do You Need Device Integrity Checks?
An attacker can gain a huge advantage by running your app in a compromised environment. Here's what you're up against:
- Rooted Devices: A user with root access can modify your app's memory, tamper with its data, and bypass security checks. This is a common way to cheat in games or bypass in-app purchase logic.
- Emulators: Malicious actors can run your app on thousands of emulated devices to create fake accounts, spam your service, or perform ad fraud.
- Tampered APKs: An attacker can decompile your app, modify it to unlock premium features for free, and redistribute it on third-party app stores.
The Play Integrity API gives you a signed and encrypted verdict from Google about the state of the device, allowing you to make an informed decision on your backend about whether to trust the request.
From SafetyNet to Play Integrity: The Migration
The Play Integrity API combines several older APIs (including SafetyNet) into one. The migration is straightforward.
Step 1: Add the Play Integrity Dependency
Remove the old SafetyNet dependency and add the new Play Integrity one.
// In app/build.gradle.kts
// remove: implementation("com.google.android.gms:play-services-safetynet:...")
implementation("com.google.android.play:integrity:1.3.0") // Use latest
Step 2: Request the Integrity Token
The client-side code is much simpler now. You create an `IntegrityManager`, build a request with a `nonce`, and get back a token.
// 1. Create an instance of the IntegrityManager.
val integrityManager = IntegrityManagerFactory.create(context)
// 2. Request the integrity token by providing a nonce.
// The nonce should be a random, securely generated string from your server.
val nonce: String = // ... get nonce from your server
val integrityTokenResponse: Task<IntegrityTokenResponse> = integrityManager.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce)
.build()
)
integrityTokenResponse.addOnSuccessListener { response ->
val integrityToken = response.token()
// 3. Send this token and the original nonce to your secure backend server.
myBackend.sendTokenToServer(integrityToken, nonce)
}.addOnFailureListener { e ->
// Handle error
}
The Most Important Step: Backend Verification
Never trust the result on the client. An attacker could bypass your client-side logic. The integrity token MUST be sent to your backend, where it can be securely decrypted and verified using Google's APIs.
On your server, you will receive the `integrityToken` and the `nonce`.
- Get Google API Credentials: In your Google Cloud Console, enable the "Google Play Integrity API" and create a service account with the "Service Account User" and "Play Integrity API Admin" roles.
- Decrypt and Verify: Using the Google API client libraries for your backend language (e.g., Java, Node.js, Python), you will decrypt the token. The library handles the complex cryptography.
- Inspect the Verdict: The decrypted payload contains the integrity verdict. You can now check the fields.
{
"deviceIntegrity": {
"deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"]
},
"accountDetails": {
"appLicensingVerdict": "LICENSED"
},
"requestDetails": {
"requestPackageName": "com.your.app.package",
"nonce": "the-nonce-you-sent-from-the-client",
"timestampMillis": "..."
}
}
Your backend should now:
- Verify the Nonce: Does the nonce in the token match the one you originally generated for this request?
- Check Device Integrity: Does `deviceRecognitionVerdict` contain `MEETS_DEVICE_INTEGRITY`? If not, the device may be rooted, an emulator, or otherwise compromised.
- Check App Integrity: Is the `requestPackageName` correct? Is the app `LICENSED`? This helps prevent use by tampered or unofficial app versions.
Only after all these server-side checks pass should you grant access to your sensitive content or accept the request. By moving the decision-making logic to your secure backend, you create a robust defense against a wide range of threats.