← Back to all posts
Security Deep Dive

Device Attestation:
Securing Mobile Apps 🛡️

Ever wondered how banking apps detect if they're running on a rooted phone? Or how payment apps prevent fraud? Device attestation is the invisible security layer that makes mobile apps trust (or distrust) the devices they run on. Let's build it in React Native! 🔥

📅 December 18, 2025 🔐 Critical Security Layer 📱 iOS + Android + Firebase 📖 18 min read
Scroll to learn
01

What is Device Attestation?

Picture this: You're running an online ticket platform for concerts. Someone tries to buy tickets, but how do you know it's not a bot trying to scalp thousands of tickets in seconds? 🤔

Device attestation is like asking for a "birth certificate" from the device itself. It's a cryptographic process where your app asks the device's operating system: "Hey OS, can you prove this is a genuine iPhone/Android running my real app, not some hacked emulator?"

🔧 Think of It Like This

Imagine you're trying to get into a secured building. You can't just say "I work here!" You need a security badge that:

  • ✅ Was issued by the building's security office (can't be faked)
  • ✅ Has a photo and signature that match you (proves identity)
  • ✅ Has a holographic seal that changes when you tilt it (impossible to counterfeit)
  • ✅ Gets scanned at the door to verify it's still valid (real-time check)

Device attestation works the same way! Apple/Google (the "security office") gives your app a cryptographic "badge" that proves: your app is genuine, the device hasn't been tampered with, and this request is coming from a real human, not a bot. Fraudsters can copy your app code, but they can't fake Apple or Google's cryptographic signature! 🔐✨

$32B Lost to mobile app fraud in 2024
75% Of financial apps use attestation
iOS 14+ App Attest availability
API 24+ Android Play Integrity support
02

Why Device Attestation Matters

Without device attestation, attackers can exploit your mobile app in countless ways. Here's where it gets scary! 😱

Common Attacks Device Attestation Prevents:

🎭 App Repackaging: Attacker downloads your APK/IPA, modifies it to bypass premium features or steal data, then redistributes it. Without attestation, your server can't tell the difference!
🔓 Rooted/Jailbroken Devices: On compromised devices, attackers can read your app's memory, intercept API calls, or modify app behavior. Banking apps lose millions to this.
🤖 Bot Farms: Automated scripts that abuse your API — think fake accounts, vote manipulation, or scraping. Attestation proves there's a real human with a real device.
⚙️ Reverse Engineering: Attackers use emulators or debugging tools to figure out your business logic and exploit it. Attestation detects these environments.
Key Insight

Device attestation doesn't prevent attacks by itself — it's a detection mechanism. It tells your server: "This request came from a suspicious device, so treat it with extra scrutiny or block it entirely." Think of it as your app's immune system! 🦠🛡️

03

How Device Attestation Works

Here's the magic behind the scenes. Both iOS and Android use similar approaches with different names:

The High-Level Flow:

Attestation Flow (Both iOS & Android)
Step 1
📱 App Requests Key
Step 2
🔐 OS Generates Key
Step 3
📝 Create Attestation
Step 4
🚀 Send to Server
Step 5
✅ Server Validates
🔑 Key Generation: The OS creates a unique cryptographic key pair tied to your app and device. This key lives in the device's secure hardware (Secure Enclave on iOS, Trusted Execution Environment on Android) and never leaves the device.
📜 Attestation Object: Your app sends a challenge (random data) to the OS. The OS signs it with the private key and returns a cryptographically signed attestation object containing device integrity info.
✅ Server Validation: Your backend validates the signature against Apple/Google's public keys, checks the attestation data (device integrity, app identity), and decides whether to trust the request.

Sounds complex? Let's see it in code! Here's where it gets interesting! 🎯

04

iOS App Attest Implementation

iOS provides the DCAppAttestService API. Let's implement it step by step.

Step 1: Check if Device Supports App Attest

🍎 Swift — Check App Attest Support
import DeviceCheck

func isAppAttestSupported() -> Bool {
    if #available(iOS 14.0, *) {
        return DCAppAttestService.shared.isSupported
    }
    return false
}
⚠️ Important: App Attest only works on iOS 14+ and requires a physical device (won't work on simulator). Always have a fallback strategy!

Step 2: Generate an Attestation Key

🍎 Swift — Generate Key
func generateKey(completion: @escaping (String?, Error?) -> Void) {
    guard #available(iOS 14.0, *) else {
        completion(nil, NSError(domain: "Unsupported", code: 0))
        return
    }

    DCAppAttestService.shared.generateKey { keyId, error in
        if let error = error {
            completion(nil, error)
            return
        }

        // 🔑 Store this keyId securely (UserDefaults or Keychain)
        UserDefaults.standard.set(keyId, forKey: "AppAttestKeyID")
        completion(keyId, nil)
    }
}

Step 3: Attest the Key with Your Server

🍎 Swift — Create Attestation
func attestKey(keyId: String, challenge: Data, completion: @escaping (Data?, Error?) -> Void) {
    guard #available(iOS 14.0, *) else { return }

    DCAppAttestService.shared.attestKey(keyId, clientDataHash: challenge) { attestation, error in
        if let error = error {
            print("❌ Attestation failed: \(error)")
            completion(nil, error)
            return
        }

        // ✅ Send this attestation object to your server
        completion(attestation, nil)
    }
}

Step 4: Generate Assertions for API Requests

After the initial attestation, use assertions for subsequent API calls (cheaper than full attestation):

🍎 Swift — Generate Assertion
func generateAssertion(keyId: String, requestData: Data, completion: @escaping (Data?) -> Void) {
    guard #available(iOS 14.0, *) else { return }

    // Hash the request data (e.g., JSON payload)
    let hash = SHA256.hash(data: requestData)
    let clientDataHash = Data(hash)

    DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: clientDataHash) { assertion, error in
        if let error = error {
            print("❌ Assertion failed: \(error)")
            completion(nil)
            return
        }

        // ✅ Attach this assertion to your API request header
        completion(assertion)
    }
}
🎯 Best Practice

Do the full attestKey() once during onboarding, then use lightweight generateAssertion() for every sensitive API call. This keeps performance high while maintaining security! 💪

05

Android Play Integrity Implementation

Android uses the Play Integrity API (successor to SafetyNet). Let's implement it!

Step 1: Add Dependencies

📦 Gradle — build.gradle
dependencies {
    implementation 'com.google.android.play:integrity:1.3.0'
}

Step 2: Initialize Integrity Manager

🤖 Kotlin — Setup
import com.google.android.play.core.integrity.IntegrityManager
import com.google.android.play.core.integrity.IntegrityManagerFactory

class DeviceAttestationHelper(context: Context) {
    private val integrityManager: IntegrityManager =
        IntegrityManagerFactory.create(context)
}

Step 3: Request Integrity Token

🤖 Kotlin — Request Token
fun requestIntegrityToken(
    nonce: String, // Challenge from your server
    cloudProjectNumber: Long, // Your Google Cloud project number
    callback: (String?) -> Unit
) {
    // Build the integrity token request
    val tokenRequest = IntegrityTokenRequest.builder()
        .setNonce(nonce)
        .setCloudProjectNumber(cloudProjectNumber)
        .build()

    integrityManager
        .requestIntegrityToken(tokenRequest)
        .addOnSuccessListener { response ->
            val token = response.token()
            println("✅ Got integrity token: $token")
            callback(token)
        }
        .addOnFailureListener { exception ->
            println("❌ Integrity request failed: $exception")
            callback(null)
        }
}

Step 4: Send Token to Your Server

🤖 Kotlin — API Call with Token
fun makeSecureApiCall(integrityToken: String) {
    val request = Request.Builder()
        .url("https://yourapi.com/secure-endpoint")
        .addHeader("X-Integrity-Token", integrityToken)
        .post(requestBody)
        .build()

    // Your server will validate this token!
    client.newCall(request).enqueue(...)
}
⚠️ Play Integrity Limitations:
  • Requires Google Play Services (won't work on custom ROMs without Play Store)
  • Has rate limits (10,000 requests/day for free tier)
  • Token expires after a few minutes
06

React Native Integration

Now let's bridge this native code to React Native! We'll create a unified JavaScript API. 🚀

Create a Native Module

⚛️ JavaScript — DeviceAttestation.js
import { NativeModules, Platform } from 'react-native';

const { RNDeviceAttestation } = NativeModules;

class DeviceAttestation {
  // Generate and attest a key (iOS) or request token (Android)
  static async attest(challenge) {
    try {
      if (Platform.OS === 'ios') {
        // iOS: Generate key, then attest it
        const keyId = await RNDeviceAttestation.generateKey();
        const attestation = await RNDeviceAttestation.attestKey(keyId, challenge);
        return { keyId, attestation };
      } else {
        // Android: Request integrity token
        const token = await RNDeviceAttestation.requestIntegrityToken(challenge);
        return { token };
      }
    } catch (error) {
      console.error('❌ Attestation failed:', error);
      throw error;
    }
  }

  // Generate assertion for API calls (iOS only)
  static async generateAssertion(keyId, requestData) {
    if (Platform.OS !== 'ios') {
      throw new Error('Assertions only available on iOS');
    }
    return RNDeviceAttestation.generateAssertion(keyId, requestData);
  }

  // Check if attestation is supported
  static async isSupported() {
    return RNDeviceAttestation.isSupported();
  }
}

export default DeviceAttestation;

Using It in Your App

⚛️ React Native — Login Flow
import DeviceAttestation from './DeviceAttestation';

async function handleLogin(username, password) {
  try {
    // 1. Get challenge from your server
    const challengeResponse = await fetch('https://api.example.com/auth/challenge');
    const { challenge } = await challengeResponse.json();

    // 2. Perform device attestation
    const attestationData = await DeviceAttestation.attest(challenge);

    // 3. Send login + attestation to server
    const loginResponse = await fetch('https://api.example.com/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username,
        password,
        attestation: attestationData, // 🔐 Server validates this!
      }),
    });

    if (loginResponse.ok) {
      console.log('✅ Login successful with verified device!');
    } else {
      console.error('❌ Login failed or device unverified');
    }
  } catch (error) {
    console.error('Error during attestation:', error);
  }
}
🎯 UX Tip

Always have a fallback! If attestation fails (e.g., old device, network issue), don't block the user entirely. Instead, trigger additional verification steps like 2FA or rate limiting. Security is about layers, not walls! 🧅🔒

07

Server-Side Validation

The real magic happens on your server. Let's validate the attestation! 🪄

iOS App Attest Validation

🖥️ Node.js — Validate iOS Attestation
const crypto = require('crypto');
const cbor = require('cbor'); // For decoding attestation

async function validateIOSAttestation(attestationBase64, keyId, challenge) {
  try {
    // 1. Decode the attestation object
    const attestation = Buffer.from(attestationBase64, 'base64');
    const decoded = cbor.decodeFirstSync(attestation);

    // 2. Verify the signature chain (uses Apple's root certificate)
    const cert = decoded.attStmt.x5c[0];
    const isValidCert = await verifyAppleCertificate(cert);
    if (!isValidCert) {
      return { valid: false, reason: 'Invalid certificate' };
    }

    // 3. Verify the challenge matches
    const authData = decoded.authData;
    const clientDataHash = crypto.createHash('sha256').update(challenge).digest();
    const isValidChallenge = clientDataHash.equals(decoded.clientDataHash);

    if (!isValidChallenge) {
      return { valid: false, reason: 'Challenge mismatch' };
    }

    // 4. Extract and store the public key for future assertions
    const publicKey = decoded.attStmt.x5c[0]; // Simplified
    await storePublicKey(keyId, publicKey);

    return { valid: true, publicKey };
  } catch (error) {
    console.error('Validation error:', error);
    return { valid: false, reason: error.message };
  }
}
💡 Pro Tip: Use Apple's official validation libraries like webauthn or duo-labs/apple-app-attest instead of rolling your own. Certificate validation is tricky! 😅

Android Play Integrity Validation

🖥️ Node.js — Validate Android Token
const axios = require('axios');

async function validateAndroidIntegrity(token) {
  try {
    // Call Google's Play Integrity API to decode the token
    const response = await axios.post(
      `https://playintegrity.googleapis.com/v1/your-package-name:decodeIntegrityToken`,
      { integrityToken: token },
      {
        headers: {
          'Authorization': `Bearer ${YOUR_GOOGLE_CLOUD_API_KEY}`,
        },
      }
    );

    const verdict = response.data.tokenPayloadExternal;

    // Check device integrity
    const deviceIntegrity = verdict.deviceIntegrity.deviceRecognitionVerdict;
    if (!deviceIntegrity.includes('MEETS_DEVICE_INTEGRITY')) {
      return { valid: false, reason: 'Device compromised' };
    }

    // Check app integrity (not sideloaded or tampered)
    const appIntegrity = verdict.appIntegrity.appRecognitionVerdict;
    if (appIntegrity !== 'PLAY_RECOGNIZED') {
      return { valid: false, reason: 'App not from Play Store' };
    }

    // ✅ All checks passed!
    return { valid: true, verdict };
  } catch (error) {
    console.error('Android validation error:', error);
    return { valid: false, reason: error.message };
  }
}
✅ What to Check:
  • MEETS_DEVICE_INTEGRITY: Device is not rooted/tampered
  • PLAY_RECOGNIZED: App installed from Google Play
  • MEETS_BASIC_INTEGRITY: Device passes basic checks
🔒 Security Best Practice

Never trust the client! Always validate attestations on your server. Even if your app code is perfect, an attacker can bypass it by modifying the APK/IPA. Server-side validation is your last line of defense! 🛡️

08

Firebase App Check: The Easy Way

Implementing device attestation from scratch is complex. What if there was an easier way? 🤔

Enter Firebase App Check! It's Google's managed service that handles all the heavy lifting of device attestation. Instead of manually integrating App Attest and Play Integrity, Firebase does it for you and automatically protects all your Firebase services (Firestore, Cloud Functions, Realtime Database, Storage). Think of it as attestation-as-a-service! 🚀

Why Firebase App Check?

DIY Attestation: You write ~500 lines of native code + complex server validation logic.
Firebase App Check: You add 5 lines of code. Firebase handles everything. 😎

How Firebase App Check Works

Firebase App Check Flow
Step 1
📱 App Requests Token
Step 2
🔐 Provider Attests
Step 3
✅ Firebase Issues Token
Step 4
🚀 Auto-Protected APIs

Firebase App Check uses the same underlying technologies (App Attest on iOS, Play Integrity on Android) but abstracts away all the complexity. It exchanges the platform's attestation for a short-lived Firebase App Check token that gets automatically attached to every Firebase request.

Step 1: Setup Firebase App Check

Prerequisites:
  • Firebase project configured
  • React Native Firebase installed (@react-native-firebase/app)
  • Apple Developer account for App Attest (iOS)
  • Google Cloud project for Play Integrity (Android)
📦 Install Firebase App Check
# Install the App Check package
npm install @react-native-firebase/app-check

# iOS only: Install pods
cd ios && pod install

Step 2: Configure iOS (App Attest Provider)

First, enable App Attest in your Firebase console:

Firebase Console Setup (iOS):
  1. Go to Project Settings → App Check
  2. Select your iOS app
  3. Choose "App Attest" as provider
  4. Copy your Team ID and App ID
⚛️ React Native — Initialize App Check (iOS)
import appCheck from '@react-native-firebase/app-check';
import { Platform } from 'react-native';

// Initialize App Check with App Attest provider (iOS 14+)
async function initializeAppCheck() {
  if (Platform.OS === 'ios') {
    await appCheck().initializeAppCheck({
      provider: 'appAttest',
      isTokenAutoRefreshEnabled: true, // Auto-refresh tokens
    });

    console.log('✅ iOS App Check initialized with App Attest!');
  }
}

// Call this in your App.js/index.js
initializeAppCheck();
⚠️ Development Mode: App Attest only works on physical devices with iOS 14+. For testing on simulator, use the debug provider:
🐛 Debug Provider (Development Only)
if (__DEV__) {
  await appCheck().initializeAppCheck({
    provider: 'debug',
    debugToken: 'YOUR_DEBUG_TOKEN', // Get from Firebase Console
  });
}

Step 3: Configure Android (Play Integrity Provider)

Firebase Console Setup (Android):
  1. Go to Project Settings → App Check
  2. Select your Android app
  3. Choose "Play Integrity" as provider
  4. Link your Google Cloud project number
  5. Make sure your app is published or in internal testing on Play Console
⚛️ React Native — Initialize App Check (Android)
async function initializeAppCheck() {
  if (Platform.OS === 'android') {
    await appCheck().initializeAppCheck({
      provider: 'playIntegrity',
      isTokenAutoRefreshEnabled: true,
    });

    console.log('✅ Android App Check initialized with Play Integrity!');
  }
}

Step 4: Protect Your Backend (Custom APIs)

Firebase automatically protects Firebase services, but what about your custom Node.js/Express backend? Easy! Just validate the App Check token:

🖥️ Node.js Backend — Validate App Check Token
const admin = require('firebase-admin');

// Initialize Firebase Admin SDK
admin.initializeApp({
  credential: admin.credential.applicationDefault(),
});

// Middleware to verify App Check token
async function verifyAppCheckToken(req, res, next) {
  const appCheckToken = req.headers['x-firebase-appcheck'];

  if (!appCheckToken) {
    return res.status(401).json({ error: 'Missing App Check token' });
  }

  try {
    // Verify the token with Firebase
    const appCheckClaims = await admin.appCheck().verifyToken(appCheckToken);

    // ✅ Token is valid! Request came from your legitimate app
    console.log('✅ App Check verified:', appCheckClaims);
    next();
  } catch (error) {
    // ❌ Token invalid or expired
    console.error('❌ App Check verification failed:', error);
    return res.status(401).json({ error: 'Invalid App Check token' });
  }
}

// Protect your endpoints
app.post('/api/purchase-ticket', verifyAppCheckToken, async (req, res) => {
  // This code only runs if App Check passes!
  const { ticketId, userId } = req.body;

  // Process payment safely knowing it's from a real device
  const result = await processPurchase(ticketId, userId);
  res.json(result);
});

Step 5: Send App Check Tokens from React Native

⚛️ React Native — Attach Token to API Calls
import appCheck from '@react-native-firebase/app-check';

async function purchaseTicket(ticketId) {
  try {
    // Get a fresh App Check token
    const { token } = await appCheck().getToken(true); // Force refresh

    // Make API call with the token
    const response = await fetch('https://api.example.com/api/purchase-ticket', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Firebase-AppCheck': token, // 🔐 Attach the token!
      },
      body: JSON.stringify({ ticketId, userId: 'user123' }),
    });

    if (response.ok) {
      console.log('✅ Purchase successful!');
    } else {
      console.error('❌ Purchase failed - might be a suspicious device');
    }
  } catch (error) {
    console.error('Error getting App Check token:', error);
  }
}

Monitoring & Analytics

One of the best parts of Firebase App Check? Built-in monitoring! 📊

Firebase Console Metrics:
  • Request Volume: See how many requests pass/fail App Check
  • Failure Rates: Identify suspicious traffic patterns
  • Provider Health: Monitor App Attest/Play Integrity availability
  • Token Refresh Rates: Track performance impact
Enforcement Modes
Monitor Mode
📊 Log failures, allow all
Enforce Mode
🛡️ Block invalid requests
🎯 Pro Tip: Start with Monitor Mode

Firebase App Check has two enforcement modes:

  • Monitor: Logs failures but allows all traffic (great for testing)
  • Enforce: Actively blocks requests without valid tokens

Start in monitor mode for 1-2 weeks to establish a baseline. Check your Firebase console to see: what percentage of traffic would be blocked, identify false positives (legit users on old devices), then switch to enforce mode once you're confident. This prevents accidentally blocking real users! 🚦

Handling Edge Cases

⚛️ React Native — Graceful Degradation
async function purchaseTicketWithFallback(ticketId) {
  let appCheckToken = null;

  try {
    // Try to get App Check token
    const { token } = await appCheck().getToken(true);
    appCheckToken = token;
  } catch (error) {
    console.warn('⚠️ App Check unavailable, proceeding without it', error);
    // Continue without token - server can apply stricter rate limits
  }

  const headers = {
    'Content-Type': 'application/json',
  };

  // Attach token if available
  if (appCheckToken) {
    headers['X-Firebase-AppCheck'] = appCheckToken;
  }

  const response = await fetch('https://api.example.com/api/purchase-ticket', {
    method: 'POST',
    headers,
    body: JSON.stringify({ ticketId }),
  });

  return response;
}
⚠️ Firebase App Check Limitations:
  • iOS: Requires iOS 14+, won't work on older devices
  • Android: Requires Google Play Services (won't work on custom ROMs)
  • Tokens expire: Default lifetime is 1 hour, plan for refresh failures
  • Not foolproof: Sophisticated attackers can still bypass (nothing is 100%)
  • Firebase only: Out-of-box protection only works with Firebase services

Firebase App Check vs Manual Implementation

5 min Firebase App Check setup time
2-3 days Manual implementation time
$0 Firebase App Check cost (Spark plan)
Auto Token refresh & management
When to Use What?

Use Firebase App Check if:

  • ✅ You're already using Firebase (Firestore, Cloud Functions, etc.)
  • ✅ You want quick setup without managing native code
  • ✅ You need built-in monitoring and analytics
  • ✅ You're okay with Firebase dependency

Use Manual Implementation (App Attest/Play Integrity) if:

  • ✅ You want full control over the attestation flow
  • ✅ You're not using Firebase and don't want to add it
  • ✅ You need custom token validation logic
  • ✅ You want to avoid vendor lock-in

For most React Native apps, Firebase App Check is the sweet spot between security and developer experience! 🎯

09

Best Practices & Key Takeaways

You've learned how to implement device attestation! Now let's make sure you use it effectively. 💪

🎯 Key Takeaways

1. Layer Your Security
Device attestation is one layer of defense. Combine it with:
  • TLS certificate pinning
  • API rate limiting
  • Behavioral analysis (e.g., velocity checks)
  • User authentication (JWT, OAuth)
2. Always Have a Fallback
Don't block users completely if attestation fails. Instead:
  • Trigger 2FA/MFA
  • Apply stricter rate limits
  • Flag for manual review
  • Allow access with reduced functionality
3. Monitor and Analyze
Track attestation failures to detect:
  • Sudden spikes (mass attack attempt)
  • Geographic patterns (bot farms)
  • Device types (emulators)
Use tools like Datadog, Sentry, or custom analytics.
4. Handle Edge Cases
Be prepared for:
  • Old devices: iOS <14 or Android <7 don't support attestation
  • Custom ROMs: Legitimate users might have rooted devices for valid reasons
  • Rate limits: Play Integrity has daily quotas
  • Network issues: Attestation can fail due to connectivity
5. Use Assertions for Performance
On iOS, use the full attestKey() once, then use lightweight generateAssertion() for subsequent API calls. This reduces overhead by ~95%! 🚀
6. Secure Your Server Validation
  • Validate certificates against Apple/Google's root CAs
  • Check nonce/challenge matches to prevent replay attacks
  • Verify app identifier and team ID
  • Store public keys securely (encrypted database, not plain text)
7. Combine iOS and Android Strategies
Your React Native app needs to handle both platforms gracefully:
  • iOS: One-time attestation + reusable assertions
  • Android: Fresh token per critical action (tokens expire quickly)
Design your API to accept both formats!

📚 Resources & Next Steps

🎓 Final Thought

Device attestation isn't a silver bullet, but it's a critical component of mobile app security. Think of it like a car's airbag — you hope you never need it, but you're glad it's there! By combining attestation with other security measures, you create a robust defense that makes attackers' lives much harder. 🎯🔒

Found this helpful? Share it! 🚀