Add gradle.properties with AndroidX support, Jetifier, SDK 35 compatibility, Kotlin official code style, build caching, and JVM memory settings (3GB heap with UTF-8 encoding).
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
- Copy the environment file:
cp .env.example .env
-
Replace all secrets in
.env, setPUBLIC_BASE_URL,OIDC_ISSUER,OIDC_REDIRECT_URIS, and Firebase settings. -
Start the stack:
docker compose up --build
-
Open the admin UI at
http://localhost:8080and sign in withADMIN_TOKEN. -
Create an enrollment from the backend API:
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/startPOST /api/enroll/finishPOST /api/challengesGET /api/challenges/{id}POST /api/challenges/{id}/approvePOST /api/challenges/{id}/denyGET /healthGET /metrics
Admin endpoints require Authorization: Bearer $ADMIN_TOKEN:
GET /api/admin/usersGET /api/admin/devicesPOST /api/admin/devices/{id}/revokeGET /api/admin/challengesGET /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/metricsif 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:
cd backend
pip install -e '.[test]'
pytest
uvicorn app.main:app --reload
Frontend:
cd frontend
npm install
npm run dev
Android:
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, andOIDC_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 and docs/authentik.md.