Whetstone.
Multi-Tenant AuthThe auth provider, end to end
Module 5, Lesson 316 min

The auth provider, end to end

You have the split, the ladder, the return values and the exception. What is missing is the thing that makes any of it real: a genuine identity provider on the front of it, and a walk of the whole path from a request arriving to a scoped result coming back.

One scoping note first. The specific provider, whether that is a hosted authentication service, your company’s single sign-on, or something you run yourself, is illustrative. Provider-specific configuration is a lookup and it changes on their schedule, not yours. What is examinable, and what survives changing providers, is the shape of the handler and where each decision belongs.

What sits behind authenticate

In development your authenticate handler probably compares a static key and moves on. In production it validates a bearer token issued by a real provider, and the work splits in two.

Validation confirms the token genuinely came from the issuer you trust, is not expired, and has not been tampered with. That is cryptographic work you delegate to the provider or to a library. Hand-rolling it is how you end up accepting tokens you should not.

Identity construction turns the validated token into the object every downstream decision reasons about: the caller’s identifier, plus the attributes your policy needs, such as organization membership, tenant, or asserted permissions.

Where each rule belongs

The temptation, once the token contains an organization id, is to enforce with it immediately. Resist that, and the reason is structural rather than stylistic: authenticate does not know which resource is being touched. It runs once, before the request has been resolved to a thread or a store namespace or a run. It cannot make a per-resource decision because it does not have a resource.

So tenant scoping belongs in an authorization handler, expressed as a metadata filter derived from the identity. That is precisely what the dict return value exists for: say it once, centrally, and every listing and read is confined without any endpoint doing filtering by hand.

Except for the store, where the same intent is expressed by rewriting the namespace, because the store has no metadata to filter. That exception is not an inconsistency, it is the same idea applied through the structure the store actually has.

The failure direction

Providers become unreachable. When validation cannot complete, the handler fails the authentication. An identity that cannot be verified is not an identity, and there is no safe middle ground available: returning an empty filter is a scoping instruction, and scoping instructions presuppose an authenticated caller you do not have.

This is worth stating plainly because the wrong answer is genuinely attractive under production pressure. Availability arguments are how systems end up failing open, and an auth system that fails open under load is failing open exactly when it is most likely to be under attack.

The whole line, one pass

A request arrives with a credential. authenticate validates it and returns an identity. The most specific registered @auth.on handler for that resource and action runs, and returns None or True to allow, False to deny with a 403, or a dict as a metadata filter. If the resource is the store, scoping happens through a namespace rewrite instead. Only then does the actual work happen, already confined to the caller’s slice.

Every station on that line is a fact from this module. If you can say the line out loud without stopping, this focus area is done.

Practice

Try it yourself

Recall

What the authenticate handler does with a token

The provider is illustrative and interchangeable. The shape of the handler is not.

A bearer token arrives from a real identity provider. Describe the shape of what your authenticate handler has to do with it.

Reveal answer

Validate it, then turn it into an identity. Validation means confirming the token genuinely came from the provider you trust and has not expired or been tampered with, which is work you delegate to the provider or to a library rather than hand-rolling. Turning it into an identity means returning the caller's identifier along with whatever attributes the authorization handlers will need, such as tenant or organization membership and any permissions the provider asserts. What you return is the object every downstream authorization decision reasons about, so a claim you fail to carry through is a claim your policy cannot use.

Quiz

Where tenant scoping belongs

Your provider issues tokens carrying an organization id. Users must only ever see their own organization's threads. Where does that rule get expressed?

  1. AIn the authenticate handler, by rejecting tokens whose organization does not match the request
  2. BIn each endpoint, by filtering the fetched results before returning them
  3. CIn an authorization handler, by returning a metadata filter derived from the identity
  4. DIn langgraph.json, by declaring an organization scope on the deployment
Show answer

Correct answer: C — In an authorization handler, by returning a metadata filter derived from the identity

A metadata filter derived from the established identity is exactly what the dict return value exists for: it scopes the resources a request operates over, expressed once, centrally. The tempting wrong answer is the first, because the organization id is right there in the token and rejecting early feels defensive and tidy: authenticate has no idea which resource is being touched, so it cannot make a per-resource decision, and pushing access logic into it produces a policy that is simultaneously too blunt and in the wrong place. Filtering per endpoint is the thing the platform is offering to stop you doing.

Quiz

Which failure mode is correct

Your identity provider becomes unreachable and token validation cannot complete. What must the handler do?

  1. AFall back to allowing the request, since availability matters more than a transient check
  2. BFail the authentication, because an identity that cannot be verified is not an identity
  3. CReturn an empty metadata filter, which is a safe middle ground while the provider recovers
  4. DSkip authentication and let the authorization handlers make the call instead
Show answer

Correct answer: B — Fail the authentication, because an identity that cannot be verified is not an identity

An identity that cannot be verified is not an identity, so the honest outcome is a failed authentication rather than a guess. The tempting wrong answer is the empty metadata filter, because it sounds like a conservative compromise and the vocabulary is drawn from this very module: an empty filter is a scoping instruction, which presupposes an authenticated caller, so returning one from a failed authentication is asserting an identity you do not have. Falling back to allow is the classic wrong-side failure, and skipping to authorization hands a decision to handlers whose entire input is the identity that was never established.

Recall

The request path, end to end

Walk the whole thing once. Everything in this module is a station on the same line.

Trace a request from arrival to a scoped result, naming what happens at each stage.

Reveal answer

The request arrives at the server carrying a credential. The authenticate handler validates it against the identity provider and returns an identity with its attributes. The authorization handlers are consulted, and the most specific registered handler for that resource and action is the one that runs. It returns one of four things: None or True to allow, False to deny with a 403, or a dict acting as a metadata filter that narrows which resources the request operates over. If the resource is the store, the scoping happens by rewriting the namespace instead. Only then does the run, the thread read, or the store access actually take place, already confined to the caller's slice.

Do

Write the policy on paper

No provider account, no server. This is a policy design exercise and paper is the correct medium for it.

  • Take a two-tenant product and write down, in prose, the complete access policy for threads, runs and the store.
  • For each rule, mark which handler level it belongs at, and justify why it is not at a broader or narrower level.
  • For each rule, write which of the four return values expresses it.
  • Now find the rule that cannot be expressed as a metadata filter and say what happens to it instead.
  • Finally, write down what your policy does when the identity provider is unreachable, and check that answer against the fail-safe direction.
Done whenYou have a written policy with a handler level and a return value against every rule, a justification for each level choice, the store rule correctly identified as a namespace rewrite rather than a filter, and an explicit unreachable-provider behaviour that fails closed.
Sign in to track your progress →