How do you keep an authentication hook off a health-check endpoint without leaving other endpoints unprotected?
answer
- make protection the default
- structure the exemption, do not spell it
- prefix matches more than you think
- hook and router must normalise alike
- test enumerating unauthenticated routes
basics
~20 sCarve the exemption out structurally: register the public endpoints in their own scope and attach authentication to the protected scope. If an exclusion list is unavoidable, match the routed identity exactly rather than a raw path prefix, and test the exempt set.
solid answer
~50 sThe safe shape is to make protection the default and the exemption explicit. Put the health endpoint in a small public branch of the route tree, attach the authentication hook to the other branch, and nothing new is public unless someone registers it in the public branch. The dangerous shape is a global hook carrying a list of path strings to skip, because that comparison usually happens **before routing**, against a raw path: a prefix test for `/health` also skips `/healthz-admin`, and encoding, case, duplicate separators or `..` segments can make a request that reaches a protected handler still look exempt. If you must keep an exclusion list, compare against the matched route's identity rather than the raw path, anchor the match exactly, and add a test that enumerates every route reachable without credentials and fails when that set changes.
go deeper
Know that some endpoints must answer without credentials and that the exemption should be narrow. Naming the probe path exactly, rather than a prefix around it, is the first instinct to have.
Explain the mechanics of the mismatch: a pre-routing hook compares a raw path, while the router normalises and decodes before choosing a route, so the two can disagree about the same request.
Show the production control, not just the rule: a public branch in the route tree, exemptions anchored on the matched route and method, and a test enumerating everything reachable without credentials.
Frame it as which default the organisation can afford. Decide who owns the exempt set, how a new service inherits the protected-by-default shape, and how the exemption is proved rather than asserted.
Almost every service needs one or two endpoints that answer without credentials — a liveness or readiness probe, a public metadata document, the endpoint that issues a token in the first place. The interview question is not whether you can make the probe return `200`; it is whether the mechanism you use to exempt it can exempt anything else by accident. ## Two ways to state the exemption **Structural (scope-based).** Split the route tree. One branch holds the handful of endpoints that are genuinely public; the other holds everything else and carries the authentication hook. The exemption is then a fact about where a route is registered, visible at the route declaration, and a new route is protected unless somebody deliberately puts it in the public branch. **Textual (exclusion list).** Keep one global hook and give it a list of paths to skip. The exemption is now a string comparison inside the hook, invisible from the route tree, and correct only to the extent that the comparison is correct. Both designs are deny-by-default in the sense that an unlisted new route is still protected, and that is why an exclusion list is far better than opting each route into authentication one at a time. The difference is the blast radius of a mistake: a structural mistake exposes the one route you misfiled, while a sloppy exclusion pattern can expose an entire subtree nobody was thinking about. ## How exclusion lists go wrong 1. **Prefix instead of exact match.** Skipping anything that *starts with* `/health` also skips `/health-admin`, `/healthz-internal` and `/healthcheck/debug/dump`. The rule looks like one endpoint and covers a family. 2. **Comparing the raw path before routing.** A hook attached before routing sees the request line as sent. The router may later normalise duplicate separators, resolve `..` segments, decode percent-encoding, strip a trailing slash or match case-insensitively. Any normalisation the router does and the hook does not is a gap: the two can disagree about which route this request is, and the hook's answer is the one that decides whether credentials are demanded. 3. **Query strings, matrix parameters and path parameters.** A comparison that forgets to strip everything after `?`, or that anchors on a path that contains a parameter segment, matches more or less than intended. 4. **Method blindness.** An exemption written for a read probe frequently exempts every method on that path, so a write to the same path arrives unauthenticated. 5. **Nobody audits the list.** It grows during incidents, entries outlive the endpoints they were added for, and the list is usually configuration rather than code, so no reviewer reads it. | Exclusion style | What it actually exempts | Failure mode | |---|---|---| | Prefix string, pre-routing | Every path sharing those leading characters | Silent over-exemption of a whole family | | Exact raw path, pre-routing | One spelling of one path | Normalisation and encoding mismatches with the router | | Matched route identity, post-routing | Exactly the route the router chose | Runs late, so unmatched paths never reach it | | Separate public branch | The routes deliberately registered there | Misfiling one route, visible in review | ## The safe recipe - Prefer **scope** to **string**: a public branch, a protected branch, authentication on the protected branch. - If a global hook must decide, decide **after routing**, on the route the router actually chose, so the hook and the router can never disagree about which endpoint this is. - **Anchor exactly** and include the method in the exemption, not just the path. - Keep the exempt set **small and enumerable**, and treat it as code subject to review, not as a configuration value. - Write the **test that enumerates it**: iterate the registered routes, call each without credentials, and assert that the set answering anything other than `401` is exactly the expected list. This is the control that actually catches regressions, because it fails when someone adds a route, not when someone remembers to check. - Remember the probe's own exposure: an unauthenticated endpoint should reveal nothing beyond liveness — no dependency details, no versions, no configuration. ## Why the opt-in alternative is worse It is tempting to conclude that since exclusion lists are risky, authentication should simply be attached per route. That inverts the default: every future endpoint is then public until someone remembers otherwise, and the failure is invisible because nothing in the code mentions the hook that is missing. Between an opt-out design with a carefully anchored exemption and an opt-in design with none, the opt-out design is the one whose accidents a test can enumerate.
- Why is deciding the exemption after routing safer than deciding it before?Because the hook then judges the same route identity the router chose, instead of a raw path string it normalises independently. Disagreement between the two is the whole class of bug: percent-encoding, `..` segments, duplicate separators and trailing slashes can make one see an exempt path where the other sees a protected route.
- What test actually catches an accidental exemption?One that enumerates the framework's registered routes, issues a credential-free request to each, and asserts that the set not answering `401` equals a short list checked into the test. It fails when a route is added or an exemption widens, rather than relying on a reviewer noticing.
- Should an exemption cover every method on the exempt path?No. Write the exemption for the method the public endpoint serves. A path-only exemption quietly makes writes to that path unauthenticated too, which matters as soon as the path gains a second handler for a different method.
saying these in an interview costs you the question
- Excludes by path prefix and assumes it can only match the probe
- Compares a raw request path that the router would still normalise
- Exempts a path for every method rather than the one it serves
- Moves authentication to per-route attachment so nothing needs excluding
- Treats the exclusion list as configuration that needs no test or review
- Lets the probe return dependency details because it is only a health check