@hivemagnus/hive-common 0.2.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,43 @@ 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
+
9
46
  ## [0.2.0] - 2026-11-12
10
47
 
11
48
  Options that let a product keep its existing behaviour without wrapping the shared helpers. Every
@@ -60,6 +97,7 @@ verifier and its own error, health and request-ID conventions.
60
97
  pagination and agent identity.
61
98
  - `/hono` and `/node-http` adapters for auth, request context, the error envelope and health.
62
99
 
63
- [Unreleased]: https://github.com/ChefTaco/HiveCommon/compare/v0.2.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
64
102
  [0.2.0]: https://github.com/ChefTaco/HiveCommon/compare/v0.1.0...v0.2.0
65
103
  [0.1.0]: https://github.com/ChefTaco/HiveCommon/releases/tag/v0.1.0
package/README.md CHANGED
@@ -100,7 +100,8 @@ 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
@@ -138,17 +139,48 @@ Two options make the claim rules stricter for products that always required more
138
139
  `'client_id'` (RFC 9068) or `'either'` (the default). When both claims are present they must
139
140
  agree in every mode.
140
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
+
141
171
  Identity links name the local account `localUserId`. `localId`, the name older deployment configs
142
172
  use, is accepted as an alias; an entry that sets both to different values is refused. Parsed links
143
173
  always carry `localUserId`.
144
174
 
145
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.
146
177
 
147
178
  ### Conformance vectors
148
179
 
149
- `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,
150
181
  valid service, expired, not yet valid, wrong audience, wrong issuer, bad signature, unknown `kid`,
151
- `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
152
184
  clock so they never expire. Run them against your product's verifier:
153
185
 
154
186
  ```ts
@@ -157,13 +189,15 @@ import { runOathweaveConformance } from '@hivemagnus/hive-common/oathweave/confo
157
189
  it('meets the Oathweave conformance vectors', async () => {
158
190
  await runOathweaveConformance((token, ctx) => createProductVerifier({
159
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
160
193
  fetch: ctx.fetch, // serves the vectors' JWKS and userinfo
161
194
  now: () => ctx.nowMs, // the fixed clock
162
195
  })(token));
163
196
  });
164
197
  ```
165
198
 
166
- 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
167
201
  (and any numeric `status` on the error matches); `strictReasons: true` also compares `reason`.
168
202
 
169
203
  ### errors
@@ -406,6 +440,8 @@ existing behaviour with these settings instead of wrapping the shared helpers:
406
440
  | `sessionCacheTtlMs: 0` (verifier) | 10 000 | an online session check on every request, so sign-out is immediate |
407
441
  | `requireRoles: true` (verifier) | `false` | refusing tokens without a `roles` array |
408
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 }` |
409
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 |
410
446
  | `liveBody` (health, `/hono`, `/node-http`) | the standard body | a published `/health/live` body such as `{ status: 'ok' }` |
411
447
  | `localId` in identity links | `localUserId` | existing link configs, unchanged |
@@ -423,6 +459,25 @@ registerHealthRoutes(app, { service: 'chattermost', liveBody: { status: 'ok' } }
423
459
  Every refusal, including 503 `auth_unavailable`, is still an `OathweaveError`; a product that
424
460
  answers 401 for all of them maps the error in its own handler.
425
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
+
426
481
  ## Development
427
482
 
428
483
  ```bash
@@ -58,10 +58,18 @@ export declare function extractScopes(payload: JWTPayload): string[];
58
58
  export declare function extractRoles(payload: JWTPayload, required?: boolean): string[] | undefined;
59
59
  /** A service token is one whose client ID equals its subject. */
60
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
+ }
61
66
  export interface ClaimPolicy {
62
67
  readonly accept: AcceptPrincipal;
63
- /** Largest allowed `exp - iat`, in seconds. */
64
- 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;
65
73
  /** Refuse a token without a `roles` claim. Defaults to false: an absent claim is no roles. */
66
74
  readonly requireRoles?: boolean;
67
75
  /** Which claim names supply the client ID. Defaults to `either`. */
@@ -1,4 +1,6 @@
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
  }
@@ -52,18 +54,19 @@ function validAuthTime(value) {
52
54
  */
53
55
  export function principalFromClaims(payload, policy) {
54
56
  const { iat, exp, sub, jti, iss } = payload;
55
- 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) {
56
61
  throw new OathweaveError('invalid_lifetime');
57
62
  }
58
63
  if (!nonEmpty(sub) || !nonEmpty(jti) || !nonEmpty(iss))
59
64
  throw new OathweaveError('invalid_claims');
60
65
  const permissions = payload['permissions'];
61
- const clientIdClaim = policy.clientIdClaim ?? 'either';
62
66
  const roles = extractRoles(payload, policy.requireRoles ?? false);
63
67
  const clientId = clientIdOf(payload, clientIdClaim);
64
68
  if (!stringArray(permissions) || roles === undefined || clientId === undefined)
65
69
  throw new OathweaveError('invalid_claims');
66
- const kind = principalKindOf(payload, clientIdClaim);
67
70
  if (policy.accept !== 'any' && policy.accept !== kind)
68
71
  throw new OathweaveError('wrong_principal_kind', { details: { kind } });
69
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 { CLIENT_ID_CLAIMS, clientIdOf, extractRoles, extractScopes, principalFromClaims, principalKindOf, type AcceptPrincipal, type ClaimPolicy, type ClientIdClaim, 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,7 +2,7 @@
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';
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
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';
@@ -1,5 +1,5 @@
1
1
  import { type JSONWebKeySet, type JWTVerifyGetKey } from 'jose';
2
- import { type AcceptPrincipal, type ClientIdClaim, 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;
@@ -64,7 +86,10 @@ export interface VerifyOptions {
64
86
  readonly accept?: AcceptPrincipal;
65
87
  }
66
88
  export interface OathweaveVerifier {
89
+ /** The accepted https issuers. */
67
90
  readonly issuers: readonly string[];
91
+ /** The accepted native issuer identifiers; empty unless configured. */
92
+ readonly nativeIssuers: readonly string[];
68
93
  readonly audience: string;
69
94
  verify(token: string, options?: VerifyOptions): Promise<OathweavePrincipal>;
70
95
  verifyUser(token: string): Promise<OathweaveUserPrincipal>;
@@ -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,11 +139,9 @@ 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');
113
145
  const requireRoles = options.requireRoles ?? false;
114
146
  const clientIdClaim = options.clientIdClaim ?? 'either';
115
147
  if (typeof requireRoles !== 'boolean')
@@ -121,6 +153,7 @@ export function createOathweaveVerifier(options) {
121
153
  const request = options.fetch ?? ((input, init) => fetch(input, init));
122
154
  const now = options.now ?? Date.now;
123
155
  const keys = keySource(options, request, timeoutMs);
156
+ const acceptedIssuers = [...issuers, ...nativeIssuers];
124
157
  const sessionTtl = options.sessionCacheTtlMs ?? OATHWEAVE_SESSION_CACHE_TTL_MS;
125
158
  const sessionMax = options.sessionCacheMax ?? OATHWEAVE_SESSION_CACHE_MAX;
126
159
  // Holds only tokens whose signature verified, so garbage can neither fill it nor reach userinfo.
@@ -194,10 +227,11 @@ export function createOathweaveVerifier(options) {
194
227
  try {
195
228
  ({ payload } = await jwtVerify(token, keys, {
196
229
  algorithms: ['RS256'],
197
- issuer: issuers,
230
+ issuer: acceptedIssuers,
198
231
  audience: options.audience,
199
232
  requiredClaims: ['sub', 'iat', 'exp', 'jti'],
200
- 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),
201
235
  clockTolerance,
202
236
  currentDate: new Date(now()),
203
237
  }));
@@ -210,10 +244,14 @@ export function createOathweaveVerifier(options) {
210
244
  throw new OathweaveError('invalid_audience');
211
245
  const principal = principalFromClaims(payload, {
212
246
  accept: verifyOptions.accept ?? defaultAccept,
213
- maxLifetimeSec,
247
+ maxLifetimeSec: lifetimes,
214
248
  requireRoles,
215
249
  clientIdClaim,
216
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
+ }
217
255
  if (principal.kind === 'user' && options.userinfoUrl) {
218
256
  const displayName = await sessionCheck(token, principal.subject);
219
257
  return { ...principal, displayName, sessionChecked: true };
@@ -222,6 +260,7 @@ export function createOathweaveVerifier(options) {
222
260
  }
223
261
  return {
224
262
  issuers,
263
+ nativeIssuers,
225
264
  audience: options.audience,
226
265
  verify,
227
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.2.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
  }