Esmeralda C. Cabrera Ventura
Serverless · Security

Proof of identity, not proof of humanity: a serverless gate for a public calendar

How I engineered an identity-gated scheduling system on AWS — a policy engine whose admission rules are configuration, whose verdicts are proven across the whole input space, and whose approvals travel as signed, expiring credentials.

A public portfolio has to satisfy two goals that pull directly against each other. It has to be maximally reachable, so a recruiter can book time in seconds. And it has to be defended, because a naked scheduling link on a public website is an open invitation to automated form-fillers, cold-outreach spam and calendar-stuffing bots.

The usual compromises are all bad. Hiding the link behind a contact form adds friction and loses exactly the people you wanted. Publishing the raw calendar surrenders control entirely. A CAPTCHA proves the visitor is human but says nothing about whether they are relevant — a bot and a genuine hiring manager are indistinguishable to a checkbox.

So I framed the requirement differently. What the calendar needs is not proof of humanity. It is proof of professional identity: a verifiable claim, issued by a trusted third party, that the person asking for time is who they say they are. That reframing is what made the rest of the system designable.

End-to-end architecture diagram of the verification gateway
From an anonymous click, through federated identity and a five-test policy engine, to either a signed booking token or a specific rejection reason.

The shape of the system

There is no application server in this architecture. No container, no virtual machine, no database. A static front end initiates a federated identity flow; a managed HTTP endpoint receives the callback; a single stateless function adjudicates it; and the outcome is expressed entirely as an HTTP redirect carrying either a signed approval token or a machine-readable rejection reason.

Zero dependencies was a deliberate choice rather than minimalism for its own sake. A current Node runtime provides native fetch and ES modules, which is enough to write the entire OAuth orchestration by hand. Nothing to keep patched, and no supply-chain surface at all in the component that makes the security decision.

Choosing HTTP API over REST API followed the same reasoning. Request validators, usage plans and API keys are genuinely useful features — for a single OAuth callback route they are unused surface, at higher latency and higher cost.

Designing for identity providers that decline to answer

The most interesting engineering problem in this system is one that isn't obvious until you integrate against a real identity provider.

Federated providers tier their data. Standard-tier OIDC access grants exactly three scopes and no more. Richer signals — a platform verification badge, a profile headline — live behind privileged endpoints that return HTTP 403 to applications without elevated access. That 403 does not mean the visitor lacks a badge. It means the provider declined to tell you.

A conventional pass/fail check cannot express that difference. It has two outcomes and needs three, so it collapses "we don't know" into "no" — and a gate built on that collapse rejects legitimate visitors for its own lack of permission. Absence of evidence gets scored as evidence of absence.

Any policy engine reading a third-party API needs a state for "the source declined to answer," or it will punish users for its own access tier.

So every test in the engine returns one of three values:

Each test also reports a dataAvailable flag alongside its verdict. That flag is what makes the policy tunable on evidence rather than guesswork: production logs show exactly which sources are answering, so a discretionary rule can be promoted to mandatory when — and only when — its data is reliably present. The operations notes carry that as an explicit rule for future maintainers: never make a test mandatory while its dataAvailable flag reads false.

Admission rules as configuration

The five tests cover account identity, profile completeness, platform verification badge, headline keywords and corporate email domain. What governs admission, though, is not the tests themselves but two declarations sitting above them: a veto list of tests that must actively pass, and a floor on the total number of passes.

That structure means the gate's strictness is a configuration value, not control flow. Making corporate email mandatory is a one-line edit. Requiring one criterion beyond the mandatory pair is a one-line edit. Loosening to any two of five is a one-line edit. No logic is rewritten, so no logic can be broken in the rewriting.

The domain test is worth a note on its own. It screens against a curated deny-list of more than fifty consumer, international and disposable email providers, and rejects malformed domains. Today it is scored rather than mandatory — a recruiter on a consumer address is still admitted, but the failure is recorded and carried in their token. Enforced-and-logged rather than enforced-and-blocking, so the policy can be tightened later against real traffic instead of assumptions.

Proving the policy, not trusting it

Five tests across three states is a finite space, so I did not sample it. I enumerated it.

All 162 valid combinations were generated and each evaluated against the specification: access is granted if and only if the identity and profile tests both pass. 162 of 162 correct.

When a system makes an access decision, "it worked when I tried it" is not evidence. Where the state space is small enough to enumerate, exhaustive proof is available for the cost of a loop, and spot-checking becomes a choice not to bother.

The approval page confirming a verified visitor
An approved verdict, with a signed credential minted and a fifteen-minute window to use it.

An approval that survives a redirect

An approved visitor gets forwarded into a static page, so the hand-off cannot rely on server session state. The function mints a compact signed credential instead: HMAC-SHA256 over a base64url payload carrying the account subject, email, display name, which tests passed, which were skipped as unavailable, issue and expiry timestamps, and a UUID replay identifier.

Fifteen-minute expiry — long enough to book a meeting, short enough that a leaked link is worthless almost immediately. Signatures compared with a timing-safe primitive after an explicit length check, closing a timing side-channel. The structure is deliberately JWT-shaped without inheriting JWT's algorithm-confusion pitfalls, because a format this narrow does not need the generality that causes those bugs.

Rejection as a designed outcome

Most gates treat a refusal as an error state and emit something generic. That is a design failure twice over: it tells a legitimate visitor nothing actionable, and it tells the operator nothing diagnostic.

Here rejection is a first-class path with its own destination page and twelve granular reason codes. Crucially, those codes separate policy outcomes from mechanical ones — a reused single-use authorization code produces a retry message rather than a verdict about the visitor, because the two situations are not the same and should not read the same.

Every visitor who does not get through learns precisely which criterion they did not meet.

Instrumented so the live system can be reasoned about

Every request emits a structured JSON decision record: the policy in force, each test's state, whether its data source actually answered, and the final verdict. Any decision the gate has ever made can be reconstructed from logs alone, without reproducing it.

CloudWatch log events for the verification function
Execution telemetry — the authoritative account of what the function did, and the basis for sizing it.

Those same logs are how the function was sized rather than guessed at. The report showed peak memory of 103 MB, so the 512 MB configuration carries real headroom — and on Lambda that setting matters more than it looks, because CPU is allocated proportionally to configured memory. Under-provisioning memory throttles the processor, which lengthens cold starts, which pushes the function toward its timeout. The two limits are coupled, and sizing either one without measuring the other is how serverless functions acquire intermittent failures that are miserable to diagnose.

The timeout is set at 30 seconds against a multi-hop external identity handshake, for the same reason: a cold start plus a conversation with a third-party provider is not a sub-second operation, and configuring it as though it were converts a slow request into an opaque failure.

Security as a design input

Because this system exists to make a security decision, and was always intended to be published, the controls were designed in rather than reviewed on afterwards.

Confirmation that the meeting has been scheduled
Definition of done — not "the function returned 200," but a real invitation in a real inbox.

Scope, and what comes next

Being precise about where a security boundary sits is part of building one. This gate raises the cost of abuse substantially: it converts an anonymous click into an authenticated, adjudicated, logged decision, and it does so with zero idle cost and a single public route. What it does not do is make the underlying booking URL unreachable to someone who already has it, because the success page is a static document and a static document cannot hold a signing key.

That scope was chosen deliberately, and the next increment is already specified: mint single-use booking links through the scheduling provider's API from inside the function, so approval is enforced rather than presented. Alongside it, moving credentials into a managed secret store with automatic rotation, promoting the badge and headline tests to mandatory once elevated access makes their data reliable — a change the three-state design already anticipates — and expressing the whole stack as infrastructure as code.

What the system demonstrates

The finished gate integrates six AWS services into a coherent whole, and the engineering value sits in the couplings between them rather than in any one service: that a region choice becomes part of a hostname a third party must match exactly, that a stage name silently reshapes every registered redirect URI, that configured memory governs CPU allocation.

Underneath those is the decision I would defend hardest — modelling verification with three states rather than two. It costs almost nothing to implement and it determines whether the system is honest about what it actually knows.

Decide deliberately which layer owns which guarantee, and never let a system infer a fact from data it was never given.

AWS Lambda · API Gateway · S3 · CloudFront · IAM · CloudWatch · LinkedIn OIDC · Node.js

Read the full engineering dossier →

The full technical report, with 57 captioned build screenshots, is available on request. More of my work is at esmeraldaspace.com, and I am reachable on LinkedIn.