skip to content

What must the cache key for a service's own machine token contain, and what breaks when one global slot is used?

level: middleimportance: must knowfreq 45%

answer

  1. which calls may share one token
  2. registration, scope set, callee
  3. sort and canonicalise before hashing
  4. the secret is not part of it
  5. entry count should stay tiny

basics

~20 s

The key must identify what the token is good for: the client registration it was issued to, the normalised scope set requested, and the callee it was requested for. One global slot makes two different calls overwrite each other and sends each a token the other's callee will refuse.

solid answer

~50 s

A machine token is not fungible. It was issued to one registration, for one set of permissions, and usually for one destination — so the cache key must carry all three: the **registration**, the **normalised `scope` set**, and the **callee identifier** the caller named with `audience` or `resource`. Add the authorization server itself when one process consumes more than one. Normalise before you hash, or `"meter.read meter.status"` and `"meter.status meter.read"` become two entries that each halve your hit rate. A single global slot fails in one of two ways: either two schedules with different needs overwrite each other, so acquisitions rise towards one per call and some calls carry a token the wrong callee refuses, or somebody makes it work by requesting the union of every scope, and every call now carries more authority than it needs. Note what is *not* in the key: the credential value. Tokens outlive the secret used to obtain them, so putting it there would cold-start the cache on every rotation.

code

pseudocode · 8 lines
pseudocode
function cacheKey(registration, scopes, callee, issuer):
    # normalise the parts that vary without changing what the token is good for
    scopeText = join(sort(unique(trimAll(scopes))), " ")
    calleeId  = lowercaseHost(stripTrailingSlash(callee))

    # the credential value is deliberately absent: a rotation must not
    # invalidate tokens that are still inside their granted window
    return hash(issuer + "|" + registration.clientId + "|" + scopeText + "|" + calleeId)

go deeper

for a junior

Remember that a machine token is not generic: it was issued to one client, for one set of permissions, and often for one destination, so the cache has to keep them apart.

for a middle

Be able to name the key's components and explain normalisation — why an unsorted scope list silently doubles acquisitions and halves the hit rate.

for a senior

Demonstrate the diagnosis: acquisitions climbing towards call volume, intermittent refusals at one callee, entry counts creeping upward, and what each says about the key.

for a principal

The interesting judgment is the union-scope shortcut: it makes the cache look perfect while raising what a single leaked token is worth. Decide it deliberately and write down why.

## The cache key is a statement about interchangeability A cache key answers one question: **which already-held token may this call use?** For a machine caller, a token is interchangeable with another only when three things match. 1. **The registration it was issued to.** A token names the client it was issued for. Two schedules running under two registrations cannot share one, even when everything else about the call looks the same. 2. **The permissions requested.** The `scope` set asked for decides what the token is allowed to do at the far end. A token obtained for reading meter values does not become a token that can acknowledge an alarm because the caller would like it to. 3. **The callee it was requested for.** When the caller named a destination — with `audience`, or with `resource` from RFC 8707 — the issued token is narrowed to it, and the other destination will refuse it. If your process talks to two of the utility's services, that is two entries, not one. Add a fourth component when it applies: **which authorization server** the token came from. A building-management system that reads two utilities' meter services has two issuers, two registrations and no reason to let their entries collide. ## Normalise before you hash The key is derived from request inputs, and request inputs vary in ways the authorization server does not care about: - **scope ordering and duplicates** — the same permission set written two ways; - **the callee's spelling** — a trailing slash, a differently-cased host; - **whitespace** from configuration. Each variation that survives into the key is a phantom cache miss: an extra acquisition, an extra token alive in memory, and a hit rate that looks unexplainably poor. Sort the scope set, drop duplicates, join with single spaces, canonicalise the callee identifier, then hash. ## What a single global slot actually does Teams reach for one slot because the first version of the service called one endpoint with one scope. The second endpoint is what breaks it, and it breaks in one of two directions: | behaviour | what happens | what it looks like at 3 a.m. | |---|---|---| | last write wins | each schedule overwrites the other's entry, so nearly every call misses | acquisitions climb to roughly one per call, and the token endpoint starts throttling | | whichever token is present is used | a call carries a token narrowed for the other callee | intermittent refusals from one service, correlating with nothing in that service's logs | | union of all scopes requested | every entry fits every call, so the cache works perfectly | every call now carries authority it does not need, and a leaked token is worth far more | The third is the dangerous one, because it presents as a fix. Widening the request until one token serves everything trades a cache-design problem for a blast-radius problem, and nothing in your monitoring will complain. ## What does not belong in the key - **The credential value.** Tokens already issued live out their granted window regardless of which secret obtained them, and keying on the secret would throw away every live token the moment a rotation lands. - **A person.** There is none here. If something about a user would change the token, you are holding a different kind of token than this one. - **Per-request detail** — a correlation identifier, the specific meter being read, the callee's instance. None of it changes what the token is good for; all of it destroys reuse. ## Sizing and eviction The number of live entries is bounded by the product of registrations, distinct scope sets and callees, which for most services is single digits. That means two practical things. First, a bounded map is enough, and an eviction policy is almost never the interesting part. Second, if the entry count is growing, the key is carrying something it should not — that count is the cheapest available alarm on a badly derived key. Export it, and alarm on growth rather than on an absolute number.

  • Two schedules request the same scope at the same callee but run under different registrations. One entry or two?
    Two. A token is issued to a specific client and the callee can tell which; sharing one across registrations means one of the two schedules is presenting an identity it was not given. Keeping them separate also keeps their issuance patterns, quotas and rotations independent, which is the whole reason for two registrations.
  • Does rotating the client secret invalidate the entries in the cache?
    No. A token already issued lives out its granted window; only future acquisitions are affected by which secret is presented. That is precisely why the secret is not in the key — and it is also why a broken rotation stays invisible until the first entry expires.
  • Is it acceptable to request the union of every scope so one token serves every call?
    It makes the cache trivially effective and quietly raises the value of every token you hold. A leaked or misdirected union token can do everything the service is permitted to do, rather than one endpoint's worth. Keep the sets separate and let the key carry the difference; the cost is a handful of extra entries.

saying these in an interview costs you the question

  • Uses one cache slot because the service currently calls one endpoint
  • Requests the union of every scope so one entry always hits
  • Hashes the raw scope string, so ordering creates duplicate entries
  • Puts the client secret in the key, flushing the cache on every rotation
  • Keys on the request or correlation identifier, so nothing is ever reused
  • Assumes a token issued for one callee is spendable at another from the same issuer