A graphical authentication system based on cognimetrics, built on the principle of using arbitrary objects instead of text as the "pass-phrase". Instead of typing a password, users pick an ordered sequence of 6–8 objects (an apple, a rocket, …) and later authenticate by spotting each object inside randomly composited challenge images. Reference implementation of the COMP4500 research proposal (COMP4500___Research_Proposal.pdf).
- 48 objects, each an easily recognisable everyday graphic
- NeoPass: an ordered sequence of 6–8 distinct objects (enforced server-side)
- Images: exactly 3 objects each, composited with rotation, scale and skew
- Image sets: 16 images partitioning all 48 objects exactly once
- Challenges: server-issued, single-use, 5-minute expiry, 8 image sets each, bound to one account — observed selections cannot be replayed
- Secret storage: peppered HMAC-SHA256 (
HASH_PEPPER, kept outside the DB) wrapped in Argon2id — a leaked database is uncrackable without the server environment
The frontend records anonymous timing events (registration duration, per-challenge answer times, sign-in outcomes) under a random session id — no account identifiers. Export them with:
curl -H "Authorization: Bearer $METRICS_TOKEN" https://<host>/api/metrics/exportSet METRICS_TOKEN in .env to enable the export endpoint.
- API — FastAPI (Python 3.12), managed with uv
- ORM — SQLAlchemy 2
- Database — MariaDB 11
- Frontend — React 18 + TypeScript, served by nginx
- Ingress — Cloudflare Tunnel (no open inbound ports)
Everything runs as containers via Docker Compose:
cloudflared ──> nginx (web) ──> static SPA
└── /api/ ──> FastAPI (api) ──> MariaDB (db)
cp .env.example .env
# fill in MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD, JWT_SECRET_KEY and HASH_PEPPER
# (generate secrets with: openssl rand -hex 32)
docker compose up --build -dThen open http://localhost:8080. The first challenge request generates the image sets, so the first login screen can take a few seconds.
The stack is designed to run on any small VM (e.g. an OCI free-tier Ampere instance, arm64 works out of the box) with no inbound ports open — public traffic arrives through an outbound-only Cloudflare Tunnel.
-
Install Docker on the VM and clone this repository.
-
In the Cloudflare dashboard: Zero Trust → Networks → Tunnels → Create a tunnel (Cloudflared connector). Copy the tunnel token.
-
Add a Public Hostname to the tunnel: your subdomain (e.g.
neoauth.example.com) → servicehttp://web:80. -
On the VM:
cp .env.example .env, fill in the secrets, setTUNNEL_TOKENandCORS_ORIGINS=https://neoauth.example.com. -
Start everything:
docker compose --profile tunnel up --build -d
Notes:
- Keep the VM's firewall / OCI security list closed except SSH — the tunnel needs nothing inbound.
- The
webport is bound to127.0.0.1only, for debugging on the host. - nginx rate-limits
/api/auth/per client IP (usingCF-Connecting-IP) as brute-force protection.
api/objects/ holds the object icons (SVG). The bundled set is Twemoji (CC-BY 4.0) — see api/objects/LICENSE.md. To use your own set, drop in at least 32 distinct SVG files (the filename becomes the object name shown to users); they are uploaded to the DB automatically on API start-up.
API (requires a running MariaDB, e.g. docker compose up db -d plus the MYSQL_*/JWT_SECRET_KEY env vars):
cd api
uv sync
DEV_RELOAD=1 uv run python main.pyFrontend dev server on port 3000:
cd app
npm install
npm startComposed of 4–8 arbitrary objects of the user's choice.
An object is the image of an arbitrary, recognisable singular entity — a car, an apple, etc.
An image is a collection of multiple objects randomly composited together.
An image set is a collection of 16 images that represents a single challenge; together its images contain all objects exactly once.
A challenge is a sequence of 4 or more image sets, from which the user must pick the images containing their objects.