@hivemagnus/hive-common 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,66 @@ All notable changes to `@hivemagnus/hive-common` are recorded here. The format f
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-11-12
10
+
11
+ Native-issuer service tokens. Oathweave signs client-credentials tokens with its native issuer
12
+ (its `JWT_ISSUER`, for example `hive-iam`), which is a name, not an https URL, so 0.2.0 refused the
13
+ configuration and products kept a local jose check for those tokens. Every change is additive:
14
+ without the new options a verifier behaves as in 0.2.0.
15
+
16
+ ### Added
17
+
18
+ - `/oathweave`: `nativeIssuers` accepts tokens whose `iss` is one of the listed native
19
+ identifiers. Each must match `OATHWEAVE_NATIVE_ISSUER_PATTERN` (`^[a-z][a-z0-9-]{0,63}$`), so a
20
+ URL or anything else is a startup error. They are verified with the same `jwksUrl` (still https)
21
+ or `keys` as the https issuer, because Oathweave signs both with one key. A native issuer is never
22
+ accepted unless listed. `verifier.nativeIssuers` lists them.
23
+ - `/oathweave`: `nativeIssuerAccept` (default `'service'`) limits which principals a native issuer
24
+ may sign for. A user token from a native issuer is refused with `invalid_issuer` by default,
25
+ before any userinfo call.
26
+ - `/oathweave`: `maxLifetimeSec` also takes `{ user?, service? }`, one limit per principal kind;
27
+ each defaults to 900. `ClaimPolicy.maxLifetimeSec` and `principalFromClaims()` take the same
28
+ shape. The `OathweaveLifetimeLimits` type is exported.
29
+ - `/oathweave/conformance`: `ConformanceVectors.nativeIssuers` and `ConformanceContext.nativeIssuers`.
30
+ `hiveCommonConformanceVerify()` configures them.
31
+ - Conformance vectors: `valid-native-service` (accept), `unconfigured-native-issuer` (401
32
+ `invalid_issuer`) and `native-service-lifetime-too-long` (401 `invalid_lifetime`). There is no
33
+ vector for a native-issuer user token, because Oathweave does issue those (to registered clients
34
+ that are not suite browser clients); refusing them is a product setting, not a contract.
35
+ - README: "Native issuers", and native-issuer and per-kind lifetime rows in "Choosing settings for
36
+ an existing product".
37
+
38
+ ### Changed
39
+
40
+ - The conformance vectors were regenerated with `npm run vectors:generate` to sign the new tokens,
41
+ so every token and the test JWKS changed (the signing keys are discarded after each run). The
42
+ existing vectors keep their names, claims and expected outcomes. A product verifier that is not
43
+ configured with `ctx.nativeIssuers` now fails `valid-native-service`; configure it, or exclude
44
+ that vector with `only` until the product accepts native issuers.
45
+
46
+ ## [0.2.0] - 2026-11-12
47
+
48
+ Options that let a product keep its existing behaviour without wrapping the shared helpers. Every
49
+ change is additive: the defaults are those of 0.1.0.
50
+
51
+ ### Added
52
+
53
+ - `/oathweave`: `requireRoles` (default `false`) refuses a token without a `roles` array.
54
+ - `/oathweave`: `clientIdClaim: 'clientId' | 'client_id' | 'either'` (default `'either'`) chooses
55
+ which claim names supply the client ID. Both claims present must still agree. `clientIdOf()`,
56
+ `principalKindOf()` and `extractRoles()` take matching optional arguments, and `ClaimPolicy`
57
+ matching optional fields. `CLIENT_ID_CLAIMS` and the `ClientIdClaim` type are exported.
58
+ - `/oathweave`: `parseIdentityLinks()` accepts `localId` as an alias of `localUserId`. An entry
59
+ that sets both to different values is refused. Parsed links always use `localUserId`.
60
+ - `/context`: `trustInboundRequestId` and `trustInboundTrace` trust the inbound request ID and the
61
+ inbound `traceparent`/`tracestate` separately. Each defaults to `trustInbound`. The `/hono` and
62
+ `/node-http` adapters take them too.
63
+ - `/health`: `liveBody` replaces the `/health/live` body with a product's own, as an object or a
64
+ function of the standard body. `liveness()`, `createHealth()`, `Health`, `registerHealthRoutes()`
65
+ and `hiveHandler({ health })` accept it; `HealthOptions` and `Health` take an optional type
66
+ parameter for the body, defaulting to `LiveBody`.
67
+ - README: "Choosing settings for an existing product".
68
+
9
69
  ## [0.1.0] - 2026-11-12
10
70
 
11
71
  First release: the shared server kit that replaces each product's own copy of the Oathweave
@@ -37,5 +97,7 @@ verifier and its own error, health and request-ID conventions.
37
97
  pagination and agent identity.
38
98
  - `/hono` and `/node-http` adapters for auth, request context, the error envelope and health.
39
99
 
40
- [Unreleased]: https://github.com/ChefTaco/HiveCommon/compare/v0.1.0...HEAD
100
+ [Unreleased]: https://github.com/ChefTaco/HiveCommon/compare/v0.3.0...HEAD
101
+ [0.3.0]: https://github.com/ChefTaco/HiveCommon/compare/v0.2.0...v0.3.0
102
+ [0.2.0]: https://github.com/ChefTaco/HiveCommon/compare/v0.1.0...v0.2.0
41
103
  [0.1.0]: https://github.com/ChefTaco/HiveCommon/releases/tag/v0.1.0
package/README.md CHANGED
@@ -100,11 +100,13 @@ What it checks, in order: a compact JWT of at most 16 KiB; header `alg` is `RS25
100
100
  HMAC key confusion never reach a key); signature against the JWKS (cached for 10 minutes and
101
101
  refetched, at most every 30 s, when a token names an unknown `kid`); `iss` is one of the issuers;
102
102
  `aud` is exactly the audience; `sub`, `iat`, `exp`, `jti` present; `exp`, `nbf` and `iat` within
103
- 30 s of clock tolerance; `exp - iat` and token age at most 900 s; then the principal rules:
103
+ 30 s of clock tolerance; `exp - iat` and token age at most 900 s (`maxLifetimeSec`, one number or
104
+ `{ user, service }`); then the principal rules:
104
105
 
105
106
  - **User:** `actorType: "USER"`, a `sessionId`, a `clientId` other than the subject, string-array
106
107
  `permissions` and (optional) `roles`. With `userinfoUrl`, the session is confirmed online; the
107
- answer is shared for 10 s from when the request started (failures for 2 s).
108
+ answer is shared for 10 s from when the request started (failures for 2 s), or not at all with
109
+ `sessionCacheTtlMs: 0`.
108
110
  - **Service:** `clientId` (or `client_id`) equals `sub`, `actorType: "SERVICE_ACCOUNT"`,
109
111
  `token_type: "access"`, no session.
110
112
 
@@ -129,13 +131,56 @@ const watch = watchStreamAuthorization({
129
131
  });
130
132
  ```
131
133
 
134
+ Two options make the claim rules stricter for products that always required more:
135
+
136
+ - `requireRoles: true` refuses a token without a `roles` array. The default (`false`) treats an
137
+ absent `roles` claim as no roles. A `roles` claim that is not a string array is always refused.
138
+ - `clientIdClaim` chooses which claim names supply the client ID: `'clientId'` (Oathweave),
139
+ `'client_id'` (RFC 9068) or `'either'` (the default). When both claims are present they must
140
+ agree in every mode.
141
+
142
+ #### Native issuers
143
+
144
+ Oathweave signs browser-client tokens with its https issuer, but client-credentials tokens with its
145
+ *native* issuer (its `JWT_ISSUER`, for example `hive-iam`) unless the client's machine policy selects
146
+ the browser issuer. Both are signed by the same key and published in the same JWKS. List the native
147
+ identifiers in `nativeIssuers` to accept those tokens too:
148
+
149
+ ```ts
150
+ const verifier = createOathweaveVerifier({
151
+ issuer: process.env.OATHWEAVE_ISSUER!, // still required, still https
152
+ nativeIssuers: ['hive-iam'], // exact `iss` values; never accepted unless listed
153
+ audience: 'hivemem',
154
+ jwksUrl: process.env.OATHWEAVE_JWKS_URL!, // the one https JWKS serves both issuers
155
+ userinfoUrl: process.env.OATHWEAVE_USERINFO_URL,
156
+ accept: 'service',
157
+ });
158
+ ```
159
+
160
+ - Each entry must match `OATHWEAVE_NATIVE_ISSUER_PATTERN` (`^[a-z][a-z0-9-]{0,63}$`), so a URL or
161
+ any other shape is a startup error. There is no separate JWKS setting: `jwksUrl` (https, or
162
+ loopback http with `allowInsecureLoopback`) or `keys` verify both kinds of issuer.
163
+ - A native issuer signs service principals only, by default: a user token from a native issuer is
164
+ refused with `invalid_issuer` before any session check. Suite user tokens come from browser
165
+ clients, which Oathweave always signs with the https issuer. `nativeIssuerAccept: 'any'` (or
166
+ `'user'`) changes that for a product that really takes native user tokens.
167
+ - Every other rule (audience, lifetime, principal rules) is unchanged. `principal.issuer` is the
168
+ native identifier, so identity and service links keyed by `(issuer, clientId)` keep working.
169
+ `verifier.nativeIssuers` lists them; `verifier.issuers` still lists only the https issuers.
170
+
171
+ Identity links name the local account `localUserId`. `localId`, the name older deployment configs
172
+ use, is accepted as an alias; an entry that sets both to different values is refused. Parsed links
173
+ always carry `localUserId`.
174
+
132
175
  Endpoints must be `https`. For tests against a loopback server, pass `allowInsecureLoopback: true`.
176
+ Native issuer identifiers are names, not endpoints, and are never fetched.
133
177
 
134
178
  ### Conformance vectors
135
179
 
136
- `test-vectors/oathweave/vectors.json` ships a test JWKS and twelve pre-signed tokens (valid user,
180
+ `test-vectors/oathweave/vectors.json` ships a test JWKS and fifteen pre-signed tokens (valid user,
137
181
  valid service, expired, not yet valid, wrong audience, wrong issuer, bad signature, unknown `kid`,
138
- `alg: none`, HS256 key confusion, overlong lifetime, user without session), evaluated at a fixed
182
+ `alg: none`, HS256 key confusion, overlong lifetime, user without session, valid native-issuer
183
+ service, unconfigured native issuer, overlong native-issuer service token), evaluated at a fixed
139
184
  clock so they never expire. Run them against your product's verifier:
140
185
 
141
186
  ```ts
@@ -144,13 +189,15 @@ import { runOathweaveConformance } from '@hivemagnus/hive-common/oathweave/confo
144
189
  it('meets the Oathweave conformance vectors', async () => {
145
190
  await runOathweaveConformance((token, ctx) => createProductVerifier({
146
191
  issuer: ctx.issuer, audience: ctx.audience, jwksUrl: ctx.jwksUrl, userinfoUrl: ctx.userinfoUrl,
192
+ nativeIssuers: ctx.nativeIssuers, // native issuers the vectors use, for service tokens
147
193
  fetch: ctx.fetch, // serves the vectors' JWKS and userinfo
148
194
  now: () => ctx.nowMs, // the fixed clock
149
195
  })(token));
150
196
  });
151
197
  ```
152
198
 
153
- It throws a readable error listing failed vectors. A refusal passes when the verifier rejects
199
+ A verifier that does not accept native issuers fails `valid-native-service`; pass `only` to run
200
+ the other vectors until it does. It throws a readable error listing failed vectors. A refusal passes when the verifier rejects
154
201
  (and any numeric `status` on the error matches); `strictReasons: true` also compares `reason`.
155
202
 
156
203
  ### errors
@@ -205,6 +252,16 @@ A check is down when it throws, returns `false` or `{ ok: false }`, or exceeds i
205
252
  `signal` aborts). Error text is hidden unless `exposeErrors: true`. Serve legacy aliases only
206
253
  while old clients still call them.
207
254
 
255
+ A product that has published its own liveness body can keep it with `liveBody`: an object served
256
+ as is, or a function that receives the standard body on every request. Status, headers and
257
+ readiness are unchanged, and liveness still never runs checks. `liveness()`, `createHealth()`,
258
+ `registerHealthRoutes()` and `hiveHandler({ health })` all take it.
259
+
260
+ ```ts
261
+ createHealth({ service: 'studio', liveBody: { status: 'ok' } });
262
+ createHealth({ service: 'hive-skills', liveBody: standard => ({ status: 'ok', service: standard.service, protocol: 1 }) });
263
+ ```
264
+
208
265
  ### context
209
266
 
210
267
  ```ts
@@ -221,6 +278,12 @@ Also `parseTraceparent()`, `formatTraceparent()`, `generateTraceparent()`, `chil
221
278
  `readRequestId()`, `requireIdempotencyKey()` (428 when missing). On an edge service exposed to the
222
279
  internet, pass `trustInbound: false` so callers cannot choose IDs.
223
280
 
281
+ `trustInboundRequestId` and `trustInboundTrace` decide the request ID and the trace separately; each
282
+ defaults to `trustInbound` (itself `true`). An internet-facing service that should still join its
283
+ callers' traces passes `{ trustInboundRequestId: false, trustInboundTrace: true }`. The `/hono`
284
+ `requestContext()` middleware and the `/node-http` `hiveHandler()` and `requestContextFor()` take
285
+ the same options.
286
+
224
287
  ### registry
225
288
 
226
289
  ```ts
@@ -366,6 +429,55 @@ Behaviour that changes when a product adopts the shared verifier:
366
429
  - Duplicate identity links fail at startup by default (`duplicates: 'ignore'` keeps the old
367
430
  Magnus behaviour of granting nothing to that identity).
368
431
 
432
+ ## Choosing settings for an existing product
433
+
434
+ The defaults suit a new service. A product moving from its own code to HiveCommon can keep its
435
+ existing behaviour with these settings instead of wrapping the shared helpers:
436
+
437
+ | Setting | Default | Set it to keep |
438
+ | --- | --- | --- |
439
+ | `clockToleranceSec: 0` (verifier) | 30 | no clock skew allowance on `exp`, `nbf` and `iat` |
440
+ | `sessionCacheTtlMs: 0` (verifier) | 10 000 | an online session check on every request, so sign-out is immediate |
441
+ | `requireRoles: true` (verifier) | `false` | refusing tokens without a `roles` array |
442
+ | `clientIdClaim: 'clientId'` (verifier) | `'either'` | requiring the Oathweave `clientId` claim; RFC 9068 `client_id` alone is refused |
443
+ | `nativeIssuers: ['hive-iam']` (verifier) | none | accepting client-credentials tokens signed with Oathweave's native issuer, which a local jose check used to handle |
444
+ | `maxLifetimeSec: { user, service }` (verifier) | 900 for both | a different limit per principal kind, for example `{ user: 900, service: 300 }` |
445
+ | `trustInboundRequestId: false` with `trustInboundTrace: true` (context, `/hono`, `/node-http`) | both `trustInbound` | generating every request ID on an internet-facing service while continuing callers' traces |
446
+ | `liveBody` (health, `/hono`, `/node-http`) | the standard body | a published `/health/live` body such as `{ status: 'ok' }` |
447
+ | `localId` in identity links | `localUserId` | existing link configs, unchanged |
448
+ | `duplicates: 'ignore'` (identity links) | `'reject'` | granting nothing to a duplicated identity instead of failing at startup |
449
+
450
+ ```ts
451
+ const verifier = createOathweaveVerifier({
452
+ issuer, audience: 'chattermost', jwksUrl, userinfoUrl,
453
+ clockToleranceSec: 0, sessionCacheTtlMs: 0, requireRoles: true, clientIdClaim: 'clientId',
454
+ });
455
+ app.use(requestContext({ trustInboundRequestId: false, trustInboundTrace: true }));
456
+ registerHealthRoutes(app, { service: 'chattermost', liveBody: { status: 'ok' } });
457
+ ```
458
+
459
+ Every refusal, including 503 `auth_unavailable`, is still an `OathweaveError`; a product that
460
+ answers 401 for all of them maps the error in its own handler.
461
+
462
+ Service tokens from a native issuer. Products that kept a local jose verifier for Oathweave's
463
+ native issuer replace it with a second shared verifier on the same key source, so one JWKS cache
464
+ serves both:
465
+
466
+ ```ts
467
+ const keys = createRemoteJWKSet(new URL(jwksUrl)); // or let each verifier fetch jwksUrl itself
468
+ const users = createOathweaveVerifier({ issuer, audience: 'hivemem', keys, userinfoUrl, /* user settings */ });
469
+ const services = createOathweaveVerifier({
470
+ issuer, // https; browser-issued machine clients use it
471
+ nativeIssuers: ['hive-iam'], // Oathweave's JWT_ISSUER, from configuration
472
+ audience: 'hivemem', keys, accept: 'service',
473
+ clientIdClaim: 'clientId', // Oathweave service tokens always carry clientId
474
+ });
475
+ ```
476
+
477
+ The fifteen-minute limit applies to service tokens as before (`maxLifetimeSec` sets it per kind).
478
+ Native identifiers are exact: configure the value Oathweave's `JWT_ISSUER` is set to, and keep the
479
+ service links that grant access keyed by that issuer.
480
+
369
481
  ## Development
370
482
 
371
483
  ```bash
@@ -66,9 +66,21 @@ export interface RequestContext {
66
66
  export interface CreateRequestContextOptions {
67
67
  /**
68
68
  * Accept the inbound request ID and trace context. Defaults to true; set false on an edge
69
- * service that must not let the public internet choose its IDs.
69
+ * service that must not let the public internet choose its IDs. The two settings below
70
+ * override it for one of the two.
70
71
  */
71
72
  readonly trustInbound?: boolean;
73
+ /**
74
+ * Accept an inbound `x-request-id` (or `x-hive-request-id`, `x-correlation-id`). Defaults to
75
+ * `trustInbound`. When false, every request gets a generated ID.
76
+ */
77
+ readonly trustInboundRequestId?: boolean;
78
+ /**
79
+ * Continue an inbound `traceparent` and `tracestate`. Defaults to `trustInbound`. When false,
80
+ * every request starts a new trace. An internet-facing service can set this true with
81
+ * `trustInboundRequestId: false` to join a caller's trace while choosing its own request IDs.
82
+ */
83
+ readonly trustInboundTrace?: boolean;
72
84
  readonly generateRequestId?: () => string;
73
85
  readonly now?: () => number;
74
86
  }
@@ -147,8 +147,10 @@ export function requireIdempotencyKey(headers) {
147
147
  }
148
148
  export function createRequestContext(headers, options = {}) {
149
149
  const trust = options.trustInbound ?? true;
150
- const requestId = (trust ? readRequestId(headers) : undefined) ?? (options.generateRequestId ?? generateRequestId)();
151
- const parentTrace = trust ? parseTraceparent(readHeader(headers, HIVE_HEADERS.traceparent)) : undefined;
150
+ const trustRequestId = options.trustInboundRequestId ?? trust;
151
+ const trustTrace = options.trustInboundTrace ?? trust;
152
+ const requestId = (trustRequestId ? readRequestId(headers) : undefined) ?? (options.generateRequestId ?? generateRequestId)();
153
+ const parentTrace = trustTrace ? parseTraceparent(readHeader(headers, HIVE_HEADERS.traceparent)) : undefined;
152
154
  const rawState = parentTrace ? readHeader(headers, HIVE_HEADERS.tracestate)?.trim() : undefined;
153
155
  // eslint-disable-next-line no-control-regex
154
156
  const tracestate = rawState && rawState.length <= 512 && !/[\u0000-\u001f\u007f]/.test(rawState) ? rawState : undefined;
@@ -51,7 +51,13 @@ export interface HealthResponse<B> {
51
51
  readonly body: B;
52
52
  readonly headers: Readonly<Record<string, string>>;
53
53
  }
54
- export interface HealthOptions {
54
+ /**
55
+ * A product's own liveness body, in place of the standard `LiveBody`: an object served as is,
56
+ * or a function called on every liveness request with the standard body.
57
+ */
58
+ export type LiveBodyOverride<L extends object> = L | ((standard: LiveBody) => L);
59
+ /** `L` is the liveness body type: `LiveBody` unless `liveBody` is set. */
60
+ export interface HealthOptions<L extends object = LiveBody> {
55
61
  readonly service?: string;
56
62
  readonly version?: string;
57
63
  readonly checks?: readonly HealthCheck[];
@@ -65,21 +71,31 @@ export interface HealthOptions {
65
71
  /** Also serve the legacy paths in `LEGACY_HEALTH_ALIASES`, or a custom alias map. */
66
72
  readonly aliases?: boolean | Partial<Readonly<Record<HealthKind, readonly string[]>>>;
67
73
  readonly now?: () => number;
74
+ /**
75
+ * Serve this body from `/health/live` instead of the standard one, for a product that keeps
76
+ * a published liveness body such as `{ status: 'ok' }`. The status (200) and headers are
77
+ * unchanged, and liveness still never runs checks. Readiness is not affected.
78
+ */
79
+ readonly liveBody?: LiveBodyOverride<L>;
68
80
  }
69
- /** The liveness response: 200 whenever the process can answer. It never runs checks. */
70
- export declare function liveness(options?: HealthOptions): HealthResponse<LiveBody>;
81
+ /**
82
+ * The liveness response: 200 whenever the process can answer. It never runs checks. The body
83
+ * is the standard `LiveBody`, or the product's own when `liveBody` is set.
84
+ */
85
+ export declare function liveness<L extends object = LiveBody>(options?: HealthOptions<L>): HealthResponse<L>;
71
86
  /** Run every check concurrently and build the readiness response: 503 if a critical check is down. */
72
- export declare function readiness(options?: HealthOptions): Promise<HealthResponse<ReadyBody>>;
87
+ export declare function readiness(options?: HealthOptions<object>): Promise<HealthResponse<ReadyBody>>;
73
88
  /** Which health endpoint a path names, including legacy aliases when enabled. */
74
89
  export declare function resolveHealthPath(pathname: string, aliases?: HealthOptions['aliases']): HealthKind | undefined;
75
90
  /** All paths served for each kind, standard first. */
76
91
  export declare function healthPaths(aliases?: HealthOptions['aliases']): Readonly<Record<HealthKind, readonly string[]>>;
77
- export interface Health {
78
- live(): HealthResponse<LiveBody>;
92
+ /** `L` is the liveness body type: `LiveBody` unless `liveBody` was set. */
93
+ export interface Health<L extends object = LiveBody> {
94
+ live(): HealthResponse<L>;
79
95
  ready(): Promise<HealthResponse<ReadyBody>>;
80
96
  /** The response for a health path, or undefined when the path is not a health path. */
81
- handle(pathname: string): Promise<HealthResponse<LiveBody | ReadyBody>> | undefined;
97
+ handle(pathname: string): Promise<HealthResponse<L | ReadyBody>> | undefined;
82
98
  readonly paths: Readonly<Record<HealthKind, readonly string[]>>;
83
99
  }
84
100
  /** Bind options once and serve both endpoints. */
85
- export declare function createHealth(options?: HealthOptions): Health;
101
+ export declare function createHealth<L extends object = LiveBody>(options?: HealthOptions<L>): Health<L>;
@@ -12,10 +12,22 @@ const HEADERS = { 'content-type': 'application/json; charset=utf-8', 'cache-cont
12
12
  function identity(options) {
13
13
  return { ...(options.service ? { service: options.service } : {}), ...(options.version ? { version: options.version } : {}) };
14
14
  }
15
- /** The liveness response: 200 whenever the process can answer. It never runs checks. */
15
+ function assertLiveBody(value) {
16
+ if (value !== undefined && typeof value !== 'function' && (typeof value !== 'object' || value === null || Array.isArray(value))) {
17
+ throw new TypeError('liveBody must be an object or a function');
18
+ }
19
+ }
20
+ /**
21
+ * The liveness response: 200 whenever the process can answer. It never runs checks. The body
22
+ * is the standard `LiveBody`, or the product's own when `liveBody` is set.
23
+ */
16
24
  export function liveness(options = {}) {
25
+ assertLiveBody(options.liveBody);
17
26
  const now = options.now ?? Date.now;
18
- return { status: 200, body: { status: 'live', ...identity(options), time: new Date(now()).toISOString() }, headers: HEADERS };
27
+ const standard = { status: 'live', ...identity(options), time: new Date(now()).toISOString() };
28
+ const custom = options.liveBody;
29
+ const body = custom === undefined ? standard : typeof custom === 'function' ? custom(standard) : { ...custom };
30
+ return { status: 200, body: body, headers: HEADERS };
19
31
  }
20
32
  class CheckTimeout extends Error {
21
33
  }
@@ -97,6 +109,7 @@ function aliasMap(aliases) {
97
109
  }
98
110
  /** Bind options once and serve both endpoints. */
99
111
  export function createHealth(options = {}) {
112
+ assertLiveBody(options.liveBody);
100
113
  return {
101
114
  live: () => liveness(options),
102
115
  ready: () => readiness(options),
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { Context, Env, ErrorHandler, Hono, MiddlewareHandler, NotFoundHandler } from 'hono';
13
13
  import { type CreateRequestContextOptions, type RequestContext } from '../context/index.js';
14
- import { type HealthOptions } from '../health/index.js';
14
+ import { type HealthOptions, type LiveBody } from '../health/index.js';
15
15
  import type { Logger } from '../log/index.js';
16
16
  import type { AcceptPrincipal, OathweavePrincipal } from '../oathweave/claims.js';
17
17
  import { type PermissionOptions } from '../oathweave/links.js';
@@ -60,5 +60,8 @@ export interface ErrorHandlerOptions {
60
60
  export declare function errorHandler(options?: ErrorHandlerOptions): ErrorHandler;
61
61
  /** `app.notFound(notFoundHandler())`: a 404 envelope. */
62
62
  export declare function notFoundHandler(): NotFoundHandler;
63
- /** Serve `/health/live`, `/health/ready` and any aliases on `app` (GET and HEAD). */
64
- export declare function registerHealthRoutes<E extends Env>(app: Hono<E>, options?: HealthOptions): void;
63
+ /**
64
+ * Serve `/health/live`, `/health/ready` and any aliases on `app` (GET and HEAD). Set
65
+ * `liveBody` to keep a product's published liveness body.
66
+ */
67
+ export declare function registerHealthRoutes<E extends Env, L extends object = LiveBody>(app: Hono<E>, options?: HealthOptions<L>): void;
@@ -91,7 +91,10 @@ export function errorHandler(options = {}) {
91
91
  export function notFoundHandler() {
92
92
  return c => respond(c, HiveError.notFound(), {});
93
93
  }
94
- /** Serve `/health/live`, `/health/ready` and any aliases on `app` (GET and HEAD). */
94
+ /**
95
+ * Serve `/health/live`, `/health/ready` and any aliases on `app` (GET and HEAD). Set
96
+ * `liveBody` to keep a product's published liveness body.
97
+ */
95
98
  export function registerHealthRoutes(app, options = {}) {
96
99
  const health = createHealth(options);
97
100
  const serve = (kind) => async (c) => {
@@ -24,11 +24,14 @@ export interface SendErrorOptions {
24
24
  }
25
25
  /** Send any thrown value as the error envelope. Non-HiveErrors become a logged, generic 500. */
26
26
  export declare function sendError(res: ServerResponse, error: unknown, options?: SendErrorOptions): void;
27
- /** Serve a health path. Returns true when `req` was a health request and has been answered. */
28
- export declare function handleHealth(req: IncomingMessage, res: ServerResponse, health: Health): Promise<boolean>;
27
+ /**
28
+ * Serve a health path. Returns true when `req` was a health request and has been answered.
29
+ * The liveness body is the one `health` was created with (see `liveBody`).
30
+ */
31
+ export declare function handleHealth(req: IncomingMessage, res: ServerResponse, health: Health<object>): Promise<boolean>;
29
32
  export interface HiveHandlerOptions extends CreateRequestContextOptions {
30
33
  /** Serve health endpoints before `handler` runs. */
31
- readonly health?: HealthOptions | Health;
34
+ readonly health?: HealthOptions<object> | Health<object>;
32
35
  readonly logger?: Pick<Logger, 'error'>;
33
36
  /** Echo `x-request-id` on every response. Defaults to true. */
34
37
  readonly responseHeader?: boolean;
@@ -46,7 +46,10 @@ export function sendError(res, error, options = {}) {
46
46
  });
47
47
  sendJson(res, response.status, response.body, response.headers);
48
48
  }
49
- /** Serve a health path. Returns true when `req` was a health request and has been answered. */
49
+ /**
50
+ * Serve a health path. Returns true when `req` was a health request and has been answered.
51
+ * The liveness body is the one `health` was created with (see `liveBody`).
52
+ */
50
53
  export async function handleHealth(req, res, health) {
51
54
  if (req.method !== 'GET' && req.method !== 'HEAD')
52
55
  return false;
@@ -38,18 +38,42 @@ export interface OathweaveServicePrincipal extends PrincipalBase {
38
38
  readonly kind: 'service';
39
39
  }
40
40
  export type OathweavePrincipal = OathweaveUserPrincipal | OathweaveServicePrincipal;
41
- /** The client claim: Oathweave's `clientId`, or RFC 9068 `client_id`. Both present must agree. */
42
- export declare function clientIdOf(payload: JWTPayload): string | undefined;
41
+ /**
42
+ * Which claim names carry the client ID: Oathweave's `clientId`, RFC 9068 `client_id`, or
43
+ * `either` (the default).
44
+ */
45
+ export type ClientIdClaim = 'clientId' | 'client_id' | 'either';
46
+ export declare const CLIENT_ID_CLAIMS: readonly ClientIdClaim[];
47
+ /**
48
+ * The client claim: Oathweave's `clientId`, or RFC 9068 `client_id`. Both present must agree,
49
+ * whichever name is accepted. With `clientId` or `client_id`, only that claim supplies the value.
50
+ */
51
+ export declare function clientIdOf(payload: JWTPayload, claim?: ClientIdClaim): string | undefined;
43
52
  /** Scopes from `scope` (space-separated) or `scp` (array or string), de-duplicated. */
44
53
  export declare function extractScopes(payload: JWTPayload): string[];
45
- /** Roles; an absent claim is no roles. A present claim that is not a string array is invalid. */
46
- export declare function extractRoles(payload: JWTPayload): string[] | undefined;
54
+ /**
55
+ * Roles; an absent claim is no roles, or invalid when `required` is true. A present claim that
56
+ * is not a string array is invalid.
57
+ */
58
+ export declare function extractRoles(payload: JWTPayload, required?: boolean): string[] | undefined;
47
59
  /** A service token is one whose client ID equals its subject. */
48
- export declare function principalKindOf(payload: JWTPayload): PrincipalKind;
60
+ export declare function principalKindOf(payload: JWTPayload, claim?: ClientIdClaim): PrincipalKind;
61
+ /** A lifetime limit in seconds for each principal kind. */
62
+ export interface OathweaveLifetimeLimits {
63
+ readonly user?: number;
64
+ readonly service?: number;
65
+ }
49
66
  export interface ClaimPolicy {
50
67
  readonly accept: AcceptPrincipal;
51
- /** Largest allowed `exp - iat`, in seconds. */
52
- readonly maxLifetimeSec: number;
68
+ /**
69
+ * Largest allowed `exp - iat`, in seconds: one limit for every token, or one per principal
70
+ * kind. A kind missing from the object gets `OATHWEAVE_MAX_LIFETIME_SEC` (900).
71
+ */
72
+ readonly maxLifetimeSec: number | OathweaveLifetimeLimits;
73
+ /** Refuse a token without a `roles` claim. Defaults to false: an absent claim is no roles. */
74
+ readonly requireRoles?: boolean;
75
+ /** Which claim names supply the client ID. Defaults to `either`. */
76
+ readonly clientIdClaim?: ClientIdClaim;
53
77
  }
54
78
  /**
55
79
  * Check the Oathweave claim rules on a payload whose signature, issuer, audience and time
@@ -1,17 +1,23 @@
1
1
  import { OathweaveError } from './errors.js';
2
+ /** Mirrors `OATHWEAVE_MAX_LIFETIME_SEC`, which lives in the verifier module. */
3
+ const MAX_LIFETIME_SEC = 900;
2
4
  function stringArray(value) {
3
5
  return Array.isArray(value) && value.every(item => typeof item === 'string');
4
6
  }
5
7
  function nonEmpty(value) {
6
8
  return typeof value === 'string' && value.length > 0;
7
9
  }
8
- /** The client claim: Oathweave's `clientId`, or RFC 9068 `client_id`. Both present must agree. */
9
- export function clientIdOf(payload) {
10
+ export const CLIENT_ID_CLAIMS = ['clientId', 'client_id', 'either'];
11
+ /**
12
+ * The client claim: Oathweave's `clientId`, or RFC 9068 `client_id`. Both present must agree,
13
+ * whichever name is accepted. With `clientId` or `client_id`, only that claim supplies the value.
14
+ */
15
+ export function clientIdOf(payload, claim = 'either') {
10
16
  const camel = payload['clientId'];
11
17
  const snake = payload['client_id'];
12
18
  if (camel !== undefined && snake !== undefined && camel !== snake)
13
19
  return undefined;
14
- const value = camel ?? snake;
20
+ const value = claim === 'clientId' ? camel : claim === 'client_id' ? snake : camel ?? snake;
15
21
  return typeof value === 'string' ? value : undefined;
16
22
  }
17
23
  /** Scopes from `scope` (space-separated) or `scp` (array or string), de-duplicated. */
@@ -20,16 +26,19 @@ export function extractScopes(payload) {
20
26
  const list = typeof raw === 'string' ? raw.split(/\s+/) : stringArray(raw) ? raw : [];
21
27
  return [...new Set(list.map(item => item.trim()).filter(Boolean))];
22
28
  }
23
- /** Roles; an absent claim is no roles. A present claim that is not a string array is invalid. */
24
- export function extractRoles(payload) {
29
+ /**
30
+ * Roles; an absent claim is no roles, or invalid when `required` is true. A present claim that
31
+ * is not a string array is invalid.
32
+ */
33
+ export function extractRoles(payload, required = false) {
25
34
  const roles = payload['roles'];
26
35
  if (roles === undefined)
27
- return [];
36
+ return required ? undefined : [];
28
37
  return stringArray(roles) ? [...roles] : undefined;
29
38
  }
30
39
  /** A service token is one whose client ID equals its subject. */
31
- export function principalKindOf(payload) {
32
- const clientId = clientIdOf(payload);
40
+ export function principalKindOf(payload, claim = 'either') {
41
+ const clientId = clientIdOf(payload, claim);
33
42
  return clientId !== undefined && clientId === payload.sub ? 'service' : 'user';
34
43
  }
35
44
  function validAuthTime(value) {
@@ -45,17 +54,19 @@ function validAuthTime(value) {
45
54
  */
46
55
  export function principalFromClaims(payload, policy) {
47
56
  const { iat, exp, sub, jti, iss } = payload;
48
- if (typeof iat !== 'number' || typeof exp !== 'number' || exp <= iat || exp - iat > policy.maxLifetimeSec) {
57
+ const clientIdClaim = policy.clientIdClaim ?? 'either';
58
+ const kind = principalKindOf(payload, clientIdClaim);
59
+ const maxLifetimeSec = typeof policy.maxLifetimeSec === 'number' ? policy.maxLifetimeSec : policy.maxLifetimeSec[kind] ?? MAX_LIFETIME_SEC;
60
+ if (typeof iat !== 'number' || typeof exp !== 'number' || exp <= iat || exp - iat > maxLifetimeSec) {
49
61
  throw new OathweaveError('invalid_lifetime');
50
62
  }
51
63
  if (!nonEmpty(sub) || !nonEmpty(jti) || !nonEmpty(iss))
52
64
  throw new OathweaveError('invalid_claims');
53
65
  const permissions = payload['permissions'];
54
- const roles = extractRoles(payload);
55
- const clientId = clientIdOf(payload);
66
+ const roles = extractRoles(payload, policy.requireRoles ?? false);
67
+ const clientId = clientIdOf(payload, clientIdClaim);
56
68
  if (!stringArray(permissions) || roles === undefined || clientId === undefined)
57
69
  throw new OathweaveError('invalid_claims');
58
- const kind = principalKindOf(payload);
59
70
  if (policy.accept !== 'any' && policy.accept !== kind)
60
71
  throw new OathweaveError('wrong_principal_kind', { details: { kind } });
61
72
  const base = {
@@ -25,6 +25,8 @@ export interface ConformanceVector {
25
25
  export interface ConformanceVectors {
26
26
  readonly schema: 1;
27
27
  readonly issuer: string;
28
+ /** Native issuer identifiers the verifier under test must be configured to accept. */
29
+ readonly nativeIssuers?: readonly string[];
28
30
  readonly audience: string;
29
31
  readonly jwksUrl: string;
30
32
  readonly userinfoUrl: string;
@@ -35,6 +37,11 @@ export interface ConformanceVectors {
35
37
  }
36
38
  export interface ConformanceContext {
37
39
  readonly issuer: string;
40
+ /**
41
+ * Native issuer identifiers (such as `hive-iam`) to configure alongside `issuer`, for service
42
+ * tokens. Empty when the vectors have none.
43
+ */
44
+ readonly nativeIssuers: readonly string[];
38
45
  readonly audience: string;
39
46
  readonly jwks: JSONWebKeySet;
40
47
  readonly jwksUrl: string;
@@ -76,6 +76,7 @@ export async function runOathweaveConformance(verify, options = {}) {
76
76
  continue;
77
77
  const context = {
78
78
  issuer: vectors.issuer,
79
+ nativeIssuers: vectors.nativeIssuers ?? [],
79
80
  audience: vectors.audience,
80
81
  jwks: vectors.jwks,
81
82
  jwksUrl: vectors.jwksUrl,
@@ -116,6 +117,7 @@ export async function runOathweaveConformance(verify, options = {}) {
116
117
  export function hiveCommonConformanceVerify() {
117
118
  return (token, context) => createOathweaveVerifier({
118
119
  issuer: context.issuer,
120
+ nativeIssuers: context.nativeIssuers,
119
121
  audience: context.audience,
120
122
  keys: context.jwks,
121
123
  now: () => context.nowMs,
@@ -2,8 +2,8 @@
2
2
  * `@hivemagnus/hive-common/oathweave`: Oathweave access-token verification for suite services.
3
3
  * Framework-free; see `/hono` and `/node-http` for middleware.
4
4
  */
5
- export { createOathweaveVerifier, parseBearerToken, OATHWEAVE_DEFAULT_CLOCK_TOLERANCE_SEC, OATHWEAVE_FAILED_CHECK_TTL_MS, OATHWEAVE_MAX_LIFETIME_SEC, OATHWEAVE_MAX_TOKEN_LENGTH, OATHWEAVE_REQUEST_TIMEOUT_MS, OATHWEAVE_SESSION_CACHE_MAX, OATHWEAVE_SESSION_CACHE_TTL_MS, OATHWEAVE_STREAM_RECHECK_MS, type OathweaveVerifier, type OathweaveVerifierOptions, type VerifyOptions, } from './verifier.js';
6
- export { clientIdOf, extractRoles, extractScopes, principalFromClaims, principalKindOf, type AcceptPrincipal, type ClaimPolicy, type OathweavePrincipal, type OathweaveServicePrincipal, type OathweaveUserPrincipal, type PrincipalKind, } from './claims.js';
5
+ export { createOathweaveVerifier, parseBearerToken, OATHWEAVE_DEFAULT_CLOCK_TOLERANCE_SEC, OATHWEAVE_FAILED_CHECK_TTL_MS, OATHWEAVE_MAX_LIFETIME_SEC, OATHWEAVE_MAX_TOKEN_LENGTH, OATHWEAVE_NATIVE_ISSUER_PATTERN, OATHWEAVE_REQUEST_TIMEOUT_MS, OATHWEAVE_SESSION_CACHE_MAX, OATHWEAVE_SESSION_CACHE_TTL_MS, OATHWEAVE_STREAM_RECHECK_MS, type OathweaveVerifier, type OathweaveVerifierOptions, type VerifyOptions, } from './verifier.js';
6
+ export { CLIENT_ID_CLAIMS, clientIdOf, extractRoles, extractScopes, principalFromClaims, principalKindOf, type AcceptPrincipal, type ClaimPolicy, type ClientIdClaim, type OathweaveLifetimeLimits, type OathweavePrincipal, type OathweaveServicePrincipal, type OathweaveUserPrincipal, type PrincipalKind, } from './claims.js';
7
7
  export { OathweaveError, isOathweaveError, statusForReason, type OathweaveFailureReason } from './errors.js';
8
8
  export { assertOathweaveEndpoint, endpointProblem, isLoopbackHost, isOathweaveEndpoint, type EndpointOptions } from './endpoint.js';
9
9
  export { effectivePermissions, hasAnyRole, hasPermissions, hasScopes, IdentityLinkError, parseIdentityLinks, permissionMatches, requireIdentityLink, requirePermissions, uniqueIdentityLink, type OathweaveIdentityLink, type ParseIdentityLinksOptions, type PermissionOptions, } from './links.js';
@@ -2,8 +2,8 @@
2
2
  * `@hivemagnus/hive-common/oathweave`: Oathweave access-token verification for suite services.
3
3
  * Framework-free; see `/hono` and `/node-http` for middleware.
4
4
  */
5
- export { createOathweaveVerifier, parseBearerToken, OATHWEAVE_DEFAULT_CLOCK_TOLERANCE_SEC, OATHWEAVE_FAILED_CHECK_TTL_MS, OATHWEAVE_MAX_LIFETIME_SEC, OATHWEAVE_MAX_TOKEN_LENGTH, OATHWEAVE_REQUEST_TIMEOUT_MS, OATHWEAVE_SESSION_CACHE_MAX, OATHWEAVE_SESSION_CACHE_TTL_MS, OATHWEAVE_STREAM_RECHECK_MS, } from './verifier.js';
6
- export { clientIdOf, extractRoles, extractScopes, principalFromClaims, principalKindOf, } from './claims.js';
5
+ export { createOathweaveVerifier, parseBearerToken, OATHWEAVE_DEFAULT_CLOCK_TOLERANCE_SEC, OATHWEAVE_FAILED_CHECK_TTL_MS, OATHWEAVE_MAX_LIFETIME_SEC, OATHWEAVE_MAX_TOKEN_LENGTH, OATHWEAVE_NATIVE_ISSUER_PATTERN, OATHWEAVE_REQUEST_TIMEOUT_MS, OATHWEAVE_SESSION_CACHE_MAX, OATHWEAVE_SESSION_CACHE_TTL_MS, OATHWEAVE_STREAM_RECHECK_MS, } from './verifier.js';
6
+ export { CLIENT_ID_CLAIMS, clientIdOf, extractRoles, extractScopes, principalFromClaims, principalKindOf, } from './claims.js';
7
7
  export { OathweaveError, isOathweaveError, statusForReason } from './errors.js';
8
8
  export { assertOathweaveEndpoint, endpointProblem, isLoopbackHost, isOathweaveEndpoint } from './endpoint.js';
9
9
  export { effectivePermissions, hasAnyRole, hasPermissions, hasScopes, IdentityLinkError, parseIdentityLinks, permissionMatches, requireIdentityLink, requirePermissions, uniqueIdentityLink, } from './links.js';
@@ -33,6 +33,8 @@ export interface ParseIdentityLinksOptions {
33
33
  }
34
34
  /**
35
35
  * Parse a JSON array of links, for example from `<PRODUCT>_OATHWEAVE_IDENTITIES_JSON`.
36
+ * An entry may name its local account `localUserId` or, as older configs do, `localId`; an
37
+ * entry that sets both to different values is refused. Parsed links always use `localUserId`.
36
38
  * Throws an `IdentityLinkError` naming the first problem; the raw value is never echoed.
37
39
  */
38
40
  export declare function parseIdentityLinks(raw: string | unknown, options?: ParseIdentityLinksOptions): OathweaveIdentityLink[];
@@ -2,7 +2,8 @@ import { OathweaveError } from './errors.js';
2
2
  export class IdentityLinkError extends Error {
3
3
  name = 'IdentityLinkError';
4
4
  }
5
- const LINK_FIELDS = new Set(['issuer', 'subject', 'localUserId', 'role', 'displayName', 'permissions']);
5
+ /** `localId` is accepted as an alias of `localUserId`, the name older deployment configs use. */
6
+ const LINK_FIELDS = new Set(['issuer', 'subject', 'localUserId', 'localId', 'role', 'displayName', 'permissions']);
6
7
  function isHttpsUrl(value) {
7
8
  if (typeof value !== 'string')
8
9
  return false;
@@ -15,6 +16,8 @@ function isHttpsUrl(value) {
15
16
  }
16
17
  /**
17
18
  * Parse a JSON array of links, for example from `<PRODUCT>_OATHWEAVE_IDENTITIES_JSON`.
19
+ * An entry may name its local account `localUserId` or, as older configs do, `localId`; an
20
+ * entry that sets both to different values is refused. Parsed links always use `localUserId`.
18
21
  * Throws an `IdentityLinkError` naming the first problem; the raw value is never echoed.
19
22
  */
20
23
  export function parseIdentityLinks(raw, options = {}) {
@@ -40,7 +43,11 @@ export function parseIdentityLinks(raw, options = {}) {
40
43
  for (const key of Object.keys(link))
41
44
  if (!LINK_FIELDS.has(key))
42
45
  throw new IdentityLinkError(`${where} has unknown field ${key.slice(0, 40)}`);
43
- const { issuer, subject, localUserId, role, displayName, permissions } = link;
46
+ const { issuer, subject, localId, role, displayName, permissions } = link;
47
+ if (link['localUserId'] !== undefined && localId !== undefined && link['localUserId'] !== localId) {
48
+ throw new IdentityLinkError(`${where}: localId and localUserId disagree`);
49
+ }
50
+ const localUserId = link['localUserId'] ?? localId;
44
51
  if (!isHttpsUrl(issuer))
45
52
  throw new IdentityLinkError(`${where}: issuer must be an https URL`);
46
53
  if (typeof subject !== 'string' || !subject || subject.length > 256)
@@ -1,5 +1,5 @@
1
1
  import { type JSONWebKeySet, type JWTVerifyGetKey } from 'jose';
2
- import { type AcceptPrincipal, type OathweavePrincipal, type OathweaveServicePrincipal, type OathweaveUserPrincipal } from './claims.js';
2
+ import { type AcceptPrincipal, type ClientIdClaim, type OathweaveLifetimeLimits, type OathweavePrincipal, type OathweaveServicePrincipal, type OathweaveUserPrincipal } from './claims.js';
3
3
  import { type EndpointOptions } from './endpoint.js';
4
4
  /** How long a userinfo answer is reused, measured from when its request started. */
5
5
  export declare const OATHWEAVE_SESSION_CACHE_TTL_MS = 10000;
@@ -14,9 +14,28 @@ export declare const OATHWEAVE_MAX_LIFETIME_SEC = 900;
14
14
  export declare const OATHWEAVE_DEFAULT_CLOCK_TOLERANCE_SEC = 30;
15
15
  /** Longest accepted compact token. Real Oathweave tokens are well under 4 KiB. */
16
16
  export declare const OATHWEAVE_MAX_TOKEN_LENGTH = 16384;
17
+ /**
18
+ * What a native issuer identifier must look like: a lowercase name such as `hive-iam`, never a
19
+ * URL, so it cannot be confused with an https issuer.
20
+ */
21
+ export declare const OATHWEAVE_NATIVE_ISSUER_PATTERN: RegExp;
17
22
  export interface OathweaveVerifierOptions extends EndpointOptions {
18
23
  /** Accepted issuer, or issuers. Each must be an https URL. */
19
24
  readonly issuer: string | readonly string[];
25
+ /**
26
+ * Oathweave's native issuer identifiers (its `JWT_ISSUER`, for example `hive-iam`) to accept
27
+ * as well. Oathweave signs client-credentials tokens with its native issuer unless the
28
+ * client's policy selects the browser issuer, and with the same keys, so these tokens are
29
+ * verified against the same `jwksUrl` or `keys`. Each must match
30
+ * `OATHWEAVE_NATIVE_ISSUER_PATTERN`. Defaults to none: a native issuer is never accepted
31
+ * unless it is listed here.
32
+ */
33
+ readonly nativeIssuers?: readonly string[];
34
+ /**
35
+ * Which principals a native issuer may sign for. Defaults to `service`: suite user tokens
36
+ * come from browser clients, which Oathweave always signs with the https issuer.
37
+ */
38
+ readonly nativeIssuerAccept?: AcceptPrincipal;
20
39
  /** This service's resource audience, for example `hivemem`. Matched exactly. */
21
40
  readonly audience: string;
22
41
  /** The issuer's JWKS endpoint. Required unless `keys` is given. */
@@ -32,8 +51,11 @@ export interface OathweaveVerifierOptions extends EndpointOptions {
32
51
  readonly accept?: AcceptPrincipal;
33
52
  /** Seconds of clock skew tolerated on `exp`, `nbf` and `iat`. Defaults to 30. */
34
53
  readonly clockToleranceSec?: number;
35
- /** Largest `exp - iat` and token age in seconds. Defaults to 900. */
36
- readonly maxLifetimeSec?: number;
54
+ /**
55
+ * Largest `exp - iat` and token age in seconds: one limit for every token, or one per
56
+ * principal kind (`{ user: 900, service: 300 }`). Each defaults to 900.
57
+ */
58
+ readonly maxLifetimeSec?: number | OathweaveLifetimeLimits;
37
59
  readonly fetch?: typeof fetch;
38
60
  /** Clock in epoch milliseconds. */
39
61
  readonly now?: () => number;
@@ -42,15 +64,32 @@ export interface OathweaveVerifierOptions extends EndpointOptions {
42
64
  readonly jwksCacheMaxAgeMs?: number;
43
65
  /** Minimum time between refetches triggered by an unknown `kid` (key rotation). Defaults to 30 s. */
44
66
  readonly jwksCooldownMs?: number;
67
+ /**
68
+ * How long a userinfo answer is reused, in milliseconds. Defaults to 10 000; 0 checks the
69
+ * session online on every call.
70
+ */
45
71
  readonly sessionCacheTtlMs?: number;
46
72
  readonly sessionCacheMax?: number;
73
+ /**
74
+ * Refuse a token without a `roles` array. Defaults to false, where an absent `roles` claim
75
+ * means no roles. A present claim that is not a string array is always refused.
76
+ */
77
+ readonly requireRoles?: boolean;
78
+ /**
79
+ * Which claim names supply the client ID: `clientId` (Oathweave), `client_id` (RFC 9068) or
80
+ * `either` (the default). When both claims are present they must agree in every mode.
81
+ */
82
+ readonly clientIdClaim?: ClientIdClaim;
47
83
  }
48
84
  export interface VerifyOptions {
49
85
  /** Override the verifier's default `accept` for this call. */
50
86
  readonly accept?: AcceptPrincipal;
51
87
  }
52
88
  export interface OathweaveVerifier {
89
+ /** The accepted https issuers. */
53
90
  readonly issuers: readonly string[];
91
+ /** The accepted native issuer identifiers; empty unless configured. */
92
+ readonly nativeIssuers: readonly string[];
54
93
  readonly audience: string;
55
94
  verify(token: string, options?: VerifyOptions): Promise<OathweavePrincipal>;
56
95
  verifyUser(token: string): Promise<OathweaveUserPrincipal>;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { createHash } from 'node:crypto';
9
9
  import { createLocalJWKSet, createRemoteJWKSet, customFetch, decodeProtectedHeader, errors as joseErrors, jwtVerify, } from 'jose';
10
- import { principalFromClaims, } from './claims.js';
10
+ import { CLIENT_ID_CLAIMS, principalFromClaims, } from './claims.js';
11
11
  import { assertOathweaveEndpoint } from './endpoint.js';
12
12
  import { OathweaveError } from './errors.js';
13
13
  /** How long a userinfo answer is reused, measured from when its request started. */
@@ -23,6 +23,11 @@ export const OATHWEAVE_MAX_LIFETIME_SEC = 900;
23
23
  export const OATHWEAVE_DEFAULT_CLOCK_TOLERANCE_SEC = 30;
24
24
  /** Longest accepted compact token. Real Oathweave tokens are well under 4 KiB. */
25
25
  export const OATHWEAVE_MAX_TOKEN_LENGTH = 16_384;
26
+ /**
27
+ * What a native issuer identifier must look like: a lowercase name such as `hive-iam`, never a
28
+ * URL, so it cannot be confused with an https issuer.
29
+ */
30
+ export const OATHWEAVE_NATIVE_ISSUER_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
26
31
  const BEARER = /^Bearer[ ]+([A-Za-z0-9\-._~+/]+=*)[ ]*$/i;
27
32
  /**
28
33
  * The token in an `Authorization: Bearer` header (RFC 6750). Undefined when there is no
@@ -91,6 +96,31 @@ function mapVerifyError(error) {
91
96
  // JOSEAlgNotAllowed cannot occur: the header precheck already refused every algorithm but RS256.
92
97
  return new OathweaveError('malformed_token');
93
98
  }
99
+ const ACCEPT_VALUES = ['user', 'service', 'any'];
100
+ /** Validate the configured native issuer identifiers. */
101
+ function nativeIssuerList(value) {
102
+ if (value === undefined)
103
+ return [];
104
+ if (!Array.isArray(value))
105
+ throw new TypeError('Oathweave nativeIssuers must be an array');
106
+ for (const issuer of value) {
107
+ if (typeof issuer !== 'string' || !OATHWEAVE_NATIVE_ISSUER_PATTERN.test(issuer)) {
108
+ throw new TypeError('Oathweave nativeIssuers entries must be lowercase identifiers such as hive-iam');
109
+ }
110
+ }
111
+ return [...new Set(value)];
112
+ }
113
+ /** Resolve `maxLifetimeSec` into one limit per principal kind. */
114
+ function lifetimeLimits(value) {
115
+ const limits = typeof value === 'object' && value !== null
116
+ ? { user: value.user ?? OATHWEAVE_MAX_LIFETIME_SEC, service: value.service ?? OATHWEAVE_MAX_LIFETIME_SEC }
117
+ : { user: value ?? OATHWEAVE_MAX_LIFETIME_SEC, service: value ?? OATHWEAVE_MAX_LIFETIME_SEC };
118
+ for (const limit of [limits.user, limits.service]) {
119
+ if (!(typeof limit === 'number' && limit > 0 && limit <= 86_400))
120
+ throw new TypeError('maxLifetimeSec must be 1-86400');
121
+ }
122
+ return limits;
123
+ }
94
124
  /** Create a verifier. Throws a TypeError at startup when the configuration is unsafe. */
95
125
  export function createOathweaveVerifier(options) {
96
126
  const issuers = typeof options.issuer === 'string' ? [options.issuer] : [...options.issuer];
@@ -98,6 +128,10 @@ export function createOathweaveVerifier(options) {
98
128
  throw new TypeError('At least one Oathweave issuer is required');
99
129
  for (const issuer of issuers)
100
130
  assertOathweaveEndpoint(issuer, 'issuer', options);
131
+ const nativeIssuers = nativeIssuerList(options.nativeIssuers);
132
+ const nativeIssuerAccept = options.nativeIssuerAccept ?? 'service';
133
+ if (!ACCEPT_VALUES.includes(nativeIssuerAccept))
134
+ throw new TypeError("nativeIssuerAccept must be 'user', 'service' or 'any'");
101
135
  if (options.jwksUrl !== undefined)
102
136
  assertOathweaveEndpoint(options.jwksUrl, 'jwksUrl', options);
103
137
  if (options.userinfoUrl !== undefined)
@@ -105,16 +139,21 @@ export function createOathweaveVerifier(options) {
105
139
  if (typeof options.audience !== 'string' || !options.audience)
106
140
  throw new TypeError('Oathweave audience is required');
107
141
  const clockTolerance = options.clockToleranceSec ?? OATHWEAVE_DEFAULT_CLOCK_TOLERANCE_SEC;
108
- const maxLifetimeSec = options.maxLifetimeSec ?? OATHWEAVE_MAX_LIFETIME_SEC;
142
+ const lifetimes = lifetimeLimits(options.maxLifetimeSec);
109
143
  if (!(clockTolerance >= 0 && clockTolerance <= 300))
110
144
  throw new TypeError('clockToleranceSec must be 0-300');
111
- if (!(maxLifetimeSec > 0 && maxLifetimeSec <= 86_400))
112
- throw new TypeError('maxLifetimeSec must be 1-86400');
145
+ const requireRoles = options.requireRoles ?? false;
146
+ const clientIdClaim = options.clientIdClaim ?? 'either';
147
+ if (typeof requireRoles !== 'boolean')
148
+ throw new TypeError('requireRoles must be a boolean');
149
+ if (!CLIENT_ID_CLAIMS.includes(clientIdClaim))
150
+ throw new TypeError("clientIdClaim must be 'clientId', 'client_id' or 'either'");
113
151
  const defaultAccept = options.accept ?? 'user';
114
152
  const timeoutMs = options.requestTimeoutMs ?? OATHWEAVE_REQUEST_TIMEOUT_MS;
115
153
  const request = options.fetch ?? ((input, init) => fetch(input, init));
116
154
  const now = options.now ?? Date.now;
117
155
  const keys = keySource(options, request, timeoutMs);
156
+ const acceptedIssuers = [...issuers, ...nativeIssuers];
118
157
  const sessionTtl = options.sessionCacheTtlMs ?? OATHWEAVE_SESSION_CACHE_TTL_MS;
119
158
  const sessionMax = options.sessionCacheMax ?? OATHWEAVE_SESSION_CACHE_MAX;
120
159
  // Holds only tokens whose signature verified, so garbage can neither fill it nor reach userinfo.
@@ -188,10 +227,11 @@ export function createOathweaveVerifier(options) {
188
227
  try {
189
228
  ({ payload } = await jwtVerify(token, keys, {
190
229
  algorithms: ['RS256'],
191
- issuer: issuers,
230
+ issuer: acceptedIssuers,
192
231
  audience: options.audience,
193
232
  requiredClaims: ['sub', 'iat', 'exp', 'jti'],
194
- maxTokenAge: maxLifetimeSec,
233
+ // The per-kind limit is checked on `exp - iat` below, which also bounds the age.
234
+ maxTokenAge: Math.max(lifetimes.user, lifetimes.service),
195
235
  clockTolerance,
196
236
  currentDate: new Date(now()),
197
237
  }));
@@ -202,7 +242,16 @@ export function createOathweaveVerifier(options) {
202
242
  // jose accepts an audience array that contains ours; Oathweave issues exactly one audience.
203
243
  if (payload.aud !== options.audience)
204
244
  throw new OathweaveError('invalid_audience');
205
- const principal = principalFromClaims(payload, { accept: verifyOptions.accept ?? defaultAccept, maxLifetimeSec });
245
+ const principal = principalFromClaims(payload, {
246
+ accept: verifyOptions.accept ?? defaultAccept,
247
+ maxLifetimeSec: lifetimes,
248
+ requireRoles,
249
+ clientIdClaim,
250
+ });
251
+ // A native issuer signs only the principals it was configured for.
252
+ if (nativeIssuers.includes(principal.issuer) && nativeIssuerAccept !== 'any' && nativeIssuerAccept !== principal.kind) {
253
+ throw new OathweaveError('invalid_issuer');
254
+ }
206
255
  if (principal.kind === 'user' && options.userinfoUrl) {
207
256
  const displayName = await sessionCheck(token, principal.subject);
208
257
  return { ...principal, displayName, sessionChecked: true };
@@ -211,6 +260,7 @@ export function createOathweaveVerifier(options) {
211
260
  }
212
261
  return {
213
262
  issuers,
263
+ nativeIssuers,
214
264
  audience: options.audience,
215
265
  verify,
216
266
  verifyUser: token => verify(token, { accept: 'user' }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hivemagnus/hive-common",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared server kit for the Hive:Magnus suite: Oathweave token verification, error envelope, health, request context, registry client, logging, events and framework adapters",
5
5
  "keywords": [
6
6
  "hive",
@@ -3,8 +3,8 @@
3
3
  `oathweave/vectors.json` holds a test JWKS and pre-signed Oathweave access tokens with the
4
4
  outcome every suite verifier must produce. `oathweave/jwks.json` is the same JWKS on its own.
5
5
 
6
- All tokens are synthetic: the issuer is `https://oathweave.example.test`, the audience is
7
- `conformance`, and the signing keys were discarded after generation. Evaluate them at the fixed
6
+ All tokens are synthetic: the issuer is `https://oathweave.example.test`, the native issuer
7
+ (`nativeIssuers`) is `conformance-iam`, the audience is `conformance`, and the signing keys were discarded after generation. Evaluate them at the fixed
8
8
  time in `now` (`2026-01-01T00:00:00Z`), because Oathweave tokens live at most fifteen minutes.
9
9
 
10
10
  | Vector | Expected |
@@ -21,7 +21,11 @@ time in `now` (`2026-01-01T00:00:00Z`), because Oathweave tokens live at most fi
21
21
  | `hs256-confusion` | 401 `algorithm_not_allowed` |
22
22
  | `lifetime-too-long` | 401 `invalid_lifetime` |
23
23
  | `user-without-session` | 401 `invalid_claims` |
24
+ | `valid-native-service` | accept, service principal signed with the native issuer |
25
+ | `unconfigured-native-issuer` | 401 `invalid_issuer` |
26
+ | `native-service-lifetime-too-long` | 401 `invalid_lifetime` |
24
27
 
25
- Run them with `runOathweaveConformance()` from `@hivemagnus/hive-common/oathweave/conformance`.
28
+ Configure the verifier under test with `nativeIssuers` from the file (the runner passes them as
29
+ `ctx.nativeIssuers`). Run them with `runOathweaveConformance()` from `@hivemagnus/hive-common/oathweave/conformance`.
26
30
  Regenerate with `npm run vectors:generate` in the HiveCommon repository; that replaces every
27
31
  token and key, so it is a changelog-worthy change.
@@ -2,7 +2,7 @@
2
2
  "keys": [
3
3
  {
4
4
  "kty": "RSA",
5
- "n": "golRKNCwa4EHgHIg8TdQPKbdQ2ZcEyA86q-Nnhlpv77c8ivnED12nAcQC29yuR7aUkLNH1FJHvbChiXndB_PqDk_AtF_fmm0XPhdKthO6GPZOjLEX5LeWzFilQ4EmgieFIOAa-4LvWZfuvqsX2jXkbABiDFnJKwQ2hfegLTGkmqRQfEfBIZZcTlGI3tVjLV2B8Fh1cutNyUpCAtTFMUBUleioFtA9BzuIl3pBJ4Q3aBMxDvkAcaV7o9FpSz5Z1bfXUaNxi_k3jfdZh8OYStAQD0SPNWjt_Wannm9N_cSVl34jMmnFBUtyiAOHf8jVZeYtcVF75RtUJRg-QopglyIow",
5
+ "n": "yqMr7bkXQ5W1vFW-cIyvEdkdZNAdJ6S3JsmE88aLNyNTDSM8cPskt-1ZntDBFqVc2SyhMtpPHr6zQHo8EkG6VU8vmg0u-X7whM63Pnj3xM-NNJlMCyUfH66imW2N-rITu-cyCOMmWN6WprFzhsXAR4bTw8qnuVfBHQ19fKwRhOHZuTFEQbK_OUj0k_GgzOWVyaoRnCW3_AnesQuBwp68f2JEwllYRYX2vv75Q9ckl8pstGS3E1AAtgPrGp7nAxvbmQHoMlS1xYppZuWjoUttb7qVttduuMESpFva5oDqol5xKe9J0_dP9ZPEWInpzV7LfWgkry21asEIwojglTFgaQ",
6
6
  "e": "AQAB",
7
7
  "kid": "conformance-1",
8
8
  "alg": "RS256",
@@ -2,6 +2,9 @@
2
2
  "schema": 1,
3
3
  "description": "Oathweave access-token conformance vectors. Synthetic: the signing keys were discarded after generation.",
4
4
  "issuer": "https://oathweave.example.test",
5
+ "nativeIssuers": [
6
+ "conformance-iam"
7
+ ],
5
8
  "audience": "conformance",
6
9
  "jwksUrl": "https://oathweave.example.test/jwks",
7
10
  "userinfoUrl": "https://oathweave.example.test/userinfo",
@@ -10,7 +13,7 @@
10
13
  "keys": [
11
14
  {
12
15
  "kty": "RSA",
13
- "n": "golRKNCwa4EHgHIg8TdQPKbdQ2ZcEyA86q-Nnhlpv77c8ivnED12nAcQC29yuR7aUkLNH1FJHvbChiXndB_PqDk_AtF_fmm0XPhdKthO6GPZOjLEX5LeWzFilQ4EmgieFIOAa-4LvWZfuvqsX2jXkbABiDFnJKwQ2hfegLTGkmqRQfEfBIZZcTlGI3tVjLV2B8Fh1cutNyUpCAtTFMUBUleioFtA9BzuIl3pBJ4Q3aBMxDvkAcaV7o9FpSz5Z1bfXUaNxi_k3jfdZh8OYStAQD0SPNWjt_Wannm9N_cSVl34jMmnFBUtyiAOHf8jVZeYtcVF75RtUJRg-QopglyIow",
16
+ "n": "yqMr7bkXQ5W1vFW-cIyvEdkdZNAdJ6S3JsmE88aLNyNTDSM8cPskt-1ZntDBFqVc2SyhMtpPHr6zQHo8EkG6VU8vmg0u-X7whM63Pnj3xM-NNJlMCyUfH66imW2N-rITu-cyCOMmWN6WprFzhsXAR4bTw8qnuVfBHQ19fKwRhOHZuTFEQbK_OUj0k_GgzOWVyaoRnCW3_AnesQuBwp68f2JEwllYRYX2vv75Q9ckl8pstGS3E1AAtgPrGp7nAxvbmQHoMlS1xYppZuWjoUttb7qVttduuMESpFva5oDqol5xKe9J0_dP9ZPEWInpzV7LfWgkry21asEIwojglTFgaQ",
14
17
  "e": "AQAB",
15
18
  "kid": "conformance-1",
16
19
  "alg": "RS256",
@@ -23,7 +26,7 @@
23
26
  "name": "valid-user",
24
27
  "description": "A user access token with a session, signed by the trusted key.",
25
28
  "principal": "user",
26
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwfQ.H_M25DpT8q8xWyVFydzNrBQd6m1j1M_ziDq8yiLem2jgDoG2eetf_gGl0eUSwP-RuRlcykPvHGou1XZ3MIbbzaOP4AqxOr4aTl3HaxpXbYudYMG89ty5fcJKsFnT15zy61VjV0ZNkj734DrXDo04XbFnLuhi300ojsJpYJkWi6v-o3B-_DFVygHk_5ZNHA6hboIOCgv8ZO-kOWcHrw8Zr_VpwSmKxpj-5B8uLxXsEDb_WqGdHyXAFFt_dDjQb1dw9ssUtFhxRLE0kI2LXq6mjy_P76Z7bk_S1sSFGvMAWL_mh8QNzHspJ3CBhJmJ93JimimZu7EUpJb1vH8wIvmSVg",
29
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwfQ.h_xGzySHyRl_bLLqz7bRQfI6FXeeoFGX7WVhauLK9LUZG0tn5mtc89p6muVppqStzCpo9NtYJC-ZD1UpF_SU3CAUC7vBU8yRkVPHHGVR26nI5n29jjr1zocAHsKg0Aia7k8hWBRDdxkfxNC6clj0fzLRR2BHNlM0m4UrE29nyQItI5hYt49ngLl74ieFStYYCo1QxJR4_x0AjDcAOUtT-hATSalaBjuS9ASrGoHjhUmal-teXcUbzXV5LXQseY1IspAtx3ujIliFp4qVhCgD2DjGf0j-0vy6tD2NBNSZL6BRJfU8ODhVy9jw1drHj9HncPPgYbr9TT96qaFRklFMKw",
27
30
  "expect": {
28
31
  "outcome": "accept",
29
32
  "kind": "user",
@@ -45,7 +48,7 @@
45
48
  "name": "valid-service",
46
49
  "description": "A client-credentials token whose client ID equals its subject.",
47
50
  "principal": "service",
48
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJTRVJWSUNFX0FDQ09VTlQiLCJjbGllbnRJZCI6ImNvbmZvcm1hbmNlLXNlcnZpY2UiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwicm9sZXMiOltdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTp3cml0ZSJdLCJzY29wZSI6ImNvbmZvcm1hbmNlOndyaXRlIiwiaXNzIjoiaHR0cHM6Ly9vYXRod2VhdmUuZXhhbXBsZS50ZXN0IiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS1zZXJ2aWNlIiwianRpIjoiY29uZm9ybWFuY2Utc2VydmljZS0xNzY3MjI1NTcwLTE3NjcyMjU4NzAiLCJpYXQiOjE3NjcyMjU1NzAsImV4cCI6MTc2NzIyNTg3MH0.GX3VfFVnjLp2mPZQn3nI8FkLUT52ZAZoV5QWfSpjGqycR7FRgvaK3ba0RUimrTKrK_2-DZAQOLKJ-TGPjsJDJnyCNbUY-0V63QkSSZ15nKyDWpE9M8ubjjNM5cblP4GpyUog6rdhZryShHp9Q6eyjFMOUDlr2eVcxIFF3kuIMn_rfrO8rAqyk8jHu5Qpld0lls3hQIxEIf4c3WZOviVFkUEdqYmhNwwDkCM7uVSaFKv_Sh48KYWIJVPQH46FjiGOXiDxfpCwEn8ox2ZUICL0YbM3k2XB7FI3V32Ao6D9brVQ5GzDINI6yXsZJ4ylbaVHbEbXgvtnHqojkrzfAZ7FXg",
51
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJTRVJWSUNFX0FDQ09VTlQiLCJjbGllbnRJZCI6ImNvbmZvcm1hbmNlLXNlcnZpY2UiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwicm9sZXMiOltdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTp3cml0ZSJdLCJzY29wZSI6ImNvbmZvcm1hbmNlOndyaXRlIiwiaXNzIjoiaHR0cHM6Ly9vYXRod2VhdmUuZXhhbXBsZS50ZXN0IiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS1zZXJ2aWNlIiwianRpIjoiY29uZm9ybWFuY2Utc2VydmljZS0xNzY3MjI1NTcwLTE3NjcyMjU4NzAiLCJpYXQiOjE3NjcyMjU1NzAsImV4cCI6MTc2NzIyNTg3MH0.OX7smVqC33kIdki0cnQvYIS3TTV2Hn9Pwdgh2jBJ4SHLq_ns3StMOqaDt9uz6QB7f52JM08a-e_JitdhbqPy6J7W2JIixaqV9mSC1_T8PEKRNdJ6TbkJ_zdnw4wG20VyFBOQqXz3uL8Y9hi4rk_qCmG3iXnRBTmTFw2Ra0J9ndS4Gd6oQH8VNkjBQzS6En-yNQpOEN0kn-ssYeidPJmvOkZ2Kglp4s8sri3OtF19clwcChrbChONJSITLpg6Ee3PBJIiye_h2HONCGeYbnEkKAuqUsnkmwR37rS-Eb58nOeR6A2tornF0iX1dXUShC-siuhalYOP03ijrsLh4Th0qw",
49
52
  "expect": {
50
53
  "outcome": "accept",
51
54
  "kind": "service",
@@ -64,7 +67,7 @@
64
67
  "name": "expired",
65
68
  "description": "Expired fifteen minutes before the evaluation time.",
66
69
  "principal": "user",
67
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjM4MDAtMTc2NzIyNDcwMCIsImlhdCI6MTc2NzIyMzgwMCwiZXhwIjoxNzY3MjI0NzAwfQ.J3EHtT1KeCqs6XoHbGMC6cenCD8VW8YPHNKR2ipOlu7z1g5zNsF5M1MINm3ATEplsbNs8cC1Kaw4hbXwn0VfvRqv8jSfOhmG3kW70I9X1GYZjLPIzcvR_cQZ271_KcfcSZujnoHu5EuZBblyNB8MAEQhhfjV_KTfmtz1cDoR2ZpULYWzuqdwyeyI-9Aw_kFk_ueg-E45b1dhkx9mGUSO2cOUa43DZzVtzHqkhwHfxeSTKTxYk3oICaY_329q25cUh4w2ss7ZQQgYIV1fhY5COBQYQ61TuePgHhiGTuBuUkWIfJ0Uw2i3ZzmqrBkpMYURctE1cZ9gLAXUMu0YiZN7_g",
70
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjM4MDAtMTc2NzIyNDcwMCIsImlhdCI6MTc2NzIyMzgwMCwiZXhwIjoxNzY3MjI0NzAwfQ.vRA51HlCDqr-pfDhUKoDIO50zsykGJM-ob1umb5ByYj2uDOLPl800zyu9v5PJrGPDEQMGsxKgsEDTL5vVFKTDMGQyqYVs3JUnrJhVaIV9C8Ua_e1IZqa2V7qa-sKjiXDgLnMW0s-DQGNvoL2QKf1N9kevht-UtdfW7V9OGnSloo2wJyTN1a4j9qslV5l5PK0hXV_IWcI4oQINFhOUvI5Kt7NYUkVuOqotd-TcKNYcjwmI8v-w_2CP6fG86EP07cipUBHadpxjrA6IT8r21UAa8-Oqh5FnUE6-GY1sUZRl49fNDWrY2UDZqdYnOaGSun3cJuX1sLghStSwTXU6Gm0lA",
68
71
  "expect": {
69
72
  "outcome": "reject",
70
73
  "status": 401,
@@ -75,7 +78,7 @@
75
78
  "name": "not-yet-valid",
76
79
  "description": "nbf is ten minutes after the evaluation time, beyond any clock tolerance.",
77
80
  "principal": "user",
78
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwLCJuYmYiOjE3NjcyMjYyMDB9.IrT2Yb6p46ok6yxC33vXiQSz1YlkYr9v4tOuYyOXjaP6VLDojFzp9ePUVSpRJ3KGseYvx5wQqJEC89EVebuBGbPX4prXSFVeiJsUk9ugIX272l3_fPKhvoo8-U0hIOefqy-yE4wtr99pC3o5pVTNkRVBeLf3Dj9qK_qJuUIpkTsyq4lXMQCr5nODmrND5NHFd1RvpSgRIbmUyuS02fWW67v_nrfacRnawKCYkheBoa9Zdq-9yHiW_aeclYhXUDFE35VPTtFIQH9sODWDwGaeHN6PxsqAuaNgKK3S4Oi9W9hN6N9hKf-XctefpSMDmR8qz3NW3ICcDmEy64Zv-kpltw",
81
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwLCJuYmYiOjE3NjcyMjYyMDB9.VqgN6KtelOkvYszNnlXsZmpHVx_TnuiEBt6e9Lqb2YvPiZvZvYZLS8VMXkujQtV9dQK2q3rEOWK7WpzwsW7ejdCQVX6aixsW5W4PdsG0xPUL4NO5_Sw5f9Uk2wNccEb5fTLqm2_3As-PXe59AXtX_o7JLLsZAkqr0lycsaysp9bO53KshB04WDHWpY5jL-fmDuKPurMD3XPMQ4oe-TFvpqAMlOMLeaH3JgdT6Xiv97NtYAfWU_3vzp0O-GLxxTirRlwL9nMkXZ1lj5Bboyp4rOlwu6OBxNEo69LEgMm7Cmify2KpDotPxnqWJXCW6knwROlInIefqnkRAopF1QhkUQ",
79
82
  "expect": {
80
83
  "outcome": "reject",
81
84
  "status": 401,
@@ -86,7 +89,7 @@
86
89
  "name": "wrong-audience",
87
90
  "description": "Issued for another product.",
88
91
  "principal": "user",
89
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImFub3RoZXItcHJvZHVjdCIsInN1YiI6ImNvbmZvcm1hbmNlLXVzZXItc3ViamVjdCIsImp0aSI6ImNvbmZvcm1hbmNlLXVzZXItc3ViamVjdC0xNzY3MjI1NTQwLTE3NjcyMjY0NDAiLCJpYXQiOjE3NjcyMjU1NDAsImV4cCI6MTc2NzIyNjQ0MH0.eeXV-yXckuctyJcs7zMEu--CzOJ5jUBtGIaqZQmF8Q3qAZM9hibaQJB3Ew0gLVTQD1PIFOUM5JIv0OwUJWW7FuaEijhynTpBK037yTe-y5Hvo1btDlvGgQ7QdotS39WqLPdIorI2IndzPSq0j9CiiG9i7TRYaPUaep0dWgM4uBu-p7Tdg4n3hMFeXdaGtd9AzsMoMh-4kJx0W7jKF7YiHEibkCu1-hXmlNdylDiJ-OdaYmhPJu1HDoBs9D0KcLlJyqsvCV4E_3pK3eGLyGwRQ54v5PR5J-kHcXAXoIe-iZlYdPpfX5DxRtOAklHeRyNmNy8csGAcGFgJP7NkQ_VQnw",
92
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImFub3RoZXItcHJvZHVjdCIsInN1YiI6ImNvbmZvcm1hbmNlLXVzZXItc3ViamVjdCIsImp0aSI6ImNvbmZvcm1hbmNlLXVzZXItc3ViamVjdC0xNzY3MjI1NTQwLTE3NjcyMjY0NDAiLCJpYXQiOjE3NjcyMjU1NDAsImV4cCI6MTc2NzIyNjQ0MH0.XXFWK7gQfhV-zF0GhBm3RrgqOeQyQIpVfCXLfle2EERERF5DvLIEg4YZo6YoVR7oo-NfS9AAr4bQOom-WEGY_vrqA678LbS3626K2jE2WiPSvgj1wJnhrDPT0NoSXF05zr5FhhnVbOGF9-SCcv6hSm8K8nyftdLBG8GykqUPhwmiUDlAFJXwcLdI5vIEPKQbZLqijq-JlMngcbPq54CzqkB6xw1Fi1N791zw7uXRkwlzji55Ld6wUHpqU-lk0pLF6iKzM5xkhuCXgTS7KXK0HUaIYIGxzldCzfVNq3SVkj8dRd6UF0v7CnJxGXzNyZm9cAiwwJ1J46Nl3dsn2XnXWA",
90
93
  "expect": {
91
94
  "outcome": "reject",
92
95
  "status": 401,
@@ -97,7 +100,7 @@
97
100
  "name": "wrong-issuer",
98
101
  "description": "Signed by the trusted key but naming another issuer.",
99
102
  "principal": "user",
100
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vaW1wb3N0b3IuZXhhbXBsZS50ZXN0IiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QiLCJqdGkiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QtMTc2NzIyNTU0MC0xNzY3MjI2NDQwIiwiaWF0IjoxNzY3MjI1NTQwLCJleHAiOjE3NjcyMjY0NDB9.ZpN1PfxxUKc3dszqk_eciJKkvvwirMRF9SfKKOHH8_bLeq8mS6XpmZDPsq70u3mI0OSw1Cuzmfbr2U78Bi6Kt49VBHVo5M5K3LxmCNJkLhUddB1bpxFP1fBQbBDrqUzLntaDL30HmEWwdLagVhsw-MviWBk7C_viMh1WdgXMv_0-XEYsA76juNjrz4PU5mlpPW8zS9hIuwl7EYNlZa8kB8ok24yfPksZtE1hg8YUVrMi2i2RkgnKG-CW_WBh59sVBlouY4csr8IMHn3zV3kwdHCBAIKeiNh-z3P3asarMRPUDFUwL636ihvXyZRony6jJUjCOegYcvB9dSH6ZEQlMQ",
103
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vaW1wb3N0b3IuZXhhbXBsZS50ZXN0IiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QiLCJqdGkiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QtMTc2NzIyNTU0MC0xNzY3MjI2NDQwIiwiaWF0IjoxNzY3MjI1NTQwLCJleHAiOjE3NjcyMjY0NDB9.XJq-_kga-yWW33-wyjYjevshWgeLOgMK2QuHMQncUV5-FGE8qEZZDXpGRYi_qd40EdlzxMdZJLFXqf38Ub6pZZoo9lKmLN-zfwdLZgiujoMgruTdJzkk0JUe6_kbtVbiQIFepm7VfLmnhFeN4o0p9npmXINE3FDDMMXKBrI8APkJA1JX_zOIdamvNRZqMOF_r6lJLr2flIaPM1zAsP45AdUgNY0NHiK4jZqO5rNCMJgvwPDpBuU78zNzW5ZH5iYbdKvG_QE_AVROg9Enxg4cfH2zbrxl377AfqwbqAz1f65AaWgPx5rG_1-OWcoA8FRA2p38-Z4ZBXohlk7vERd26A",
101
104
  "expect": {
102
105
  "outcome": "reject",
103
106
  "status": 401,
@@ -108,7 +111,7 @@
108
111
  "name": "bad-signature",
109
112
  "description": "Names the trusted key ID but is signed by another key.",
110
113
  "principal": "user",
111
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwfQ.N6ySuCzMIpFUbnd-Or30sjD_E98oAQFEJlel7thmoyEWhFuiIVmwyU24bKs148r8Ko9sPQ7HKPP7lTO1G0q7IKGEjn5Ds4LkE97T0VAnViYp_5PHZ7BgvYKtFoawJZc5RQKFlXwkSUb50BjFJ_iotnroPsBYzfy_oDVcVoctizQSvCydknDzNBI-R7WTJHdK73XlUyjwJiIxfsgDdNVnm4VagQfJraQcVJGeCIrWzFz_YQZ0mWQOwMLRlwaaWBOCJ_ClaU9qTU0Vhy4AMKW2_0p61u8rN_mtA_qWSEiZYMIVYgDxYvgTO4OwVuUi_un-tA_2k8v_GikXIuyOK6ohDg",
114
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwfQ.PonwzwBFqrqWQitpXSoAaTbqsRaPS-XfF-QJkIsZG2vRJVkoJaoL6LSqEMvZs0lbGjFfqliLIJgJXsWiheGzz922AQvbsOIAeJbiGuec9yZ5E8qe2NTsdutUMZkSnC1XMhw4n7n1EihjqJXQhTxqwmuFHRKYPjRFfObIX8VMlKw-cEP0DtZZrjWBO1YXaOwg5NgzgQf4z6ZMhfAkAngPHHAcjx50Fwkje79YxfTlXFMwSbcTrBJm4Dfd81mQFpFN3mEUkJyNcGM-Nypk1HGcxSO7hiq7qI7_wohxk05EcW47Lb4sj5hxgnEoxKD2otOo5M5Tv7Ao14YOjLj2Hk-r2w",
112
115
  "expect": {
113
116
  "outcome": "reject",
114
117
  "status": 401,
@@ -119,7 +122,7 @@
119
122
  "name": "unknown-kid",
120
123
  "description": "Signed by a key the JWKS does not contain.",
121
124
  "principal": "user",
122
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLXVua25vd24iLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwfQ.PRXTFRE263S0jKbuv9l_As6iEEPFib-_d-mXNeQ5dffjbCCt7JCzT8YZyxzS_IgpNwTB1Te6oDa69TcrgGjTnh7nCUye0_tN64ZoEANPCNgS16LHWaNemW88r-Nzi-2dBtWaSHMpHMFr43B5JcIwsR9jCksguIk8oclt3i1yNIYB1dR_I5YbsWnwy26lNYgo0rlgIbkpkL35VZwejYfuHe3YYozJNbLTvwRUrAbzruNRC_ZoeRnmwAyMaBLx4SVmZSJnxzcuTiy7saArv0vedV34pst4-a9DVvwpeZy_EKHHVFlTnzLVdGVr2ZDly3Fc5uHWDApsfq_LVxuq0vE-DA",
125
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLXVua25vd24iLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyNjQ0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI2NDQwfQ.Gc9towTbaDvWaUZ54DPl4Qil36n0ajn0Ql6-uopacIHucHhD5zfQAXHNvwI6BAppRegDWewR1n764TM4W-UESIWxrReLNlEdPi_vSlWOm4ft-z5IzfGX9y-INDEUsRq2L4Ps2lRz4HPQUIW57239c_nl9UOeYHzbrg4jswbRg_Ld2d2Qc5PhUTYKIzsNCeV3UtQQQFbASnfGNEGUe-9OodPwtACmDoMfm_ZqMTZxCWEnwPdH1KeL3i7oGV2rGmRLQj3458RBY6xrGLx1CIQ1iS73jKsxuMxBWtzCOkZOPfsYo4nDAUzdyGwBXNwuP4PjDJy3W8iVyA6_HNuFRr4cPw",
123
126
  "expect": {
124
127
  "outcome": "reject",
125
128
  "status": 401,
@@ -141,7 +144,7 @@
141
144
  "name": "hs256-confusion",
142
145
  "description": "HS256, keyed with the trusted public key, to exploit key-type confusion.",
143
146
  "principal": "user",
144
- "token": "eyJhbGciOiJIUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiZm9yZ2VkIiwiaWF0IjoxNzY3MjI1NTQwLCJleHAiOjE3NjcyMjY0NDB9.X3tuRAouszRyCO7IV3TpgE-80FuN1XfsH3bfYRp9r6s",
147
+ "token": "eyJhbGciOiJIUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiZm9yZ2VkIiwiaWF0IjoxNzY3MjI1NTQwLCJleHAiOjE3NjcyMjY0NDB9.khNhRMAyBq4h1BGtn5ElcDfoPkmDMA4eFYTXeR3UMk4",
145
148
  "expect": {
146
149
  "outcome": "reject",
147
150
  "status": 401,
@@ -152,7 +155,7 @@
152
155
  "name": "lifetime-too-long",
153
156
  "description": "A one-hour token; Oathweave access tokens live at most fifteen minutes.",
154
157
  "principal": "user",
155
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyOTE0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI5MTQwfQ.Txf8bZffFR4740gNpE5gb7wNAoQLwC6lrDSN57-ZeqpbdzYykF3-FtkBbRBHsGvOnUysoK7rCkatUki4Yb3YASncBHFYrpQJ10j6BpAZMFevE4GM_b021cWC5vMVLur9foh0j5F4KcvuBPeaFWvGw_NlhVA8oyVV4xkm2MPcUT5e4G8ZvKI2trBDR1RZSgJqv8WgqXUW7_hTCjXs1eazV_rqCJ5WeEaLmynQASVqSwaOIjiIZQC_UWp_0RLkLcEoTFkhKCfYZGrtuCA2yGqPbtoPK_mzzedWv-ww1dP39J4CTA2K4v5w-7kcPNrRO8SvSrVo42FVkD-FwaI-Ybkz5g",
158
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwic2Vzc2lvbklkIjoiY29uZm9ybWFuY2Utc2Vzc2lvbiIsInVzZXJuYW1lIjoiY29uZm9ybWFuY2UtdXNlciIsInJvbGVzIjpbIlZJRVdFUiJdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTpyZWFkIl0sInNjb3BlIjoib3BlbmlkIGNvbmZvcm1hbmNlOnJlYWQiLCJhdXRoX3RpbWUiOjE3NjcyMjU0ODAsImlzcyI6Imh0dHBzOi8vb2F0aHdlYXZlLmV4YW1wbGUudGVzdCIsImF1ZCI6ImNvbmZvcm1hbmNlIiwic3ViIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0IiwianRpIjoiY29uZm9ybWFuY2UtdXNlci1zdWJqZWN0LTE3NjcyMjU1NDAtMTc2NzIyOTE0MCIsImlhdCI6MTc2NzIyNTU0MCwiZXhwIjoxNzY3MjI5MTQwfQ.Bse6FyPkiHXCRdtFFuMKxUpCoSVRp0-WrKTaD3FFo8bKcewHe67hyFKpXHJNulnrtExvDzRD1V0tJKhTQPnyzxGp-GarlwPKIrQuu6aMOOTkSdFdVzgZSCK8yhvL5xZPbtkD8EiX5o7t7aWKZ4XadvAWL0B-FKXTaPCOTIMhNMbi3OX3l0xgiRWZcfPiSLgJQ2ExAzuYYyh-ick6fqY9raLjuDnp4Kms3X2VxYvPzoiV5h8HfPsa_sPJDnlYOx4n1UKWpIyaN1tR0wuKo_1pBGnOLwpN4_yTgbMWo3vLI-99ONmDEQc7iVYyOGjdKWqT0cNPhee0XcpMGfzty4DRmg",
156
159
  "expect": {
157
160
  "outcome": "reject",
158
161
  "status": 401,
@@ -163,12 +166,53 @@
163
166
  "name": "user-without-session",
164
167
  "description": "A USER token with no session cannot be revoked and is refused.",
165
168
  "principal": "user",
166
- "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwidXNlcm5hbWUiOiJjb25mb3JtYW5jZS11c2VyIiwicm9sZXMiOlsiVklFV0VSIl0sInBlcm1pc3Npb25zIjpbImNvbmZvcm1hbmNlOnJlYWQiXSwic2NvcGUiOiJvcGVuaWQgY29uZm9ybWFuY2U6cmVhZCIsImF1dGhfdGltZSI6MTc2NzIyNTQ4MCwiaXNzIjoiaHR0cHM6Ly9vYXRod2VhdmUuZXhhbXBsZS50ZXN0IiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QiLCJqdGkiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QtMTc2NzIyNTU0MC0xNzY3MjI2NDQwIiwiaWF0IjoxNzY3MjI1NTQwLCJleHAiOjE3NjcyMjY0NDB9.Gy_tAGnksWVv8bf_k4hRoA1bSr_3r6Q6wdEa-q-4ZQAGX5L10O-o_uZHhW-DFjS7Z9LDWNarrHH04MW1kkC2DUX5hylt-JX72vHJXHDJH4XedJC8yZ5y2yDz2qgss68iSx-4y4cmTddI1Vzra3lJ7F_W9BG9znvGP-u8IfxXxu1iIBjt7oNnPbYw2xQOETFst7_exu0XbfY8t1F86x5N2a5iyOSO4_ocvj6wwXwGSKBlnLZ9he12CiRraIpOrUkJlzW3Sia_KCsBBwrrBIHDBm6WJRl8SlfbLScQJlJxA5n6riGtjPqmN44d0H2z_rj8RsyHB15ocNMUsUGapiIUHA",
169
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJVU0VSIiwiY2xpZW50SWQiOiJoaXZleHVpIiwidXNlcm5hbWUiOiJjb25mb3JtYW5jZS11c2VyIiwicm9sZXMiOlsiVklFV0VSIl0sInBlcm1pc3Npb25zIjpbImNvbmZvcm1hbmNlOnJlYWQiXSwic2NvcGUiOiJvcGVuaWQgY29uZm9ybWFuY2U6cmVhZCIsImF1dGhfdGltZSI6MTc2NzIyNTQ4MCwiaXNzIjoiaHR0cHM6Ly9vYXRod2VhdmUuZXhhbXBsZS50ZXN0IiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QiLCJqdGkiOiJjb25mb3JtYW5jZS11c2VyLXN1YmplY3QtMTc2NzIyNTU0MC0xNzY3MjI2NDQwIiwiaWF0IjoxNzY3MjI1NTQwLCJleHAiOjE3NjcyMjY0NDB9.QoN7my4VGDehZNulCrZCXdq4d01trKuSzKYofoZdzsDOlP5tjCHPxlnKVDmvIh6NXahlV3Yxauj4w6_Q_7WPytXTvJqAHF7VlqZihvMk7Qt0YPD12vjkMHC7yJSRqOiCilvAhH2nzYV6gwbHUCx_dXLhsrH3X5mWFinllLj_yrvqR7n0IXLB8oUzBNii6tcDwVXSsAtyIm0pd876XjvSfOQcAZ2kHznUtRgIybX_w3kWTnfGAEdF6qE9ksWY6RNr063pv__5UFfXDOQdMaqxpO6TA6Vonoq0e0X0vW8DIWKyxAt_rZCE1ju_pX2wP29Lt5Ja6wMRTZWs6K2yRoy8-A",
167
170
  "expect": {
168
171
  "outcome": "reject",
169
172
  "status": 401,
170
173
  "reason": "invalid_claims"
171
174
  }
175
+ },
176
+ {
177
+ "name": "valid-native-service",
178
+ "description": "A client-credentials token signed with the configured native issuer and the same keys.",
179
+ "principal": "service",
180
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJTRVJWSUNFX0FDQ09VTlQiLCJjbGllbnRJZCI6ImNvbmZvcm1hbmNlLXNlcnZpY2UiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwicm9sZXMiOltdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTp3cml0ZSJdLCJzY29wZSI6ImNvbmZvcm1hbmNlOndyaXRlIiwiaXNzIjoiY29uZm9ybWFuY2UtaWFtIiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS1zZXJ2aWNlIiwianRpIjoiY29uZm9ybWFuY2Utc2VydmljZS0xNzY3MjI1NTcwLTE3NjcyMjU4NzAiLCJpYXQiOjE3NjcyMjU1NzAsImV4cCI6MTc2NzIyNTg3MH0.tzDv8M9EYauZzS1hVvgoW5miWyRdnlatpGh6QfIxvxn2b8qG90FMQmLdPZffcXiD5T4gAq0JGO_LGiaPeY8rp7Jq_mz4RZXIAvVvQ_2hjC_o7nh8mu6KWtbqAfK1UB6ZerMo3w_9T6Dcll1-irNzlSTGvjItY1dJJHMvq-y_rKY5c_GpWOEy6BIkqAZXbl5D2If_zZIJgonJ0S65fVbG-rHUxHzlL7G4UrDO6KplCqVJbU_wBhmS7Tcgrpb2RX_tUGXZkr1B5xkSnJ-gAlm5-1YEOw9V64wK94fRn6V2hdHP6xp3fQs444GRpnhl9YytlW5bwzV9Il7h5lGvoW_Tgg",
181
+ "expect": {
182
+ "outcome": "accept",
183
+ "kind": "service",
184
+ "subject": "conformance-service",
185
+ "clientId": "conformance-service",
186
+ "roles": [],
187
+ "permissions": [
188
+ "conformance:write"
189
+ ],
190
+ "scopes": [
191
+ "conformance:write"
192
+ ]
193
+ }
194
+ },
195
+ {
196
+ "name": "unconfigured-native-issuer",
197
+ "description": "A client-credentials token from a native issuer that is not configured.",
198
+ "principal": "service",
199
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJTRVJWSUNFX0FDQ09VTlQiLCJjbGllbnRJZCI6ImNvbmZvcm1hbmNlLXNlcnZpY2UiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwicm9sZXMiOltdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTp3cml0ZSJdLCJzY29wZSI6ImNvbmZvcm1hbmNlOndyaXRlIiwiaXNzIjoiaW1wb3N0b3ItaWFtIiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS1zZXJ2aWNlIiwianRpIjoiY29uZm9ybWFuY2Utc2VydmljZS0xNzY3MjI1NTcwLTE3NjcyMjU4NzAiLCJpYXQiOjE3NjcyMjU1NzAsImV4cCI6MTc2NzIyNTg3MH0.au69pXP0GSRHLRlcE3mF3_pquzd7OK2_2xkfZNJPmvBzdkgDG2-xTk3oInpgYdEqBS2gHyi9P2ZbkEM3UhhD0sgDNudQYZQsYSzaptX17oQYEJAk_3fnPL9WMUlmEqt0ifpOaXmIkSq-gPu9G3GXP1VMtZPpueuMBRshQQncj7bs5amNTck-48MaR8IY7LY95ghzkczgVOdO783lqjPu0u1tvYKwLe7MP8I8DxKzYwEhn7aHYlL_f6a1i6QAmrq_D55_M39CEff-AZC6koqo7SlaeQcJpj6dJSpZIDwbbHUdlzCmpzwWDLlEqnJOEoRdw_teSP0223Xd-LwNkJfSnw",
200
+ "expect": {
201
+ "outcome": "reject",
202
+ "status": 401,
203
+ "reason": "invalid_issuer"
204
+ }
205
+ },
206
+ {
207
+ "name": "native-service-lifetime-too-long",
208
+ "description": "A one-hour client-credentials token from the native issuer.",
209
+ "principal": "service",
210
+ "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNvbmZvcm1hbmNlLTEiLCJ0eXAiOiJKV1QifQ.eyJhY3RvclR5cGUiOiJTRVJWSUNFX0FDQ09VTlQiLCJjbGllbnRJZCI6ImNvbmZvcm1hbmNlLXNlcnZpY2UiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwicm9sZXMiOltdLCJwZXJtaXNzaW9ucyI6WyJjb25mb3JtYW5jZTp3cml0ZSJdLCJzY29wZSI6ImNvbmZvcm1hbmNlOndyaXRlIiwiaXNzIjoiY29uZm9ybWFuY2UtaWFtIiwiYXVkIjoiY29uZm9ybWFuY2UiLCJzdWIiOiJjb25mb3JtYW5jZS1zZXJ2aWNlIiwianRpIjoiY29uZm9ybWFuY2Utc2VydmljZS0xNzY3MjI1NTcwLTE3NjcyMjkxNzAiLCJpYXQiOjE3NjcyMjU1NzAsImV4cCI6MTc2NzIyOTE3MH0.d0o46Nuw0c29NsGRaeCrdaIN2tUoUX5tLech0f1sIwMw3qxGYPWIyVkhrefmGOWkOW2gP3HMF7UhNiUo26suDSnIRB5qwlZQ48rqA4ymRYWbeT_cfArXU_F-7tUOTgnaMSe8r6TqbvaGfQjJciuv_NULkta_Q7mrFz6hYHxu2beDoFP1xrlBOIqUxzYpBznAUro08cjIumN-kMP29D_v3MaQac0ea4_C1ZBtdUd-VISe756pIv9qygtTal3QG3nU00OrKTKNEgNNmmjMmX6C3On5rSD4LFZhsFXfkvIBS6KJWHSHs2fLTC9V2m0EIJI5oHtOYYN6ij4YEaVE9Vi1gw",
211
+ "expect": {
212
+ "outcome": "reject",
213
+ "status": 401,
214
+ "reason": "invalid_lifetime"
215
+ }
172
216
  }
173
217
  ]
174
218
  }