skip to content

Why should the signature algorithms your token verifier accepts be deployment configuration rather than a library default?

level: middleimportance: should knowfreq 41%

answer

  1. the sender must not choose the rules
  2. an allowlist an operator wrote down
  3. checked before any key is selected
  4. widen for the overlap, then narrow
  5. empty list means refuse to start

basics

~20 s

Because the verifier, not the sender, must decide how a token is checked, and that set has to change on the issuer's rotation schedule across a fleet deployed in waves. An inherited default is invisible in review and can drift between environments.

solid answer

~40 s

The accepted set is an allowlist an operator writes down, checked against the token's `alg` header before any key is selected. It belongs in deployment configuration for three reasons. First, it must change on a schedule the issuer sets, not on your release cadence: an algorithm migration means widening the list to accept both values for an overlap window, then narrowing it — two configuration changes with a dated task between them. Second, a value inherited from a library default is invisible in code review, differs between environments, and can move under you on a dependency upgrade. Third, configuration can be asserted: refuse to start on an empty list, log the accepted set at boot, and keep a test that a token signed outside the list is rejected. Empty must never mean "accept anything".

code

json · 7 lines
json
{
  "tokenVerification": {
    "acceptedAlgorithms": ["<algorithm-in-use>", "<algorithm-being-migrated-to>"],
    "overlapEndsOn": "2026-11-01",
    "failStartOnEmptyList": true
  }
}

go deeper

for a junior

Remember that the verifier keeps its own list of algorithms it will accept, and that the token's own header is a claim to check, not an instruction to follow.

for a middle

Explain where the list lives and why: deployment configuration, asserted at boot, checked before key selection, widened for a migration overlap and narrowed afterwards on a dated task.

for a senior

Talk about the operational half — proving what a running fleet accepts, keeping a non-production value from reaching production, and the negative test that catches a list that quietly grew.

for a principal

Frame it as ownership: which security-relevant values must be explicit deployment configuration with a review on change, and which may safely be inherited from a dependency's defaults.

## The verifier decides how a token is checked An incoming token names, in its `alg` header, the algorithm it claims to have been signed with. That value is written by whoever produced the token. A verifier that simply does what the header says has let the sender choose the rules of its own examination. The fix is structural rather than clever: the verifier holds its own list of algorithms it will accept, compares the token's declared value against that list, and rejects anything outside it — **before** a key is selected and before anything is fetched. The interesting question for a server author is not *whether* to hold such a list. It is **where that list lives and who owns it**. ## Why it is configuration rather than code Three properties push it out of the source and into the deployment. - **It changes on somebody else's schedule.** The issuer decides when it migrates to a different signing algorithm. Your fleet must accept the new value before the first token carrying it arrives, and must stop accepting the old one after the last one expires. Neither moment is aligned with your release train. - **A default is invisible.** A value nobody wrote down is a value nobody reviewed. It also differs between environments the moment two of them run different dependency versions, and it can change underneath you on an upgrade that nothing in your own diff mentions. - **Configuration can be asserted and audited.** You can refuse to boot on a bad value, log the effective set on startup, and read from a running instance what it actually accepts. You cannot do any of that with a value that exists only as a library's fallback. ## The migration is the whole reason An algorithm change is a three-state sequence, and the list is the only thing that moves: | Phase | Accepted list holds | The risk in this phase | |---|---|---| | Before | The current algorithm only | None; this is the steady state | | Overlap | Current **and** incoming | Widest acceptance; must be time-boxed and dated | | After | The incoming algorithm only | None — **provided somebody actually removed the old value** | The step teams miss is the third. Widening is urgent because tokens start failing without it; narrowing is not urgent for anybody, so it does not happen. The result an audit finds a year later is a fleet that still accepts a retired algorithm nobody has signed with in months — the acceptance surface of the migration without any of its benefit. Make the narrowing a dated task at the moment you widen, and give it an owner. ## Guardrails that make the configured list true A configured value is only worth what enforces it: 1. **Fail to start on an absent or empty list.** The empty case must be a refusal, never "accept anything the code can parse". This is the single highest-value line in the whole subject. 2. **Log the effective set at boot**, so an incident can be answered from the logs rather than from a guess about which configuration layer won. 3. **Keep a negative test.** A token signed with an algorithm outside the list must be rejected, asserted in a test that runs on every change. Positive tests do not catch a list that quietly grew. 4. **Check environment parity.** A permissive value set for a test environment must not be able to reach production; treat the configured set like any other security-relevant setting, with a review on change. 5. **Do not derive the list from the issuer's published key set.** That inverts the control: the list exists precisely so the accepted set is yours, decided in advance, and does not follow whatever the issuer's document happens to contain today. ## Where the check sits in the verify path Order matters for a reason beyond tidiness. If the algorithm check runs first, a token naming an algorithm you do not accept is rejected using nothing but locally held configuration — no key selection, no lookup, no network call. That keeps the cheapest possible rejection on the path an attacker can trigger most easily, and it means a flood of nonsense tokens costs your verifier a string comparison each. Run the check after key selection instead, and every malformed token has already cost you the work of looking one up. ## What the list is not It is not key management: it says which **kind** of signature you will check, never which key you will check it with or where that key came from. It is not a substitute for verifying the signature itself, and it does not tell you whether the signer was the issuer you expect — that is what the key and the issuer claim are for. It is one narrow, explicit, operator-owned statement: *these and only these algorithms will ever be honoured here*.

  • Your configuration layer supplies no value for the accepted list. Why is refusing to start better than accepting everything the verifier can parse?
    Because the two failures are not comparable. Refusing to start is loud, immediate, and caught by the deployment that caused it. Accepting everything is silent, ships successfully, and only shows up when someone exploits it. A missing security-relevant value is a misconfiguration, and the safe reading of a misconfiguration is 'stop', not 'be maximally permissive'.
  • Could you just derive the accepted algorithms from the key set the issuer publishes?
    That inverts the control. The list exists so the decision about what you will honour is yours, made in advance and reviewable. Deriving it means the issuer's document silently decides, and anything that can influence that document — including a misdirected fetch — widens your acceptance without a deployment. Read the key set for keys; keep the algorithm decision local.

saying these in an interview costs you the question

  • Lets the token's own header decide how it is checked
  • Treats an empty accepted list as 'accept anything'
  • Leaves the set to whatever the verifying library defaults to
  • Widens the list for a migration and never narrows it
  • Derives the accepted algorithms from the issuer's published key set
  • Checks the algorithm only after a key has been selected