first commit
This commit is contained in:
59
docs/security-stack/security/jws/src/keys/resolver.md
Normal file
59
docs/security-stack/security/jws/src/keys/resolver.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# `KeyResolver`
|
||||
|
||||
## Purpose
|
||||
|
||||
Caller-supplied key-resolution function consulted when `header.kid` is present and no key has been supplied via `opts.key`, `header.jwk`, or `header.x5c`. The resolver receives the parsed header so dispatch can branch on additional parameters (`iss`, custom tenant headers, environment-specific kid namespaces). Returning `undefined` surfaces as `JwsError(JWS.KEY_NOT_FOUND)`; throwing propagates verbatim so callers can map their own error taxonomy onto JWS verification.
|
||||
|
||||
## Features
|
||||
|
||||
- Async or sync — both signatures are accepted.
|
||||
- Receives `(kid, header)`; the header is the parsed `JoseHeader`, including unknown parameters.
|
||||
- `undefined` / `null` return → typed `JWS.KEY_NOT_FOUND` thrown by the pipeline.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { compactVerify, type KeyResolver } from "@suaveplan/jws";
|
||||
|
||||
const keyResolver: KeyResolver = async (kid) => keyStore.get(kid);
|
||||
|
||||
await compactVerify(token, undefined, {
|
||||
algorithms: ["RS256"],
|
||||
keyResolver,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { compactVerify, JwsError, type KeyResolver } from "@suaveplan/jws";
|
||||
|
||||
// Dispatch on a custom tenant header:
|
||||
const keyResolver: KeyResolver = async (kid, header) => {
|
||||
const tenant = header["tenant"] as string | undefined;
|
||||
if (tenant === "acme") return acmeKeyStore.get(kid);
|
||||
if (tenant === "globex") return globexKeyStore.get(kid);
|
||||
return undefined; // → KEY_NOT_FOUND
|
||||
};
|
||||
|
||||
try {
|
||||
await compactVerify(token, undefined, {
|
||||
algorithms: ["RS256"],
|
||||
keyResolver,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof JwsError && err.code === "JWS.KEY_NOT_FOUND") {
|
||||
// Inspect err.context.kid for the unresolved id.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type KeyResolver = (kid: string, header: JoseHeader) => Promise<unknown> | unknown`
|
||||
|
||||
Returning anything other than `undefined` / `null` is treated as the verification key and passed to the algorithm impl unchanged. The pipeline performs no type-check on the returned value — it relies on the algorithm impl to throw `JWS.KEY_ALG_MISMATCH` if the resolved key doesn't match the declared `alg`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Resolution priority in the verify pipeline is `opts.key → header.jwk → header.x5c → opts.keyResolver(header.kid)`. The resolver only runs when none of the earlier sources resolved a key; this means a token carrying both `kid` AND `jwk` will route through `jwk` first (assuming `allowEmbeddedJwk: true`). That ordering is deliberate: when a producer self-describes a key in the token, the consumer's stored key for the same `kid` should be re-verified against the embedded one — but doing that match is the consumer's responsibility, not this library's. The pipeline simply uses whichever source the caller has opted in to first.
|
||||
Reference in New Issue
Block a user