" * and the backend verifies the token with Google's public keys. * Uses FCM_PROJECT_ID (or FIREBASE_PROJECT_ID) for aud/iss validation. */ if (!function_exists('firebase_auth_get_bearer_token')) { /** * Get Bearer token from Authorization header. * * @return string|null Token or null if missing/invalid */ function firebase_auth_get_bearer_token() { $auth = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : ''; if ($auth === '' && function_exists('apache_request_headers')) { $headers = apache_request_headers(); $auth = isset($headers['Authorization']) ? $headers['Authorization'] : (isset($headers['authorization']) ? $headers['authorization'] : ''); } if (preg_match('/^\s*Bearer\s+(\S+)\s*$/i', $auth, $m)) { return $m[1]; } return null; } } if (!function_exists('firebase_auth_base64url_decode')) { function firebase_auth_base64url_decode($data) { $pad = 4 - (strlen($data) % 4); if ($pad !== 4) { $data .= str_repeat('=', $pad); } return base64_decode(strtr($data, '-_', '+/')); } } if (!function_exists('firebase_auth_fetch_google_jwks')) { /** * Fetch and cache Firebase Auth public keys (JWK set). * * @return array|null JWK set array or null on failure */ function firebase_auth_fetch_google_jwks() { $cache_key = 'firebase_auth_jwks'; $cache_ttl = 3600; if (function_exists('get_instance')) { $ci = &get_instance(); if (isset($ci->cache) && method_exists($ci->cache, 'get')) { $cached = $ci->cache->get($cache_key); if ($cached !== false) { return $cached; } } } $url = 'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com'; $ctx = stream_context_create(array('http' => array('timeout' => 10))); $raw = @file_get_contents($url, false, $ctx); if ($raw === false) { log_message('error', 'Firebase Auth: Failed to fetch JWKs'); return null; } $jwks = json_decode($raw, true); if (empty($jwks['keys'])) { log_message('error', 'Firebase Auth: Invalid JWKs response'); return null; } if (function_exists('get_instance')) { $ci = &get_instance(); if (isset($ci->cache) && method_exists($ci->cache, 'save')) { $ci->cache->save($cache_key, $jwks, $cache_ttl); } } return $jwks; } } if (!function_exists('firebase_auth_jwk_to_pem')) { /** * Convert JWK (RSA n,e) to PEM for openssl. * * @param array $jwk JWK with kty RSA, n, e * @return string|false PEM or false */ function firebase_auth_jwk_to_pem($jwk) { if (empty($jwk['n']) || empty($jwk['e']) || (isset($jwk['kty']) && $jwk['kty'] !== 'RSA')) { return false; } $n = firebase_auth_base64url_decode($jwk['n']); $e = firebase_auth_base64url_decode($jwk['e']); if ($n === false || $e === false) { return false; } $rsa = array( 'n' => $n, 'e' => $e, ); $der = firebase_auth_build_rsa_der($rsa); if ($der === false) { return false; } $pem = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($der), 64, "\n") . "-----END PUBLIC KEY-----"; return $pem; } } if (!function_exists('firebase_auth_build_rsa_der')) { function firebase_auth_build_rsa_der($rsa) { $n = $rsa['n']; $e = $rsa['e']; $n_len = strlen($n); $e_len = strlen($e); if ($n[0] & "\x80") { $n_len++; } if ($e[0] & "\x80") { $e_len++; } $seq = "\x02" . chr($e_len) . ($e[0] & "\x80" ? "\0" : '') . $e . "\x02" . chr($n_len) . ($n[0] & "\x80" ? "\0" : '') . $n; $bit_string = "\x03" . chr(strlen($seq) + 1) . "\x00" . $seq; $oid = "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00"; $inner = "\x30" . firebase_auth_der_len(strlen($bit_string) + strlen($oid)) . $bit_string . $oid; $outer = "\x30" . firebase_auth_der_len(strlen($inner)) . $inner; return $outer; } } if (!function_exists('firebase_auth_der_len')) { function firebase_auth_der_len($len) { if ($len < 128) { return chr($len); } $buf = ''; while ($len > 0) { $buf = chr($len & 0xff) . $buf; $len >>= 8; } return chr(0x80 | strlen($buf)) . $buf; } } if (!function_exists('firebase_auth_verify_id_token')) { /** * Verify a Firebase ID token (JWT) and return decoded payload. * * @param string $id_token The JWT string * @param string|null $project_id Firebase project ID (default: FCM_PROJECT_ID or FIREBASE_PROJECT_ID constant) * @return array|null Payload (uid, email, etc.) or null if invalid */ function firebase_auth_verify_id_token($id_token, $project_id = null) { if (!is_string($id_token) || $id_token === '') { return null; } $parts = explode('.', $id_token); if (count($parts) !== 3) { return null; } $header = json_decode(firebase_auth_base64url_decode($parts[0]), true); $payload = json_decode(firebase_auth_base64url_decode($parts[1]), true); if (!$header || !$payload) { log_message('debug', 'Firebase Auth: Invalid JWT decode'); return null; } if ($project_id === null || $project_id === '') { if (defined('FCM_PROJECT_ID') && FCM_PROJECT_ID !== '') { $project_id = FCM_PROJECT_ID; } elseif (defined('FIREBASE_PROJECT_ID') && FIREBASE_PROJECT_ID !== '') { $project_id = FIREBASE_PROJECT_ID; } else { $project_id = ''; } } if ($project_id === '') { log_message('error', 'Firebase Auth: FCM_PROJECT_ID not set'); return null; } $expected_iss = 'https://securetoken.google.com/' . $project_id; if (empty($payload['iss']) || $payload['iss'] !== $expected_iss) { log_message('debug', 'Firebase Auth: iss mismatch'); return null; } if (empty($payload['aud']) || $payload['aud'] !== $project_id) { log_message('debug', 'Firebase Auth: aud mismatch'); return null; } if (empty($payload['exp']) || (int) $payload['exp'] < time()) { log_message('debug', 'Firebase Auth: token expired'); return null; } $jwks = firebase_auth_fetch_google_jwks(); if (!$jwks) { return null; } $kid = isset($header['kid']) ? $header['kid'] : null; $key = null; foreach ($jwks['keys'] as $k) { if (isset($k['kid']) && $k['kid'] === $kid) { $key = $k; break; } } if (!$key) { $key = $jwks['keys'][0]; } $pem = firebase_auth_jwk_to_pem($key); if (!$pem) { return null; } $pub = openssl_pkey_get_public($pem); if ($pub === false) { return null; } $payload_to_verify = $parts[0] . '.' . $parts[1]; $sig_raw = firebase_auth_base64url_decode($parts[2]); if ($sig_raw === false) { openssl_free_key($pub); return null; } $ok = openssl_verify($payload_to_verify, $sig_raw, $pub, OPENSSL_ALGO_SHA256); openssl_free_key($pub); if ($ok !== 1) { log_message('debug', 'Firebase Auth: signature verification failed'); return null; } return $payload; } }