# NexaMFA NexaMFA is an open-source, self-hosted Push MFA system similar to Duo Push. It provides a FastAPI backend, PostgreSQL persistence, Redis-ready queue/cache integration, a React TypeScript admin console, an Android Kotlin/Jetpack Compose app, Firebase Cloud Messaging push notifications, Docker Compose deployment, Prometheus metrics, and OIDC provider mode for authentik. ## Security Model Push delivery is only a wake-up signal. NexaMFA never treats a push notification as approval. Each Android device generates an asymmetric keypair in Android Keystore, stores only the public key on the server, and signs the exact challenge payload after local BiometricPrompt confirmation. The backend verifies the signature, checks challenge expiry, blocks replay by allowing only one terminal state, and rejects revoked devices immediately. Push notifications contain only `challenge_id`. ## Quick Start 1. Copy the environment file: ```bash cp .env.example .env ``` 2. Replace all secrets in `.env`, set `PUBLIC_BASE_URL`, `OIDC_ISSUER`, `OIDC_REDIRECT_URIS`, and Firebase settings. 3. Start the stack: ```bash docker compose up --build ``` 4. Open the admin UI at `http://localhost:8080` and sign in with `ADMIN_TOKEN`. 5. Create an enrollment from the backend API: ```bash curl -H 'Content-Type: application/json' \ -d '{"username":"alice","display_name":"Alice","email":"alice@example.com"}' \ http://localhost:8000/api/enroll/start ``` Scan the returned QR code with the Android app. ## Backend API Public/device endpoints: - `POST /api/enroll/start` - `POST /api/enroll/finish` - `POST /api/challenges` - `GET /api/challenges/{id}` - `POST /api/challenges/{id}/approve` - `POST /api/challenges/{id}/deny` - `GET /health` - `GET /metrics` Admin endpoints require `Authorization: Bearer $ADMIN_TOKEN`: - `GET /api/admin/users` - `GET /api/admin/devices` - `POST /api/admin/devices/{id}/revoke` - `GET /api/admin/challenges` - `GET /api/admin/audit` OIDC endpoints: - `/.well-known/openid-configuration` - `/oauth/authorize` - `/oauth/token` - `/oauth/userinfo` - `/oauth/jwks` ## Zoraxy Reverse Proxy Expose the backend public hostname, for example `https://mfa.example.com`, to container `backend:8000`. Enable HTTPS in Zoraxy and forward: - `/.well-known/openid-configuration` - `/oauth/*` - `/api/*` - `/health` - `/metrics` if Prometheus is remote and authorized by your network policy Expose the admin frontend separately, for example `https://mfa-admin.example.com`, to container `frontend:80`. Set `CORS_ORIGINS=https://mfa-admin.example.com`. ## Development Backend: ```bash cd backend pip install -e '.[test]' pytest uvicorn app.main:app --reload ``` Frontend: ```bash cd frontend npm install npm run dev ``` Android: ```bash cd android ./gradlew testDebugUnitTest assembleDebug ``` Add your Firebase `google-services.json` at `android/app/google-services.json`. ## Production Notes - Use long random values for `ADMIN_TOKEN`, `POSTGRES_PASSWORD`, and `OIDC_CLIENT_SECRET`. - Persist `OIDC_SIGNING_KEY_PEM`; changing it invalidates token verification until clients refresh JWKS. - Restrict admin UI and metrics at the reverse proxy or network layer. - Use HTTPS only. Android enrollment and challenge approval should never be sent over cleartext. - Configure Firebase service account credentials for real push delivery. See [SECURITY.md](SECURITY.md) and [docs/authentik.md](docs/authentik.md).