import * as functions from 'firebase-functions' import { GoogleAuth } from 'google-auth-library' const PACKAGE_NAME = 'app.closer' const PLAY_INTEGRITY_URL = `https://playintegrity.googleapis.com/v1/${PACKAGE_NAME}:decodeIntegrityToken` /** * Verifies a Play Integrity API token server-side and returns whether the * device meets basic integrity requirements. * * Called by [PlayIntegrityChecker] on app startup. Requires the caller to be * authenticated — the Firebase Auth token is verified automatically by the * Functions runtime. * * Setup required before this function works in production: * 1. Enable the Play Integrity API in your Google Cloud project. * 2. Link the Cloud project to your Play Console app (Play Console → * Setup → API access → Link to Cloud project). * 3. Grant the Cloud Functions service account the "Play Integrity API User" * role in IAM, or use a dedicated service account key via * GOOGLE_APPLICATION_CREDENTIALS. * * If the Play Integrity API is not configured the function fails-closed * (returns passed: false). Configure the API and grant the service account * the "Play Integrity API User" IAM role before deploying to production. */ export const checkDeviceIntegrity = functions.https.onCall( async (data: { token?: string }, context) => { if (!context.auth) { throw new functions.https.HttpsError( 'unauthenticated', 'Caller must be authenticated.' ) } if (!context.app) { throw new functions.https.HttpsError( 'failed-precondition', 'App Check verification required.' ) } const token = data?.token if (!token || typeof token !== 'string') { throw new functions.https.HttpsError( 'invalid-argument', 'token is required.' ) } try { const verdicts = await decodeIntegrityToken(token) const passed = verdicts.includes('MEETS_DEVICE_INTEGRITY') || verdicts.includes('MEETS_STRONG_INTEGRITY') return { passed, verdicts } } catch (err) { console.error('[checkDeviceIntegrity] verification failed:', err) // Fail-closed: an unverifiable request is treated as failed, not passed. // Ensure the Play Integrity API is enabled and the service account has // "Play Integrity API User" role before deploying to production. return { passed: false, verdicts: [], error: 'verification_unavailable' } } } ) interface IntegrityResponse { tokenPayloadExternal?: { deviceIntegrity?: { deviceRecognitionVerdict?: string[] } } } async function decodeIntegrityToken(token: string): Promise { const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/playintegrity'], }) const client = await auth.getClient() const response = await client.request({ url: PLAY_INTEGRITY_URL, method: 'POST', data: { integrity_token: token }, }) return ( response.data.tokenPayloadExternal?.deviceIntegrity ?.deviceRecognitionVerdict ?? [] ) }