If we build the Autharky app
A small dedicated Android client is feasible. It should treat FCM only as a wake-up transport and make the device's cryptographic key—not the notification payload—the factor.
Enrollment
The authenticated Security page creates a short-lived, single-use enrollment QR.
The app generates a non-exportable P-256 signing key in Android Keystore.
The app sends its public key, device name, push token and enrollment proof to Autharky.
Autharky stores a tenant/user-scoped device. Removal revokes it immediately.
Authentication
After primary login, Autharky creates a random challenge bound to tenant, user, browser session, client, nonce and a 60–90 second expiry.
FCM delivers a high-priority signed challenge with no reusable credential.
The app verifies Autharky's signature and shows client, time/IP context and a short number also visible in the browser.
Approve and Deny are native notification actions. On Android 12+, Approve requires device authentication before execution.
The device signs its decision and posts it to Autharky.
Autharky verifies the enrolled public key and atomically changes pending to approved or denied. The original browser consumes approval once and records push in amr.
Non-negotiables: one outstanding challenge, resend cooldown, number matching, rate limits, audit events, device removal, token rotation, fallback factors, replay protection, tenant/session binding and atomic concurrent approval/denial handling.
FCM data is not end-to-end encrypted by default. Send only a signed opaque challenge and minimal display data, or encrypt the payload to the enrolled device. Never put a reusable Authorization header or approval secret in a notification.