dual-authentication-model

The admin service answers two different trust questions on every request, and it keeps them cleanly apart. One question is which service is calling — the transport-level boundary between internal services. The other is which human is behind this call — the identity that decides what is allowed. Each has its own layer, and neither is asked to do the other's job.

Layer one: service-to-service JWTs

Internal traffic is authenticated with a hand-rolled JSON Web Token in the classic three-segment shape — header, payload, signature — signed with HMAC-SHA256 over a shared service secret. There is no external library doing the signing; the codebase mints and verifies the token itself against Node's built-in crypto.

A token is minted per outbound call. The signing side stamps an issued-at time and, optionally, an expiry, and carries a claim naming the caller service. The verifying side is a request hook that runs before the route handler: it requires a bearer token, recomputes the signature and compares it, checks that the header declares the expected algorithm and type, and rejects a token whose expiry has passed or whose issued-at sits in the future — each bound allowing a small tolerance for clock drift between machines. On success it attaches a small context (the caller service name plus the raw claims) to the request; on any failure it raises a 401. Health-style public routes are exempt from the whole check.

The effect is that every non-public inbound request must prove it came from a trusted internal service before a handler ever sees it.

Layer two: admin session guards

The service JWT says nothing about who the human is — that is the second layer. Human identity rides on a session cookie forwarded from the browser (with a separate development fallback). The admin service does not itself validate the session. Instead a helper pulls the session cookie off the request and asks the auth service's own identity endpoint to resolve it, and the resolved user — id, email, a user-type, and name — is what the admin trusts.

That resolution call is itself wrapped in a layer-one service JWT, so the two layers compose: the request to the identity endpoint carries both the machine credential and the human's session cookie.

On top of resolution sit two guards. One requires any resolved user and fails with 401 when the session is missing or unresolvable. The other requires an admin user specifically and fails with 403 when the resolved user-type is anything else. Admin-ness is derived purely from the user-type the auth service returns, never computed locally.

Why it is shaped this way

The design keeps a single source of truth for identity: the auth service. The admin service stays stateless about sessions — it holds no session store and makes no authorization decision from a password. It only asks "who is this cookie?" and "is that person an admin?". The machine-trust question and the human-identity question stay independent, which is what lets a browser request and a service-to-service request travel the same code path while being judged on different evidence.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →