Live website · Try it live · App Store · Google Play
MIEAuth is a self-hosted push-authentication service. It combines a Meteor server, a React web experience, and Cordova applications for iOS and Android. External systems can enroll users, send approval requests, and wait for an approve, reject, or timeout response. A Duo-compatible API allows existing Duo Auth and Admin API clients to use the same users and devices.
This repository contains the server, mobile and web clients, administration dashboard, operational scripts, and release pipelines.
- What MIEAuth does
- Architecture
- Technology
- Getting started
- Configuration
- Development workflow
- API surface
- Mobile development
- Testing and code quality
- Operations and deployment
- Project structure
- Troubleshooting
- Known constraints
- Creates one-time, 48-hour registration invites by email or QR code.
- Registers one or more mobile devices per user and tracks device approval.
- Sends Firebase Cloud Messaging (FCM) approval requests to approved devices.
- Correlates phone or paired-watch actions with the originating HTTP request.
- Keeps notification history and active response state in MongoDB.
- Exposes Duo Auth API v2 and Duo Admin API v1 compatibility endpoints.
- Provides an LDAP-protected admin dashboard for users, devices, API keys, Duo integrations, and email history.
- Serves public landing, FAQ, privacy, support, and account-deletion pages.
- Supports biometric sign-in, invite deep links, QR scanning, and mobile session locking.
Apple Watch and Wear OS support uses mirrored phone notifications; there is no standalone watch application. Approval actions taken on a watch are processed by the paired phone and attributed to that phone's device UUID.
flowchart LR
Integrator[External system] -->|Invite and approval REST APIs| Server
Duo[Duo-compatible client] -->|Signed Auth v2 / Admin v1 requests| Server
Admin[Administrator] -->|LDAP session| Dashboard[Admin dashboard]
Dashboard --> Server[Meteor server]
Web[React web client] <-->|DDP and HTTP| Server
Mobile[React + Cordova mobile app] <-->|DDP| Server
Server <-->|Collections and pending responses| Mongo[(MongoDB)]
Server -->|Firebase Admin SDK| FCM[Firebase Cloud Messaging]
FCM --> Mobile
Mobile -.->|Mirrored actionable notification| Watch[Apple Watch / Wear OS]
Server -->|SMTP| Mail[Mail service]
- An integrator calls
POST /send-notificationfor a username. - The server validates the request, finds approved devices, records the notification, and sends an FCM message to each approved device.
- A pending response is stored in MongoDB and polled for up to 25 seconds.
- The user approves or rejects from the app, notification tray, or paired watch. The client resolves the pending response through a Meteor method.
- The original HTTP request returns
approve,reject, ortimeout.
MongoDB-backed pending responses allow the HTTP request and mobile response to land on different application instances. Notification state is also synced to the user's other devices.
| Area | Current implementation |
|---|---|
| Application platform | Meteor 3.4 and Node.js 20 in CI/deployment |
| Client | React 18, React Router 6, @mieweb/ui, Framer Motion |
| Mobile | Cordova for iOS and Android |
| Server | Meteor methods, publications, and WebApp HTTP handlers |
| Data | MongoDB through Meteor collections |
| Push | Firebase Admin SDK and @havesource/cordova-plugin-push 7 |
| Admin authentication | LDAP bind, group membership, in-memory bearer sessions |
| Integration authentication | PBKDF2-hashed API keys and Duo request signatures |
| Build | Meteor Rspack, SWC minimization, PostCSS, Tailwind CSS 4 |
| Quality | Mocha, ESLint 9, Prettier 3, Husky, lint-staged |
For server and web development:
- Node.js 20 and npm
- Meteor 3.4
- Git
Meteor starts a local MongoDB automatically for normal local development. Use
MONGO_URL when connecting to an external database.
Mobile builds additionally require:
- Java 17 and Android Studio/Android SDK for Android
- macOS and Xcode for iOS
- Firebase Android and iOS application configuration files
- Signing credentials for distributable builds
git clone https://github.com/mieweb/mieweb_auth_app.git
cd mieweb_auth_app
npm installnpm install also installs the Husky pre-commit hook.
For the basic web and server application:
npm run startThis generates public/buildInfo.json and runs meteor run, normally at
http://localhost:3000. It does not load settings.json explicitly. To
load the checked-in Meteor settings file, run:
meteor run --settings settings.jsonThe web pages and server can run without Firebase, but push delivery is
disabled until FIREBASE_SERVICE_ACCOUNT_JSON is configured. Email-backed
invite flows also require mail configuration and EMAIL_FROM.
There is no committed local seed or fixture that creates users, API keys, LDAP accounts, Firebase credentials, or signing credentials. Those must come from the deployment environment or be created with the management tools below.
npm run start-devThis is an organization-specific workflow. bin/start.mjs requests a YubiKey
OTP, registers a temporary public URL with the BlueHive Magic Box service,
sets ROOT_URL, chooses an available port, and starts Meteor with
settings.json. Arguments such as android-device are passed through and the
generated URL is used as --mobile-server.
Developers without access to that service should use npm run start or
meteor run --settings settings.json instead.
The application loads normal process environment variables and .env through
dotenv. Keep secrets outside version control. The private/,
server/private/, .env*, signing-key, and local set-env*.sh paths are
ignored by Git, but they may exist in a developer workspace.
| Variable | Required | Purpose |
|---|---|---|
ROOT_URL |
Production | Canonical application URL; also used for internal notification calls and generated absolute URLs. |
PORT |
No | Listening port; Meteor defaults to 3000. |
MONGO_URL |
Production/external DB | MongoDB connection string. Standalone management scripts also use it. |
FIREBASE_SERVICE_ACCOUNT_JSON |
Push notifications | Complete Firebase service-account JSON serialized as one environment value. Invalid or absent JSON disables push. |
MAIL_URL |
SMTP URL consumed by Meteor Email. | |
SENDGRID_API_KEY |
Alternative email setup | If MAIL_URL is absent, the server constructs a SendGrid SMTP URL. |
EMAIL_FROM |
Invite/support/account email | Sender address used by current email flows. |
EMAIL_ADMIN |
Support/account workflows | One or more administrative recipient addresses as expected by the server. |
SEND_NOTIFICATION_FORCE_AUTH |
Recommended in production | Set to "true" to require an API key on /send-notification. The default permits unauthenticated requests. |
INTERNAL_SERVER_SECRET |
Multi-instance production | Shared secret for server-to-server notification calls. A random per-process fallback is suitable only for local, single-instance use. |
DUO_SECRET_ENCRYPTION_KEY |
Duo in production | Exactly 64 hexadecimal characters (32 bytes), used for AES-256-GCM encryption of Duo secret keys at rest. Without it, Duo secrets are stored in plaintext. |
APP_URL, ADMIN_EMAIL, and FROM_EMAIL are referenced only by the legacy
device-approval email helper. Current invite, support, and account workflows
use ROOT_URL, EMAIL_ADMIN, and EMAIL_FROM.
| Variable | Required | Purpose |
|---|---|---|
LDAP_URL |
Yes | One or more comma-separated ldap:// or ldaps:// URLs, tried in order on connection failure. |
LDAP_BASE_DN |
Configured by deployments | Directory base DN; retained in the current LDAP configuration. |
LDAP_USER_BASE_DN |
Yes | Base used to construct the user's bind DN. |
LDAP_ADMIN_GROUP_DN |
Yes | Group whose members may access the admin dashboard. |
LDAP_USER_RDN_ATTR |
No | User RDN attribute; defaults to uid. Some directories use cn. |
LDAP_GROUP_MEMBER_ATTR |
No | Group membership attribute; defaults to memberUid. Common member and uniqueMember forms are also checked. |
LDAP_BIND_DN |
Directory-dependent | Service-account DN for group lookup when anonymous search is not allowed. |
LDAP_BIND_PASSWORD |
With LDAP_BIND_DN |
Service-account password. |
LDAP_REJECT_UNAUTHORIZED |
No | TLS verification is enabled unless set to "false". Do not disable it in production. |
Admin passwords are encrypted in the browser with an ephemeral RSA-2048 public key before being submitted, then validated with LDAP bind. HTTPS is still required in production.
settings.json defines a public FCM flag and configures Meteor account session
storage. Pass it with --settings settings.json when those settings are
required. The file is tracked, despite also matching an entry in .gitignore,
so do not add secrets to it.
mobile-config.js expects both files during a Cordova build:
| Platform | Local source path | Packaged destination |
|---|---|---|
| Android | private/android/google-services.json |
app/google-services.json |
| iOS | private/ios/GoogleService-Info.plist |
GoogleService-Info.plist |
The CI workflows create both paths, including a placeholder for the platform not being built, because the Cordova configuration references both resources.
| Command | Purpose |
|---|---|
npm run start |
Generate build metadata and run the local Meteor app. |
npm run start-dev |
Run through the private YubiKey/Magic Box tunnel launcher. |
npm test |
Run the Meteor Mocha suite once. |
npm run test-app |
Run the full-app test target in watch mode. |
npm run lint |
Check JavaScript, JSX, and MJS with ESLint. |
npm run lint:fix |
Apply ESLint fixes. |
npm run format |
Format the repository with Prettier. |
npm run format:check |
Check formatting without changing files. |
npm run visualize |
Build in production mode with Meteor's bundle visualizer. |
npm run prebuild |
Regenerate version, commit, and build-date metadata. |
The pre-commit hook runs lint-staged: JavaScript files are formatted and
linted, while JSON, Markdown, CSS, and HTML files are formatted.
All standalone management scripts connect directly to MongoDB. Set MONGO_URL
to the same application database before using them. Their local fallback is
mongodb://localhost:3001/meteor, except the migration script, whose fallback
is mongodb://localhost:27017/meteor.
# API keys
node manage-api-keys.js generate <client-id>
node manage-api-keys.js list
node manage-api-keys.js regenerate <client-id>
node manage-api-keys.js delete <client-id>
# Duo integrations; type defaults to auth
node manage-duo-integrations.js generate <name> [auth|admin]
node manage-duo-integrations.js list
node manage-duo-integrations.js enable <name>
node manage-duo-integrations.js disable <name>
node manage-duo-integrations.js regenerate <name>
node manage-duo-integrations.js delete <name>API keys and Duo secret keys are displayed only when created or rotated. Store them immediately in a secret manager.
generate-build-info.js reads the version from mobile-config.js, reads the
current Git commit and date, and writes public/buildInfo.json. The support UI
uses this generated file.
Generate all configured iOS and Android icons and launch screens with Pillow:
python3 -m pip install Pillow
python3 generate_app_resources.py path/to/source.png public/resourcesThe source should be a high-resolution square PNG. The script removes alpha
from iOS icons, preserves Android transparency, and creates the filenames
referenced by mobile-config.js.
| Method | Path | Authentication | Purpose |
|---|---|---|---|
POST |
/api/invite |
API key in Authorization: Bearer ... |
Create and email a one-time registration invite. |
POST |
/send-notification |
Request-body API key when enforcement is enabled, or internal secret header | Send an approval request and wait up to 25 seconds for a response. |
GET, HEAD |
/healthcheck |
None | Report MongoDB reachability and writable-primary status. |
GET |
/api/pending-responses |
None | Return up to 100 response records for monitoring. |
| Various | /auth/v2/* |
Duo signed request | Duo Auth API compatibility: ping, check, preauth, enroll, auth, and QR generation. |
| Various | /admin/v1/* |
Duo signed request | Duo Admin API compatibility for user and phone synchronization. |
The invite request and response contract is documented in docs/API_INVITES.md. API-key behavior and a complete notification example are in docs/API_KEY_AUTHENTICATION.md.
Minimal notification payload:
{
"username": "alice",
"title": "Sign-in request",
"body": "Approve sign-in to the example service?",
"actions": [
{ "title": "Approve", "callback": "approve" },
{ "title": "Reject", "callback": "reject" }
],
"apikey": "optional-or-required-by-server-policy",
"client_id": "example-service"
}Successful delivery returns an action such as:
{
"success": true,
"action": "approve",
"message": "Notification sent successfully"
}timeout is also a successful, expected action when the user does not respond
within the request window.
Open /admin after configuring LDAP. The dashboard manages:
- API keys
- users and registration approval
- devices and device approval/revocation
- outgoing email records
- Duo Auth and Admin integrations
Admin API calls use an eight-hour bearer session created after LDAP group and credential validation. See docs/ADMIN_DASHBOARD.md for the endpoint reference.
Meteor already tracks both mobile platforms in .meteor/platforms. With the
native toolchains and Firebase files configured:
# Android emulator
meteor run android --settings settings.json
# Connected Android device
meteor run android-device --mobile-server=https://reachable-server.example \
--settings settings.json
# iOS simulator (macOS)
meteor run ios --settings settings.json
# Connected iOS device (macOS)
meteor run ios-device --mobile-server=https://reachable-server.example \
--settings settings.jsonA physical device cannot reach a server advertised as localhost; use a
reachable HTTPS server or the internal tunnel launcher.
# Android AAB
meteor build ./android-build \
--platforms android \
--server=https://your-server.example
# iOS Xcode workspace
meteor build ./ios-build \
--platforms ios \
--server=https://your-server.exampleThese commands do not provide signing credentials. Android signing and iOS archive/export steps are implemented in the GitHub Actions workflows and rely on repository secrets.
The default application ID in mobile-config.js is org.mieweb.opensource.
Production CI patches Android to com.mieweb.mieauth; development CI patches
both mobile builds to org.mieweb.os.dev and uses beta branding.
For Apple Watch or Wear OS:
- Pair the watch and enable notification mirroring/bridging.
- Install a fresh mobile build. iOS approval categories are registered at app startup, so source changes require rebuild and reinstall.
- Lock or background the phone and send an approval request.
- Verify Approve and Reject on the wrist, server resolution, timeout behavior, and dismissal synchronization across devices.
Use physical hardware for final Apple Watch validation; simulator behavior is not sufficient for mirrored notification actions.
npm test
npm run lint
npm run format:checkThe Meteor Mocha suite currently covers:
- client/server environment behavior and screen-lock state
- expired-registration cleanup and complete user removal
- approval-link error templates and token error classification
- API-key hashing, verification, and management methods
- Duo signature canonicalization, Auth/Admin response envelopes, pagination, and user/device mapping
- MongoDB healthcheck behavior, including older
isMasterfallback
The repository does not currently configure coverage reporting or a separate
pull-request CI workflow. Release workflows build and deploy, but they do not
run npm test, lint, or formatting checks before release.
curl http://localhost:3000/healthcheckThe endpoint returns 200 only when MongoDB responds to ping and reports the
connected node as writable. It returns 503 for a disconnected or read-only
database. HEAD is supported for probes.
Pending approvals are shared through MongoDB. Indexes are created at Meteor startup, but the repository also provides a migration/verification utility:
MONGO_URL="mongodb://host/database" node migrate-multi-instance.js --dry-run
MONGO_URL="mongodb://host/database" node migrate-multi-instance.jsThe non-dry run deletes all existing pendingResponses records before
creating and verifying indexes. Do not run it while live approvals are active.
Every application instance must use the same MongoDB database and the same
INTERNAL_SERVER_SECRET.
The detailed design is in docs/MULTI_INSTANCE_SOLUTION.md.
The scripts/ directory contains the deployment-specific service unit,
installation helper, and startup wrapper. They assume a particular Linux user,
home-directory layout, Node installation, and set-env.sh location. Review and
adapt them before using them on another host.
Releases trigger three parallel delivery paths: server deployment, Android build/publish, and iOS build/upload.
| Environment | Tag | Source branch | Behavior |
|---|---|---|---|
| Development | dev-v* |
development |
Deploys the dev server, publishes Android to the internal track, and uploads iOS to TestFlight. Release text can select [server], [android], and/or [ios]; no labels runs all three. |
| Production | v* excluding dev-v* |
main |
Deploys the production server, publishes Android to the internal Play track, and uploads iOS to TestFlight. |
The workflows update mobile-config.js from the release tag and commit the
version/build metadata back to the corresponding branch. Server deployment
builds a Meteor server bundle over SSH, switches a Builds/current symlink,
restarts mieauth.service, and retains the newest three builds.
The workflows depend on organization-specific SSH, Firebase, Android signing,
Google Play, Apple signing, and App Store Connect secrets. Their names and
expected usage are visible in .github/workflows/deploy-development.yml and
.github/workflows/deploy-production.yml; secret values are intentionally not
documented here.
The two test-ios-*.yml workflows are manual iOS/Fastlane experiments, not the
primary release path.
| Path | Responsibility |
|---|---|
client/main.jsx |
Shared client entry point and Cordova initialization. |
client/mobile/ |
Biometrics, device capture, deep links, push handling, and mobile React UI. |
client/web/ |
Public website pages and shared web layout. |
server/main.js |
Server entry point, REST handlers, registration, email, notification, and Meteor methods. |
server/adminAuth.js |
LDAP authentication, RSA credential transport, and admin sessions. |
server/adminApi.js |
Admin dashboard REST endpoints. |
server/firebase.js |
Firebase initialization and FCM delivery. |
server/duo/ |
Duo Auth API v2 and Admin API v1 compatibility layers. |
server/templates/ |
Admin and approval-result HTML templates. |
utils/api/ |
MongoDB collections and related Meteor methods/publications. |
tests/ |
Meteor Mocha server and client tests. |
docs/ |
Focused API, admin, and multi-instance documentation. |
scripts/ |
systemd unit and deployment startup helpers. |
.github/workflows/ |
Development, production, and manual iOS automation. |
mobile-config.js |
Cordova metadata, native preferences, plugins, Firebase files, icons, and launch screens. |
rspack.config.js |
Meteor Rspack/SWC optimization. |
generate_app_resources.py |
Mobile icon and launch-screen generator. |
_build/, android-build/, ios-build/, public/build-chunks/, and
public/buildInfo.json are generated outputs or local artifacts, not primary
source directories.
- Confirm
FIREBASE_SERVICE_ACCOUNT_JSONis set and parses as JSON; startup logs explicitly report whether Firebase initialized. - Confirm the user has at least one approved device with a current FCM token.
- Reopen the mobile app to refresh token registration.
- Confirm the mobile build contains the Firebase file for its platform.
- A normal no-response result is
action: "timeout"after approximately 25 seconds; transport errors return an error response instead.
- Do not advertise
localhostas--mobile-serverto a physical device. - Use a reachable HTTPS URL and make
ROOT_URLmatch it. - Confirm the device trusts the TLS certificate and can reach the host.
- Verify all required LDAP variables and the constructed user DN.
- For failover, provide comma-separated LDAP URLs.
- If group searches reject anonymous access, configure
LDAP_BIND_DNandLDAP_BIND_PASSWORD. - Leave certificate verification enabled and install the correct CA rather
than setting
LDAP_REJECT_UNAUTHORIZED=falsein production. - Admin sessions are in memory; a restart invalidates every session.
- Ensure all instances use the same
MONGO_URL. - Ensure all instances use the same
INTERNAL_SERVER_SECRET. - Check
/healthcheckon each instance and inspect/api/pending-responsesduring a request. - Verify the pending-response TTL and lookup indexes exist.
Always set MONGO_URL explicitly. A local Meteor development database often
runs on port 3001, while a separately installed MongoDB commonly runs on 27017.
The scripts have different fallbacks, so relying on defaults can modify the
wrong database.
Create both private/android/google-services.json and
private/ios/GoogleService-Info.plist. mobile-config.js references both even
when building only one platform.
- The YubiKey/Magic Box development launcher and server deployment scripts are tied to internal MIEWeb infrastructure; no public replacement service or generic deployment manifest is included.
- Firebase projects, LDAP schema values, mail service, app-store accounts, and signing material cannot be inferred from source and must be supplied by the operator.
- Admin sessions are process-local. Behind a load balancer, use session affinity or redesign the session store; MongoDB-backed approval responses do not solve admin-session sharing.
/api/pending-responsesis currently unauthenticated and may expose request metadata. Restrict it at the reverse proxy or add application authentication before exposing it outside a trusted network./send-notificationallows unauthenticated requests unlessSEND_NOTIFICATION_FORCE_AUTH=true. Production deployments should enable enforcement.- The migration utility advertises
--force, but the current implementation does not use that flag; it also removes pending responses without prompting. - The focused documents under
docs/include some historical implementation notes. When they conflict with source,server/,client/, and the current workflows are authoritative.
MIEAuth is available under the MIT License.