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?"
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! 🔐✨
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:
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! 🦠🛡️
How Device Attestation Works
Here's the magic behind the scenes. Both iOS and Android use similar approaches with different names:
- iOS: App Attest (iOS 14+) and DeviceCheck
- Android: Play Integrity API (replaces SafetyNet)
The High-Level Flow:
Sounds complex? Let's see it in code! Here's where it gets interesting! 🎯
iOS App Attest Implementation
iOS provides the DCAppAttestService API. Let's implement it step by step.
Step 1: Check if Device Supports App Attest
import DeviceCheck
func isAppAttestSupported() -> Bool {
if #available(iOS 14.0, *) {
return DCAppAttestService.shared.isSupported
}
return false
}
Step 2: Generate an Attestation 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
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):
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)
}
}
Do the full attestKey() once during onboarding, then use lightweight generateAssertion()
for every sensitive API call. This keeps performance high while maintaining security! 💪
Android Play Integrity Implementation
Android uses the Play Integrity API (successor to SafetyNet). Let's implement it!
Step 1: Add Dependencies
dependencies {
implementation 'com.google.android.play:integrity:1.3.0'
}
Step 2: Initialize Integrity Manager
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
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
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(...)
}
- 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
React Native Integration
Now let's bridge this native code to React Native! We'll create a unified JavaScript API. 🚀
Create a Native Module
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
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);
}
}
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! 🧅🔒
Server-Side Validation
The real magic happens on your server. Let's validate the attestation! 🪄
iOS App Attest Validation
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 };
}
}
Android Play Integrity Validation
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 };
}
}
MEETS_DEVICE_INTEGRITY: Device is not rooted/tamperedPLAY_RECOGNIZED: App installed from Google PlayMEETS_BASIC_INTEGRITY: Device passes basic checks
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! 🛡️
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! 🚀
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 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
- 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 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:
- Go to Project Settings → App Check
- Select your iOS app
- Choose "App Attest" as provider
- Copy your Team ID and App ID
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();
if (__DEV__) {
await appCheck().initializeAppCheck({
provider: 'debug',
debugToken: 'YOUR_DEBUG_TOKEN', // Get from Firebase Console
});
}
Step 3: Configure Android (Play Integrity Provider)
- Go to Project Settings → App Check
- Select your Android app
- Choose "Play Integrity" as provider
- Link your Google Cloud project number
- Make sure your app is published or in internal testing on Play Console
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:
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
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! 📊
- 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
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
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;
}
- 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
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! 🎯
Best Practices & Key Takeaways
You've learned how to implement device attestation! Now let's make sure you use it effectively. 💪
🎯 Key Takeaways
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)
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
Track attestation failures to detect:
- Sudden spikes (mass attack attempt)
- Geographic patterns (bot farms)
- Device types (emulators)
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
On iOS, use the full
attestKey() once, then use lightweight generateAssertion()
for subsequent API calls. This reduces overhead by ~95%! 🚀
- 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)
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)
📚 Resources & Next Steps
- Apple: Validating Apps That Connect to Your Server
- Google: Play Integrity API Overview
- Duo Labs: App Attest Server Library
- OWASP Mobile Top 10
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. 🎯🔒