Verify app attestation on your backend, never in the app. On Android, decode a Play Integrity token through Google’s server API and check that its request hash, package name and verdicts match the request you received. On iOS, validate the App Attest key once, then verify a signed assertion with that key on every sensitive request. In both cases, bind the evidence to the exact operation, reject replays and decide in advance what happens when no evidence is available. Attestation tells you the app and device are genuine. It does not authenticate the user or prove the account may perform the action.
Prepare the trust configuration
List the app identifiers, distribution environments and sensitive endpoints first. Assign owners for server credentials, trust material, policy changes and recovery. Keep development and production evidence apart.
| Platform | What you set up | Who checks what |
|---|---|---|
| Android, Play Integrity standard request | Google Cloud project linked to the app in Play Console; a service account allowed to call the decode API | Google signs and decrypts the token; your server checks request details and verdicts |
| iOS, App Attest | App Attest capability and the com.apple.developer.devicecheck.appattest-environment entitlement; server challenge and key storage | Your server validates the first attestation and every later assertion |
Agree a stable, canonical representation of the action to protect: operation name plus the parameters that matter for authorisation. App and server must serialise it the same way. A digest of two different serialisations fails even for a legitimate user.
Otukan app attestation supplies application authenticity evidence, and Otukan runtime evidence feeds the same server decision. Your backend combines it with the platform verdict and the account context before it authorises the request.
Android: Play Integrity standard requests
Warm up the token provider
Call prepareIntegrityToken() on a StandardIntegrityManager well before the protected action, for example at app start or when the user opens the flow. Preparation can take several seconds. The returned StandardIntegrityTokenProvider then issues tokens quickly when the user acts. If a request fails with INTEGRITY_TOKEN_PROVIDER_INVALID, prepare a new provider and retry once.
Request, decode and check
- For the protected action, compute a SHA-256 digest of its canonical form and pass it with
setRequestHash(). Never place raw personal data in this field. - Send the token with the request. On the server, call the Play Integrity
decodeIntegrityTokenmethod with server-held credentials, never credentials shipped in the app. - In
requestDetails, checkrequestPackageName, recompute the digest from the request you actually received and compare it withrequestHash, and checktimestampMillisagainst your freshness window. - Read
appIntegrity.appRecognitionVerdict(PLAY_RECOGNIZEDfor your Play build),deviceIntegrity.deviceRecognitionVerdictand, if relevant,accountDetails.appLicensingVerdict. AnUNEVALUATEDverdict is not a pass. - Apply account authorisation and your own duplicate-submission control before committing.
Standard requests include Google’s replay protection for the token. Your business operation still needs its own idempotency handling.
Quotas
Play Integrity has a default daily request quota per app, shared across standard and classic requests. Estimate your peak volume, request only on sensitive actions rather than on every screen, and ask Google for a higher quota before launch if you need it. Monitor quota errors and route them to your unavailable-evidence policy.
iOS: App Attest
Check availability first
Read DCAppAttestService.shared.isSupported before generating a key. It returns false in the Simulator, on devices that cannot attest and in some app extensions. Treat that as “no evidence”, not as an attack, and handle it under the same policy as an outage. Apple also advises rolling App Attest out gradually to a large existing user base, because key attestation calls go to Apple’s servers.
Attest the key once
- Call
generateKey()and keep the returned key identifier on the device. - Ask your server for a random, single-use challenge. Hash it and pass it to
attestKey(_:clientDataHash:). - On the server, validate the attestation object as Apple documents: certificate chain to the Apple App Attestation root, nonce derived from the challenge, key identifier, RP ID hash of your Team ID and bundle ID, a counter of zero, the
aaguidfor the expected environment and the credential ID. - Store the verified public key with the app instance and user association, keeping development and production keys apart.
Verify an assertion on each sensitive request
- Issue a new challenge and include it in the client data that describes the action.
- The app calls
generateAssertion(_:clientDataHash:)with the hash of that client data. - The server verifies the signature with the stored key, the RP ID hash and the embedded challenge.
- Require the counter to be higher than the stored value, then store the new value. Reject a reused challenge. Coordinate counter updates across server instances so concurrent requests cannot slip through.
Keys do not move to a new device through backup and restore, and reinstalling the app loses them. When the server sees an unknown key or the app receives an invalid-key error, run attestation again rather than locking the user out.
Test rejection, retry and outage paths
Use authorised test accounts and non-production operations. Start with an accepted request, then change one property at a time so each rejection has a clear cause.
| Case | Expected handling |
|---|---|
| Legitimate request, valid evidence | Proceed to normal account authorisation |
| Changed action parameters | Binding mismatch; the action does not run |
| Wrong package, Team ID or environment | Evidence from the wrong scope is rejected |
| Reused assertion or consumed challenge | Replay rejected without repeating a transaction |
| Expired evidence | Fresh evidence requested through a bounded retry |
isSupported false, quota reached or service outage | The endpoint’s explicit unavailable-evidence policy applies |
| Duplicate legitimate submission | Original outcome returned |
Log invalid evidence and unavailable evidence separately. Never let verification failure fall back to approval. A content screen and an account-recovery change need not share one outage policy. Keep support reachable and avoid retry loops that keep prompting the user.
Record app, OS, protection and verifier versions with expected and actual results. Repeat install, update, sign-in and the protected flow, plus concurrent requests and a server restart for replay state. Keep raw tokens and assertions out of general logs.
- Trust configuration is owned and versioned.
- Android and iOS evidence is tested separately.
- Token providers are prepared ahead of the protected action.
- Challenge consumption and counter updates survive concurrency.
- Missing evidence never becomes a successful check.
- Account authorisation stays independent of attestation.
For product scope, see mobile app attestation and iOS app attestation. For the in-app side of the same decision, see how to handle runtime threat signals.
Further reading
- Make a standard API request — Android Developers
- Integrity verdicts — Android Developers
- Establishing your app’s integrity — Apple Developer Documentation
- Validating apps that connect to your server — Apple Developer Documentation
- Preparing to use the App Attest service — Apple Developer Documentation