nessi 83f372c648
NexaMFA CI / backend (push) Successful in 15s
NexaMFA CI / frontend (push) Successful in 26s
NexaMFA CI / android (push) Failing after 3m7s
chore(android): add Gradle properties configuration
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).
2026-06-28 10:05:56 +02:00

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:
cp .env.example .env
  1. Replace all secrets in .env, set PUBLIC_BASE_URL, OIDC_ISSUER, OIDC_REDIRECT_URIS, and Firebase settings.

  2. Start the stack:

docker compose up --build
  1. Open the admin UI at http://localhost:8080 and sign in with ADMIN_TOKEN.

  2. 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/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:

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, 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 and docs/authentik.md.

S
Description
No description provided
Readme
72 KiB
Languages
Python 62.6%
Kotlin 25%
TypeScript 7.7%
CSS 3.5%
Dockerfile 1.1%