Face Liveness Challenge
On this page
- At a glance
- What it does
- A. Guides a person through three poses
- B. Captures one sharp frame per pose
- C. Refuses captures that would be useless
- D. Keeps the evidence private and hands it to a reviewer
- E. Drops into any page as a widget
- Interface
- Standards
- Inside
- Design decisions
- Specifications
- Related
- Terms
Face Liveness Challenge is a Python service that asks a person in front of a camera to prove they are there, live, at that moment: face the camera, turn right, turn left. It follows each movement frame by frame, captures one sharp picture at every pose, and stores the three pictures as evidence for a human reviewer. It does not recognise faces and does not compare them with anything — its job is to show that a living person performed the challenge, not who that person is.
I built it for applications that need a person to be present before an important action, such as signing a document, and that keep a human in the decision. A single uploaded photo proves nothing: it may be a picture of a picture. A short guided sequence of movements, performed live and recorded as three frames, gives the reviewer something a still photo cannot.
At a glance
| Language & framework | Python, with FastAPI, MediaPipe for face landmarks, and OpenCV |
|---|---|
| Architecture | Transaction Script Service |
| Version | 2.15.1 |
| Status | Stable, in production |
| Form | An HTTP and WebSocket service with an embeddable camera widget, shipped as a single bundled executable |
| Source code | Private |
What it does
A. Guides a person through three poses
The challenge is three steps in a fixed order: face the camera, turn the head to the right, turn it to the left. For every frame the service tells the widget which pose it sees, which one it expects, and how far along the current step is, so the person always knows what to do next.
B. Captures one sharp frame per pose
A pose has to be held for a moment and then kept steady before its frame is kept, so the evidence is never a blurred picture caught mid-movement. Moving too fast postpones the capture instead of starting the step again.
C. Refuses captures that would be useless
On the frontal step a mask detector checks the face before the frame is kept, because a reviewer cannot judge a face hidden behind a mask.
D. Keeps the evidence private and hands it to a reviewer
The three frames go to a private storage bucket through short-lived upload links. When a reviewer opens the case, the application asks the service for short-lived read links to the latest three frames of that person.
E. Drops into any page as a widget
The camera, the guidance, and the connection live in one self-contained page that an application embeds in a frame. It tells the host page when the challenge succeeded, expired, or failed, and the host only has to open and close it.
Interface
| Capability | Purpose |
|---|---|
| Start a challenge | for the signed-in user; returns a session code, the poses, and the expiry |
| Camera stream | frames in, guidance out, success at the end |
| Status | whether the challenge was completed |
| Camera widget | for embedding in the application's pages |
| Readiness | session store and storage |
For every frame it receives, the service answers with a status the widget turns into guidance:
Standards
| Standard | What it guarantees here |
|---|---|
| WebSocket (RFC 6455) | one open connection for frames and guidance, with low enough latency to follow a moving head |
| JSON Web Tokens with RS256, keys from a JWKS endpoint | the challenge belongs to the signed-in user, checked for issuer, audience, and client |
| Presigned URLs, AWS Signature Version 4 | evidence written and read through short-lived links, without storage credentials in the service |
| W3C Trace Context and OpenTelemetry | a challenge appears in the trace of the application that started it |
Inside
The service is a Transaction Script Service. The WebSocket endpoint is one procedure that reads every step of a challenge in order — check the token, load the session, follow the frames, upload the captures, record the result — and the work for each step sits in a module of its own: the face engine and its challenge tracker, the mask detector, authentication, the session store, the storage client, and telemetry. It is bundled into one executable together with its models.
Design decisions
Liveness, not identity, and a human makes the call. The service answers one question — did a live person perform this challenge just now — and leaves the decision about who that person is to a reviewer who sees the three frames. That keeps the service honest about what it can prove, and keeps the judgement where accountability already lives. It produces evidence, not a verdict, and logs carry only references to the stored frames, never the images themselves.
Three poses replaced a nine-dot pattern. The first challenge asked the person to move the tip of the nose across three of nine dots on the screen, in a random path like a phone unlock pattern, and the grid even had to be shrunk so the face stayed inside the guide. People found it unintuitive, so I replaced it with three head poses anyone understands at once. I deliberately kept the tracker's structure — hold, settle, advance — and the output contract, and changed only the test of whether the current pose is right, so everything already proven about sharp captures carried over.
Every check had to earn its place. Over time the challenge also required open eyes, no sunglasses, and a smile on the frontal step. Measuring with the real landmark model showed the smile check was the problem: a broad smile scored 0.787 against a threshold of 0.35, but a natural closed-lip smile stayed far below, so people had to open their mouths to pass. Two of my own explanations failed when I tested them, which stopped me from loosening the wrong thing. I removed the smile, eye, and sunglasses checks and kept only the mask check, the one that protects the reviewer's ability to see the face, and I renamed the first pose from "smile" to "frontal", because a name that lies is more dangerous than a missing feature.
Direction comes from a real camera, and the capture comes after the proof of stillness. Head turn is estimated from where the tip of the nose sits between the two edges of the face, from one landmark inference per frame. The first version had left and right reversed: the reasoning from mirrored frames was wrong, and only a test with a real camera settles which sign is the person's right, which the code now says. I lowered the turn threshold in steps as people found the poses hard to reach. A frame is kept only after the pose has been held and then stayed still, and the shutter sound plays only for that frame; earlier it played when a frame was taken provisionally, so people heard three shutters while only one step was saved.
Stillness is measured as speed, not as distance from where you started. One release made the first pose impossible to finish: it compared every frame with the position at the start of the capture, so ordinary head tremor accumulated until it exceeded the limit. A simulation showed that with tremor of twenty pixels, no capture ever happened. The tracker now measures how fast the nose or the turn is moving, smooths it, and simply waits when it is too fast, instead of starting the step over.
The widget guides the eyes and the ears. The camera page is self-contained, because splitting the logic between the host page and the frame made them fall out of step. An oval guide asks the person to come closer or move back, arrows show the turn, and sounds — a tick while the pose is wrong, a shutter when a frame is kept, a chime at the end — help someone who is looking away from the screen. The guidance is drawn as soon as the challenge starts, without waiting for the connection, and the captured frames are never shown back to the person: they are evidence for the reviewer, not a preview.
Evidence must never disappear quietly. The three frames are uploaded through one request for three upload links to the private bucket, each with an explicit content type, because without it the storage recorded them as form data. If one upload fails, the ones already written are deleted so no partial set remains. When the reviewer's read links are requested, a frame that cannot be signed is reported as missing and a total failure as an error; the earlier version simply returned a shorter list, which for evidence is worse than a visible failure.
The challenge belongs to the person who started it. Every request is authenticated with a token signed by the application's identity service, whose keys are fetched and cached, and checked for issuer, audience, and client. The token on the camera connection is checked before the connection is accepted, and the person on that connection must be the person who started the challenge.
Telemetry that can be trusted to be complete. Telemetry moved from writing straight to Elasticsearch to OpenTelemetry, instrumented in code because the bundled executable has no launcher. For months the call to storage never appeared in traces: inside the bundle, the HTTP client's package metadata was missing, so its instrumentation switched itself off without a word, until the build was made to carry it. Duration histograms vanished in the same quiet way until metrics moved to delta temporality. The service now records how many challenges start, succeed, expire, or fail, how long they take, and why poses fail, using a small fixed set of reasons.
Specifications
| Inputs | A signed-in user's token; a live camera stream of JPEG frames from the widget |
|---|---|
| Outputs | Guidance for every frame; three PNG captures in private storage; a completion flag; read links for the reviewer |
| Challenge | Frontal, right, left; each pose held, then kept steady before capture; turn threshold configurable |
| Checks | One face landmark inference per frame; mask detection on the frontal capture |
| Session | A single-user code with a short lifetime, kept in Redis |
| Observability | OpenTelemetry traces, logs, and metrics over OTLP, including challenge outcomes and pose failure reasons |
| Runs as | A bundled executable with its models, in a multi-stage container image, beside a Redis instance |
Related
The storage service that issues the links the three frames are written to and read from.
Where the service's traces, logs, and metrics go once they leave it.
Terms
- Liveness
- Evidence that a real, present person is in front of the camera, as opposed to a photo or a recording; it says nothing about who the person is.
- Face recognition
- Identifying or matching a person by comparing their face with a stored one; this service does not do it.
- Face landmarks
- Points a model places on a face — the tip of the nose, the edges of the face, the eyes — from which pose and movement can be estimated.
- Yaw
- How far the head is turned to the left or right.
- WebSocket
- A connection that stays open so a browser and a server can send messages to each other continuously.
Technologies covered
- Python
- FastAPI
- OpenTelemetry
- WebSocket
- MediaPipe