@owlmeans/server-auth-token 0.1.18-rc.3 → 0.1.18-rc.30

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/README.md CHANGED
@@ -6,7 +6,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
6
6
  your project's skill store (`.agents/skills/`):
7
7
 
8
8
  ```sh
9
- npx @owlmeans/agent-skills@^0.1.18-rc.15
9
+ npx @owlmeans/agent-skills@^0.1.18-rc.47
10
10
  ```
11
11
 
12
12
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-auth-token",
4
- "version": "0.1.18-rc.3",
5
- "generatedAt": "2026-09-12T08:39:52.739Z",
4
+ "version": "0.1.18-rc.30",
5
+ "generatedAt": "2026-10-03T20:05:07.610Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: server-auth-token
3
- description: How to use @owlmeans/server-auth-token — the server half of long-lived access tokens — the Mongo store, the guard that verifies a presented token and intersects its scopes with the profile's, the mint/list/revoke handlers, and the coguard that admits a token on every already-guarded route. Auto-invoked when registering the token guard or resources, mounting the token handlers, or diagnosing a 401 on a route an access token should reach.
3
+ description: How to use @owlmeans/server-auth-token — the server half of long-lived access tokens — the Mongo store, the guard that verifies a presented token and intersects its scopes with the profile's, the mint/list/revoke handlers, the shared `issueAccessToken`, audience admission for OAuth-issued tokens, and the coguard that admits a token on every already-guarded route. Auto-invoked when registering the token guard or resources, mounting the token handlers, or diagnosing a 401 on a route an access token should reach.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/server-auth-token
9
9
 
10
10
  **Layer:** Server
11
- **Install:** `"@owlmeans/server-auth-token": "^0.1.18-rc.3"` in `dependencies`
11
+ **Install:** `"@owlmeans/server-auth-token": "^0.1.18-rc.30"` in `dependencies`
12
12
  **Contracts:** `@owlmeans/auth-token` — the record, the routes, the format helpers
13
13
 
14
14
  ## Key Exports
@@ -21,9 +21,11 @@ user-invocable: false
21
21
  | `makeAccessTokenResource(dbAlias?)` | The Mongo resource, with its three indexes |
22
22
  | `prefixOf(context, opts?)` | The deployment's prefix: guard options → config → default |
23
23
  | `hashAccessToken(token)` · `mintAccessToken(prefix)` | The stored form; one minted `{ token, hash, display }` |
24
- | `setupAuthTokenCoguard(entrypoints, guard?)` | Append the guard to every entrypoint that already has one |
24
+ | `withAuthTokenCoguard(protocolTree, guard?)` | Return the same-shaped immutable tree with the guard appended to every already-guarded protocol |
25
25
  | `listAccessTokens` · `createAccessToken` · `revokeAccessToken` | The three handlers |
26
- | `AuthTokenGuardOptions` (`prefix`, `denyAliases`, `touchInterval`, `resourceAlias`, `profileAlias`) · `AuthTokenConfig` · `AccessTokenResource` | Types |
26
+ | `issueAccessToken(ctx, subject, request)` | Mint one token for `{ entityId, userId, profileId, role, scopes }` — what `createAccessToken` does over an HTTP body, and what an OAuth token endpoint calls over a session it verified itself. `request` is `CreateAccessToken` plus `audience?: string[]` |
27
+ | `refuseTokenAuth(req, what)` | The one "interactive session only" check — throws `AuthForbidden` when `req.auth.type` is an access token |
28
+ | `AuthTokenGuardOptions` (`prefix`, `denyAliases`, `touchInterval`, `resourceAlias`, `profileAlias`, `resources`) · `IssueAccessTokenSubject` · `IssueAccessTokenRequest` · `AuthTokenConfig` · `AccessTokenResource` | Types |
27
29
 
28
30
  ## Wiring
29
31
 
@@ -77,6 +79,16 @@ Three properties keep it from colliding with the guards beside it:
77
79
  - `entitySlug` is resolved through `ENTITY_RESOLVER` when one is registered, so the payload is
78
80
  identical to a browser session's rather than carrying a raw id.
79
81
 
82
+ **Audience admission is opt-in.** A token minted through an OAuth grant carries `audience` (the resources
83
+ it is FOR, RFC 8707). A guard configured with `resources` (`string[]` or `(ctx) => string[]`, resolved
84
+ per request, because `appendAuthTokenGuard` runs from `makeContext` before a secret-mounted hostname is
85
+ necessarily readable) refuses — as an ordinary non-match, a 401 — a token whose non-empty `audience`
86
+ does not intersect it. A token with **no** audience (every hand-minted one) is admitted everywhere its
87
+ scopes reach, and a guard with no `resources` admits every token whatever its audience, so a deployment
88
+ that never mints an audience-restricted token sees no change. Two guards over different surfaces are
89
+ how a token bound to `/mcp` stays out of the ordinary REST API while a REST-scoped token still works
90
+ through `/mcp`.
91
+
80
92
  `lastUsedAt` is a **throttled, fire-and-forget** write (`touchInterval`, default 5 minutes). A usage
81
93
  timestamp is never a reason to fail a request, and awaiting it would put a database write on the
82
94
  critical path of every authenticated call. It answers one question — "is this token still in use?" —
@@ -85,8 +97,8 @@ and is not an access log.
85
97
  ## A token may never mint or revoke a token
86
98
 
87
99
  Minting is the one operation that turns a stolen credential into a permanent one: a token that can
88
- create tokens survives the revocation of the token that leaked. `createAccessToken` and
89
- `revokeAccessToken` refuse a request whose `auth.type` is `AuthroizationType.AuthToken`, and a
100
+ create tokens survives the revocation of the token that leaked. `createAccessToken`, `revokeAccessToken` and any other minting path (an OAuth consent approval calls
101
+ `refuseTokenAuth` before it snapshots the subject) refuse a request whose `auth.type` is `AuthroizationType.AuthToken`, and a
90
102
  deployment additionally names those routes in the guard's **deny list**, so the refusal is a 401 at
91
103
  the boundary rather than a check every future handler has to remember.
92
104
 
@@ -99,7 +111,7 @@ tokens that are not theirs.
99
111
  ## The coguard admits a token everywhere, and the deny list lives in the GUARD
100
112
 
101
113
  ```typescript
102
- setupAuthTokenCoguard(managerEntrypoints as Array<{ guards?: string[] }>)
114
+ const guardedManagerProtocols = withAuthTokenCoguard(managerProtocols)
103
115
  ```
104
116
 
105
117
  An API client drives the same surface a browser does — projects, stories, files — so admitting the
@@ -108,8 +120,8 @@ never substitutes: the primary guard stays first and still claims its own creden
108
120
  matches only a value carrying the prefix. It skips entrypoints that have no guard, and it is
109
121
  idempotent.
110
122
 
111
- **Run it after the final binding**, because it reads the guard list each entrypoint actually ended
112
- up with.
123
+ Decorate the declaration tree before bindings are created. The helper preserves the input tree's
124
+ shape, leaves unguarded protocols unchanged, and is idempotent.
113
125
 
114
126
  Routes that must stay behind an interactive session — opening a checkout, starting an OAuth flow,
115
127
  minting another token — are named in `AuthTokenGuardOptions.denyAliases` rather than skipped here.
@@ -121,7 +133,7 @@ works.
121
133
  ## Tests
122
134
 
123
135
  `bun test ./tests` in the package — what the guard claims, what it resolves (including that a token
124
- can never outrank its profile), the mint/list/revoke rules, and the coguard's four properties.
136
+ can never outrank its profile), the mint/list/revoke rules, `issueAccessToken` parity with the route, audience admission, and the coguard's four properties.
125
137
 
126
138
  ## Depends On
127
139
 
@@ -132,5 +144,6 @@ can never outrank its profile), the mint/list/revoke rules, and the coguard's fo
132
144
 
133
145
  ## Related
134
146
 
135
- - [[auth-token]] — the format, the routes and the client carrier guard
147
+ - [[auth-token]] — the format, the routes, `audience` and the client carrier guard
148
+ - [[server-oauth]] — the authorization server that mints audience-scoped tokens through `issueAccessToken`
136
149
  - [[web-auth-token]] — the management panel · [[auth-protocol]] · [[server-auth]]
@@ -1,4 +1,4 @@
1
- import type { EntrypointProtocolDeclaration } from '@owlmeans/entrypoint';
1
+ import type { EntrypointTree } from '@owlmeans/entrypoint';
2
2
  /**
3
3
  * Let an access token authenticate every route that already has a guard.
4
4
  *
@@ -11,5 +11,5 @@ import type { EntrypointProtocolDeclaration } from '@owlmeans/entrypoint';
11
11
  * than skipped here — a child entrypoint inherits every guard its ancestors declare and cannot
12
12
  * drop one, so the refusal has to live where the credential is inspected.
13
13
  */
14
- export declare const withAuthTokenCoguard: <Protocol extends EntrypointProtocolDeclaration>(declarations: readonly Protocol[], guard?: string) => Protocol[];
14
+ export declare const withAuthTokenCoguard: <Tree extends EntrypointTree>(tree: Tree, guard?: string) => Tree;
15
15
  //# sourceMappingURL=coguard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"coguard.d.ts","sourceRoot":"","sources":["../src/coguard.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAA;AAEzE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,SAAS,6BAA6B,gBACnE,SAAS,QAAQ,EAAE,UAAS,MAAM,KAC/C,QAAQ,EAIT,CAAA"}
1
+ {"version":3,"file":"coguard.d.ts","sourceRoot":"","sources":["../src/coguard.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAAI,IAAI,SAAS,cAAc,QACxD,IAAI,UAAS,MAAM,KACxB,IAID,CAAA"}
package/build/coguard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GUARD_AUTH_TOKEN } from '@owlmeans/auth-token';
2
- import { decorateEntrypoint } from '@owlmeans/entrypoint';
2
+ import { decorateEntrypoint, mapProtocols } from '@owlmeans/entrypoint';
3
3
  /**
4
4
  * Let an access token authenticate every route that already has a guard.
5
5
  *
@@ -12,7 +12,7 @@ import { decorateEntrypoint } from '@owlmeans/entrypoint';
12
12
  * than skipped here — a child entrypoint inherits every guard its ancestors declare and cannot
13
13
  * drop one, so the refusal has to live where the credential is inspected.
14
14
  */
15
- export const withAuthTokenCoguard = (declarations, guard = GUARD_AUTH_TOKEN) => declarations.map(declaration => {
15
+ export const withAuthTokenCoguard = (tree, guard = GUARD_AUTH_TOKEN) => mapProtocols(tree, declaration => {
16
16
  if (declaration.guards.length < 1 || declaration.guards.includes(guard))
17
17
  return declaration;
18
18
  return decorateEntrypoint(declaration, { guards: [...declaration.guards, guard] });
@@ -1 +1 @@
1
- {"version":3,"file":"coguard.js","sourceRoot":"","sources":["../src/coguard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAGzD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,YAAiC,EAAE,KAAK,GAAW,gBAAgB,EACvD,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE;IAC9C,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAA;IAE3F,OAAO,kBAAkB,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,CAAA;AACpF,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"coguard.js","sourceRoot":"","sources":["../src/coguard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAGvE;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAU,EAAE,KAAK,GAAW,gBAAgB,EACtC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE;IAC1C,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAA;IAE3F,OAAO,kBAAkB,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,CAAA;AACpF,CAAC,CAAC,CAAA"}
package/build/guard.d.ts CHANGED
@@ -2,23 +2,5 @@ import type { GuardService } from '@owlmeans/entrypoint';
2
2
  import type { AuthTokenContext, AuthTokenGuardOptions } from './types.js';
3
3
  /** Where the deployment's prefix comes from: the guard's own options, then config, then default. */
4
4
  export declare const prefixOf: (context: AuthTokenContext, opts?: AuthTokenGuardOptions) => string;
5
- /**
6
- * The guard that verifies a long-lived access token.
7
- *
8
- * It is a GUARD, not an authentication method: nothing is exchanged, no envelope is signed, no
9
- * session begins. A caller presents a credential it was handed once, and the guard turns it into
10
- * the same `Auth` payload every other guard produces — so ownership gates, entitlement gates and
11
- * handlers written against a browser session work unchanged for an API client.
12
- *
13
- * Three properties are what keep it from colliding with the guards it sits beside:
14
- *
15
- * - it claims a header only when the value carries the deployment's prefix, so an OwlMeans
16
- * `ED25519-BASIC-TOKEN` bearer, an OIDC one, and this one can all arrive under `Authorization`
17
- * without any of them shadowing the others;
18
- * - it accepts the OwlMeans `AUTH-TOKEN` scheme AND plain `Bearer`, because a third-party client
19
- * configured with a URL sends the latter whatever the documentation says;
20
- * - it intersects the token's scopes with its profile's on every request, so a token can never
21
- * outlive or outrank the person who minted it.
22
- */
23
5
  export declare const makeAuthTokenGuard: (alias?: string, opts?: AuthTokenGuardOptions) => GuardService;
24
6
  //# sourceMappingURL=guard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAqC,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAI3F,OAAO,KAAK,EAAwC,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAE/G,oGAAoG;AACpG,eAAO,MAAM,QAAQ,YAAa,gBAAgB,SAAS,qBAAqB,KAAG,MACc,CAAA;AAEjG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,WACtB,MAAM,SAA2B,qBAAqB,KAC5D,YAsGF,CAAA"}
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAqC,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAI3F,OAAO,KAAK,EAAwC,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAG/G,oGAAoG;AACpG,eAAO,MAAM,QAAQ,YAAa,gBAAgB,SAAS,qBAAqB,KAAG,MACc,CAAA;AAmCjG,eAAO,MAAM,kBAAkB,WACtB,MAAM,SAA2B,qBAAqB,KAC5D,YAgHF,CAAA"}
package/build/guard.js CHANGED
@@ -5,6 +5,7 @@ import { ENTITY_RESOLVER } from '@owlmeans/auth-common';
5
5
  import { createService } from '@owlmeans/context';
6
6
  import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity';
7
7
  import { hashAccessToken } from './hash.js';
8
+ import { logger, logThrottle } from '@owlmeans/log';
8
9
  /** Where the deployment's prefix comes from: the guard's own options, then config, then default. */
9
10
  export const prefixOf = (context, opts) => opts?.prefix ?? context.cfg.authToken?.prefix ?? AUTH_TOKEN_DEFAULT_PREFIX;
10
11
  /**
@@ -25,6 +26,19 @@ export const prefixOf = (context, opts) => opts?.prefix ?? context.cfg.authToken
25
26
  * - it intersects the token's scopes with its profile's on every request, so a token can never
26
27
  * outlive or outrank the person who minted it.
27
28
  */
29
+ const log = logger('server-auth-token');
30
+ /**
31
+ * A presented access token the guard did not accept. Never the token, not even its display form:
32
+ * its record id only. Once a minute per token and reason — a client retrying a revoked token calls every second.
33
+ */
34
+ const refuse = (reason, record) => {
35
+ if (logThrottle(`auth-token:${reason}:${record?.id ?? ''}`, 60_000)) {
36
+ log.warn('Access token refused', {
37
+ reason, tokenId: record?.id, entityId: record?.entityId,
38
+ }, { event: 'auth.refused' });
39
+ }
40
+ return false;
41
+ };
28
42
  export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
29
43
  const touchInterval = opts.touchInterval ?? AUTH_TOKEN_TOUCH_INTERVAL;
30
44
  const present = (req, prefix) => {
@@ -58,11 +72,21 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
58
72
  const tokens = context.resource(opts.resourceAlias ?? AUTH_TOKEN_RESOURCE);
59
73
  const record = await tokens.load({ hash: hashAccessToken(presented) });
60
74
  if (record == null)
61
- return false;
75
+ return refuse('unknown');
62
76
  if (record.revokedAt != null)
63
- return false;
77
+ return refuse('revoked', record);
64
78
  if (record.expiresAt != null && new Date(record.expiresAt) < new Date())
65
- return false;
79
+ return refuse('expired', record);
80
+ // A token minted through an OAuth grant carries the resource(s) it was issued FOR. A token
81
+ // with no audience is the ordinary hand-minted kind and is admitted everywhere its scopes
82
+ // reach, exactly as before this check existed — the guard has to OPT IN with `resources` to
83
+ // narrow anything, so a deployment that never touches this option sees no change at all.
84
+ const resources = typeof opts.resources === 'function' ? opts.resources(context) : opts.resources;
85
+ if (record.audience != null && record.audience.length > 0
86
+ && resources != null && resources.length > 0
87
+ && !record.audience.some(resource => resources.includes(resource))) {
88
+ return refuse('audience', record);
89
+ }
66
90
  // A token is only ever as good as the profile behind it. Reading the profile per request is
67
91
  // what makes revoking a person's access revoke every token they ever minted, without any
68
92
  // token record being touched.
@@ -72,12 +96,12 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
72
96
  profile = await profiles.load({ entityId: record.entityId, profileId: record.profileId });
73
97
  }
74
98
  catch {
75
- return false;
99
+ return refuse('profile-unreadable', record);
76
100
  }
77
101
  if (profile == null)
78
- return false;
102
+ return refuse('profile', record);
79
103
  if (profile.expiresAt != null && new Date(profile.expiresAt) < new Date())
80
- return false;
104
+ return refuse('profile-expired', record);
81
105
  const profileScopes = profile.scopes ?? [];
82
106
  const scopes = profileScopes.includes(ALL_SCOPES)
83
107
  ? record.scopes
@@ -114,7 +138,7 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
114
138
  // Fire and forget: a usage timestamp is never a reason to fail a request, and awaiting it
115
139
  // would put a database write on the critical path of every authenticated call.
116
140
  void tokens.save({ ...record, lastUsedAt: new Date() })
117
- .catch((e) => console.error('auth-token: lastUsedAt', e));
141
+ .catch((e) => { log.warn('Access token lastUsedAt not saved', { tokenId: record.id, error: e }); });
118
142
  }
119
143
  return true;
120
144
  },
@@ -1 +1 @@
1
- {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,51 @@
1
- import type { AccessTokenList, IssuedAccessToken } from '@owlmeans/auth-token';
1
+ import { AuthRole } from '@owlmeans/auth';
2
+ import type { AccessTokenList, CreateAccessToken, IssuedAccessToken } from '@owlmeans/auth-token';
3
+ import type { AuthTokenContext } from '../types.js';
2
4
  import type { AuthTokenEntrypoints } from '@owlmeans/auth-token';
5
+ /**
6
+ * A token may never mint another token.
7
+ *
8
+ * Minting is the one operation that turns a stolen credential into a permanent one: a token that
9
+ * can create tokens survives the revocation of the token that leaked. Creating and revoking
10
+ * therefore require a credential a person produced in a browser — which is also why the deployment
11
+ * names these routes in the guard's deny list, so the refusal is a 401 at the boundary rather than
12
+ * a check every future handler has to remember.
13
+ *
14
+ * Exported because an OAuth authorization server's consent-approval handler
15
+ * (`@owlmeans/server-oauth`) mints tokens too and needs the exact same guard, over a session it
16
+ * verified itself rather than a route this package bound.
17
+ */
18
+ export declare const refuseTokenAuth: (req: {
19
+ auth?: {
20
+ type?: string;
21
+ };
22
+ }, what: string) => void;
23
+ /** Whoever is minting the token — read once, by whichever caller resolved the session. */
24
+ export interface IssueAccessTokenSubject {
25
+ entityId: string;
26
+ userId: string;
27
+ profileId: string;
28
+ role: AuthRole;
29
+ /** The subject's OWN scopes — minting can only narrow, never widen, this set. */
30
+ scopes: string[];
31
+ }
32
+ export interface IssueAccessTokenRequest extends CreateAccessToken {
33
+ /**
34
+ * The resource(s) this token is FOR (RFC 8707), when it is minted through an OAuth grant.
35
+ * Absent for a token minted by hand — the ordinary, unrestricted kind.
36
+ */
37
+ audience?: string[];
38
+ }
39
+ /**
40
+ * Mint one token for `subject`, exactly what `createAccessToken` does over an HTTP body — pulled
41
+ * out so a second caller (an OAuth token endpoint) can mint the SAME shape of record over a
42
+ * session it authenticated its own way, without a duplicate copy of the narrowing rule or the
43
+ * TTL clamp silently drifting from this one.
44
+ */
45
+ export declare const issueAccessToken: (ctx: AuthTokenContext, subject: IssueAccessTokenSubject, payload: IssueAccessTokenRequest) => Promise<IssuedAccessToken>;
3
46
  export declare const listAccessTokens: (protocol: AuthTokenEntrypoints['list']) => import("@owlmeans/server-entrypoint").BoundEntrypointHandler<import("@owlmeans/entrypoint").EntrypointProtocol<import("@owlmeans/entrypoint").OpenRequest, AccessTokenList>>;
4
47
  export declare const createAccessToken: (protocol: AuthTokenEntrypoints['create']) => import("@owlmeans/server-entrypoint").BoundEntrypointHandler<import("@owlmeans/entrypoint").EntrypointProtocol<{
5
- body: import("@owlmeans/auth-token").CreateAccessToken;
48
+ body: CreateAccessToken;
6
49
  }, IssuedAccessToken>>;
7
50
  export declare const revokeAccessToken: (protocol: AuthTokenEntrypoints['revoke']) => import("@owlmeans/server-entrypoint").BoundEntrypointHandler<import("@owlmeans/entrypoint").EntrypointProtocol<{
8
51
  params: import("@owlmeans/auth-token").AccessTokenParams;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EAAsC,iBAAiB,EACvE,MAAM,sBAAsB,CAAA;AAM7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AA0BhE,eAAO,MAAM,gBAAgB,aAAc,oBAAoB,CAAC,MAAM,CAAC,iLAarE,CAAA;AAEF,eAAO,MAAM,iBAAiB,aAAc,oBAAoB,CAAC,QAAQ,CAAC;;sBAsCxE,CAAA;AAEF,eAAO,MAAM,iBAAiB,aAAc,oBAAoB,CAAC,QAAQ,CAAC;;;;GAqBxE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgD,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAIvF,OAAO,KAAK,EACV,eAAe,EAAsC,iBAAiB,EAAE,iBAAiB,EAC1F,MAAM,sBAAsB,CAAA;AAK7B,OAAO,KAAK,EAAuB,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACxE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAWhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,QAAS;IAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,QAAQ,MAAM,KAAG,IAIjF,CAAA;AAED,0FAA0F;AAC1F,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,QAAQ,CAAA;IACd,iFAAiF;IACjF,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,MAAM,WAAW,uBAAwB,SAAQ,iBAAiB;IAChE;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QACtB,gBAAgB,WAAW,uBAAuB,WAAW,uBAAuB,KACxF,OAAO,CAAC,iBAAiB,CA+B3B,CAAA;AAED,eAAO,MAAM,gBAAgB,aAAc,oBAAoB,CAAC,MAAM,CAAC,iLAarE,CAAA;AAEF,eAAO,MAAM,iBAAiB,aAAc,oBAAoB,CAAC,QAAQ,CAAC;;sBAYxE,CAAA;AAEF,eAAO,MAAM,iBAAiB,aAAc,oBAAoB,CAAC,QAAQ,CAAC;;;;GAqBxE,CAAA"}
@@ -17,34 +17,24 @@ const tokens = (ctx) => ctx.resource(AUTH_TOKEN_RESOURCE);
17
17
  * therefore require a credential a person produced in a browser — which is also why the deployment
18
18
  * names these routes in the guard's deny list, so the refusal is a 401 at the boundary rather than
19
19
  * a check every future handler has to remember.
20
+ *
21
+ * Exported because an OAuth authorization server's consent-approval handler
22
+ * (`@owlmeans/server-oauth`) mints tokens too and needs the exact same guard, over a session it
23
+ * verified itself rather than a route this package bound.
20
24
  */
21
- const refuseTokenAuth = (req, what) => {
25
+ export const refuseTokenAuth = (req, what) => {
22
26
  if (req.auth?.type === AuthroizationType.AuthToken) {
23
27
  throw new AuthForbidden(what);
24
28
  }
25
29
  };
26
- export const listAccessTokens = (protocol) => handlers().request(protocol, async (req, context) => {
27
- const ctx = context;
28
- const entityId = requireEntityKey(req);
29
- const profileId = req.auth?.profileId;
30
- if (profileId == null)
31
- throw new AuthForbidden('profile');
32
- const result = await tokens(ctx).list({ entityId, profileId }, { size: 0 });
33
- return {
34
- items: result.items
35
- .map(view)
36
- .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
37
- };
38
- });
39
- export const createAccessToken = (protocol) => handlers().body(protocol, async (payload, context, req) => {
40
- const ctx = context;
41
- refuseTokenAuth(req, 'token-mint');
42
- const entityId = requireEntityKey(req);
43
- const profileId = req.auth?.profileId;
44
- const userId = req.auth?.userId;
45
- if (profileId == null || userId == null)
46
- throw new AuthForbidden('profile');
47
- const own = req.auth?.scopes ?? [];
30
+ /**
31
+ * Mint one token for `subject`, exactly what `createAccessToken` does over an HTTP body — pulled
32
+ * out so a second caller (an OAuth token endpoint) can mint the SAME shape of record over a
33
+ * session it authenticated its own way, without a duplicate copy of the narrowing rule or the
34
+ * TTL clamp silently drifting from this one.
35
+ */
36
+ export const issueAccessToken = async (ctx, subject, payload) => {
37
+ const own = subject.scopes;
48
38
  const scopes = payload.scopes ?? own;
49
39
  // A token is a delegation, so it can only ever narrow. Asking for a scope the caller does not
50
40
  // hold is refused rather than silently dropped: a token that quietly grants less than it was
@@ -59,18 +49,44 @@ export const createAccessToken = (protocol) => handlers().body(protocol, async (
59
49
  hash: minted.hash,
60
50
  display: minted.display,
61
51
  name: payload.name,
62
- userId,
63
- profileId,
64
- entityId,
52
+ userId: subject.userId,
53
+ profileId: subject.profileId,
54
+ entityId: subject.entityId,
65
55
  scopes,
66
- role: req.auth?.role ?? AuthRole.User,
56
+ role: subject.role,
67
57
  createdAt: now,
58
+ ...(payload.audience != null ? { audience: payload.audience } : {}),
68
59
  ...(payload.expiresIn != null
69
60
  ? { expiresAt: new Date(now.getTime() + Math.min(payload.expiresIn * 1000, AUTH_TOKEN_MAX_TTL)) }
70
61
  : {}),
71
62
  });
72
63
  // The only moment the plaintext exists outside the caller's own machine.
73
64
  return { token: minted.token, record: view(record) };
65
+ };
66
+ export const listAccessTokens = (protocol) => handlers().request(protocol, async (req, context) => {
67
+ const ctx = context;
68
+ const entityId = requireEntityKey(req);
69
+ const profileId = req.auth?.profileId;
70
+ if (profileId == null)
71
+ throw new AuthForbidden('profile');
72
+ const result = await tokens(ctx).list({ entityId, profileId }, { size: 0 });
73
+ return {
74
+ items: result.items
75
+ .map(view)
76
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
77
+ };
78
+ });
79
+ export const createAccessToken = (protocol) => handlers().body(protocol, async (payload, context, req) => {
80
+ const ctx = context;
81
+ refuseTokenAuth(req, 'token-mint');
82
+ const entityId = requireEntityKey(req);
83
+ const profileId = req.auth?.profileId;
84
+ const userId = req.auth?.userId;
85
+ if (profileId == null || userId == null)
86
+ throw new AuthForbidden('profile');
87
+ return issueAccessToken(ctx, {
88
+ entityId, profileId, userId, role: req.auth?.role ?? AuthRole.User, scopes: req.auth?.scopes ?? [],
89
+ }, payload);
74
90
  });
75
91
  export const revokeAccessToken = (protocol) => handlers().params(protocol, async (payload, context, req) => {
76
92
  const ctx = context;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"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"}
package/build/types.d.ts CHANGED
@@ -26,6 +26,21 @@ export interface AuthTokenGuardOptions {
26
26
  resourceAlias?: string;
27
27
  /** Resource alias holding the identity profiles a token is bound to. */
28
28
  profileAlias?: string;
29
+ /**
30
+ * The resource(s) this guard speaks for, opting it into audience admission.
31
+ *
32
+ * Left unset (the default), the guard admits every token whatever its `audience` — full
33
+ * backward compatibility for a deployment that never mints an audience-restricted token. Set
34
+ * it to the canonical resource URI(s) this route surface serves, and a token issued FOR a
35
+ * different resource (an OAuth-issued, MCP-only token, say) is refused here rather than
36
+ * quietly working everywhere the profile's scopes would otherwise reach.
37
+ *
38
+ * A function is resolved with the live context on every request rather than once at
39
+ * registration time — `appendAuthTokenGuard` runs from `makeContext`, before a hostname a
40
+ * Kubernetes secret mount populates is necessarily readable yet, and this guard's `handle`
41
+ * already runs per-request regardless.
42
+ */
43
+ resources?: string[] | ((context: AuthTokenContext) => string[]);
29
44
  }
30
45
  export interface AuthTokenConfig extends ServerConfig {
31
46
  authToken?: {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE3E,MAAM,MAAM,mBAAmB,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAA;AAElE,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,SAAS,CAAC,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,aAAa,CAAC,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE3E,MAAM,MAAM,mBAAmB,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAA;AAElE,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,EAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,SAAS,CAAC,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,aAAa,CAAC,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/server-auth-token",
3
- "version": "0.1.18-rc.3",
3
+ "version": "0.1.18-rc.30",
4
4
  "license": "MIT",
5
5
  "description": "The server half of OwlMeans access tokens — the store, the guard that verifies a presented token, the management handlers and the coguard that admits one on an existing route.",
6
6
  "type": "module",
@@ -24,16 +24,17 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@noble/hashes": "^1.5.0",
27
- "@owlmeans/auth": "^0.1.18-rc.12",
28
- "@owlmeans/auth-common": "^0.1.18-rc.16",
29
- "@owlmeans/auth-token": "^0.1.18-rc.3",
30
- "@owlmeans/context": "^0.1.18-rc.11",
31
- "@owlmeans/entrypoint": "^0.1.18-rc.14",
32
- "@owlmeans/mongo-resource": "^0.1.18-rc.15",
33
- "@owlmeans/resource": "^0.1.18-rc.12",
34
- "@owlmeans/server-api": "^0.1.18-rc.20",
35
- "@owlmeans/server-auth-identity": "^0.1.18-rc.16",
36
- "@owlmeans/server-context": "^0.1.18-rc.15",
27
+ "@owlmeans/auth": "^0.1.18-rc.38",
28
+ "@owlmeans/auth-common": "^0.1.18-rc.45",
29
+ "@owlmeans/auth-token": "^0.1.18-rc.30",
30
+ "@owlmeans/context": "^0.1.18-rc.32",
31
+ "@owlmeans/entrypoint": "^0.1.18-rc.39",
32
+ "@owlmeans/log": "^0.1.18-rc.0",
33
+ "@owlmeans/mongo-resource": "^0.1.18-rc.41",
34
+ "@owlmeans/resource": "^0.1.18-rc.38",
35
+ "@owlmeans/server-api": "^0.1.18-rc.49",
36
+ "@owlmeans/server-auth-identity": "^0.1.18-rc.45",
37
+ "@owlmeans/server-context": "^0.1.18-rc.41",
37
38
  "@scure/base": "^1.1.9"
38
39
  },
39
40
  "devDependencies": {
package/src/coguard.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { GUARD_AUTH_TOKEN } from '@owlmeans/auth-token'
2
- import { decorateEntrypoint } from '@owlmeans/entrypoint'
3
- import type { EntrypointProtocolDeclaration } from '@owlmeans/entrypoint'
2
+ import { decorateEntrypoint, mapProtocols } from '@owlmeans/entrypoint'
3
+ import type { EntrypointTree } from '@owlmeans/entrypoint'
4
4
 
5
5
  /**
6
6
  * Let an access token authenticate every route that already has a guard.
@@ -14,9 +14,9 @@ import type { EntrypointProtocolDeclaration } from '@owlmeans/entrypoint'
14
14
  * than skipped here — a child entrypoint inherits every guard its ancestors declare and cannot
15
15
  * drop one, so the refusal has to live where the credential is inspected.
16
16
  */
17
- export const withAuthTokenCoguard = <Protocol extends EntrypointProtocolDeclaration>(
18
- declarations: readonly Protocol[], guard: string = GUARD_AUTH_TOKEN,
19
- ): Protocol[] => declarations.map(declaration => {
17
+ export const withAuthTokenCoguard = <Tree extends EntrypointTree>(
18
+ tree: Tree, guard: string = GUARD_AUTH_TOKEN,
19
+ ): Tree => mapProtocols(tree, declaration => {
20
20
  if (declaration.guards.length < 1 || declaration.guards.includes(guard)) return declaration
21
21
 
22
22
  return decorateEntrypoint(declaration, { guards: [...declaration.guards, guard] })
package/src/guard.ts CHANGED
@@ -13,6 +13,7 @@ import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
13
13
  import type { IdentityProfile, IdentityProfileResource } from '@owlmeans/server-auth-identity'
14
14
  import { hashAccessToken } from './hash.js'
15
15
  import type { AccessTokenResource, AuthTokenConfig, AuthTokenContext, AuthTokenGuardOptions } from './types.js'
16
+ import { logger, logThrottle } from '@owlmeans/log'
16
17
 
17
18
  /** Where the deployment's prefix comes from: the guard's own options, then config, then default. */
18
19
  export const prefixOf = (context: AuthTokenContext, opts?: AuthTokenGuardOptions): string =>
@@ -36,6 +37,21 @@ export const prefixOf = (context: AuthTokenContext, opts?: AuthTokenGuardOptions
36
37
  * - it intersects the token's scopes with its profile's on every request, so a token can never
37
38
  * outlive or outrank the person who minted it.
38
39
  */
40
+ const log = logger('server-auth-token')
41
+
42
+ /**
43
+ * A presented access token the guard did not accept. Never the token, not even its display form:
44
+ * its record id only. Once a minute per token and reason — a client retrying a revoked token calls every second.
45
+ */
46
+ const refuse = (reason: string, record?: { id?: string, entityId?: string }): false => {
47
+ if (logThrottle(`auth-token:${reason}:${record?.id ?? ''}`, 60_000)) {
48
+ log.warn('Access token refused', {
49
+ reason, tokenId: record?.id, entityId: record?.entityId,
50
+ }, { event: 'auth.refused' })
51
+ }
52
+ return false
53
+ }
54
+
39
55
  export const makeAuthTokenGuard = (
40
56
  alias: string = GUARD_AUTH_TOKEN, opts: AuthTokenGuardOptions = {}
41
57
  ): GuardService => {
@@ -72,9 +88,19 @@ export const makeAuthTokenGuard = (
72
88
 
73
89
  const tokens = context.resource<AccessTokenResource>(opts.resourceAlias ?? AUTH_TOKEN_RESOURCE)
74
90
  const record = await tokens.load({ hash: hashAccessToken(presented) })
75
- if (record == null) return false as T
76
- if (record.revokedAt != null) return false as T
77
- if (record.expiresAt != null && new Date(record.expiresAt) < new Date()) return false as T
91
+ if (record == null) return refuse('unknown') as T
92
+ if (record.revokedAt != null) return refuse('revoked', record) as T
93
+ if (record.expiresAt != null && new Date(record.expiresAt) < new Date()) return refuse('expired', record) as T
94
+ // A token minted through an OAuth grant carries the resource(s) it was issued FOR. A token
95
+ // with no audience is the ordinary hand-minted kind and is admitted everywhere its scopes
96
+ // reach, exactly as before this check existed — the guard has to OPT IN with `resources` to
97
+ // narrow anything, so a deployment that never touches this option sees no change at all.
98
+ const resources = typeof opts.resources === 'function' ? opts.resources(context) : opts.resources
99
+ if (record.audience != null && record.audience.length > 0
100
+ && resources != null && resources.length > 0
101
+ && !record.audience.some(resource => resources.includes(resource))) {
102
+ return refuse('audience', record) as T
103
+ }
78
104
 
79
105
  // A token is only ever as good as the profile behind it. Reading the profile per request is
80
106
  // what makes revoking a person's access revoke every token they ever minted, without any
@@ -86,10 +112,10 @@ export const makeAuthTokenGuard = (
86
112
  try {
87
113
  profile = await profiles.load({ entityId: record.entityId, profileId: record.profileId })
88
114
  } catch {
89
- return false as T
115
+ return refuse('profile-unreadable', record) as T
90
116
  }
91
- if (profile == null) return false as T
92
- if (profile.expiresAt != null && new Date(profile.expiresAt) < new Date()) return false as T
117
+ if (profile == null) return refuse('profile', record) as T
118
+ if (profile.expiresAt != null && new Date(profile.expiresAt) < new Date()) return refuse('profile-expired', record) as T
93
119
 
94
120
  const profileScopes = profile.scopes ?? []
95
121
  const scopes = profileScopes.includes(ALL_SCOPES)
@@ -129,7 +155,7 @@ export const makeAuthTokenGuard = (
129
155
  // Fire and forget: a usage timestamp is never a reason to fail a request, and awaiting it
130
156
  // would put a database write on the critical path of every authenticated call.
131
157
  void tokens.save({ ...record, lastUsedAt: new Date() })
132
- .catch((e: unknown) => console.error('auth-token: lastUsedAt', e))
158
+ .catch((e: unknown) => { log.warn('Access token lastUsedAt not saved', { tokenId: record.id, error: e }) })
133
159
  }
134
160
 
135
161
  return true as T
@@ -3,7 +3,7 @@ import {
3
3
  AUTH_TOKEN_MAX_TTL, AUTH_TOKEN_RESOURCE
4
4
  } from '@owlmeans/auth-token'
5
5
  import type {
6
- AccessTokenList, AccessTokenRecord, AccessTokenView, IssuedAccessToken
6
+ AccessTokenList, AccessTokenRecord, AccessTokenView, CreateAccessToken, IssuedAccessToken
7
7
  } from '@owlmeans/auth-token'
8
8
  import { requireEntityKey } from '@owlmeans/auth-common'
9
9
  import { handlers } from '@owlmeans/server-api'
@@ -29,38 +29,45 @@ const tokens = (ctx: AuthTokenContext): AccessTokenResource =>
29
29
  * therefore require a credential a person produced in a browser — which is also why the deployment
30
30
  * names these routes in the guard's deny list, so the refusal is a 401 at the boundary rather than
31
31
  * a check every future handler has to remember.
32
+ *
33
+ * Exported because an OAuth authorization server's consent-approval handler
34
+ * (`@owlmeans/server-oauth`) mints tokens too and needs the exact same guard, over a session it
35
+ * verified itself rather than a route this package bound.
32
36
  */
33
- const refuseTokenAuth = (req: { auth?: { type?: string } }, what: string): void => {
37
+ export const refuseTokenAuth = (req: { auth?: { type?: string } }, what: string): void => {
34
38
  if (req.auth?.type === AuthroizationType.AuthToken) {
35
39
  throw new AuthForbidden(what)
36
40
  }
37
41
  }
38
42
 
39
- export const listAccessTokens = (protocol: AuthTokenEntrypoints['list']) => handlers<AuthTokenContext>().request(protocol, async (req, context) => {
40
- const ctx = context as AuthTokenContext
41
- const entityId = requireEntityKey(req)
42
- const profileId = req.auth?.profileId
43
- if (profileId == null) throw new AuthForbidden('profile')
44
-
45
- const result = await tokens(ctx).list({ entityId, profileId }, { size: 0 })
46
-
47
- return {
48
- items: result.items
49
- .map(view)
50
- .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
51
- } satisfies AccessTokenList
52
- })
53
-
54
- export const createAccessToken = (protocol: AuthTokenEntrypoints['create']) => handlers<AuthTokenContext>().body(protocol, async (payload, context, req) => {
55
- const ctx = context as AuthTokenContext
56
- refuseTokenAuth(req, 'token-mint')
43
+ /** Whoever is minting the token — read once, by whichever caller resolved the session. */
44
+ export interface IssueAccessTokenSubject {
45
+ entityId: string
46
+ userId: string
47
+ profileId: string
48
+ role: AuthRole
49
+ /** The subject's OWN scopes — minting can only narrow, never widen, this set. */
50
+ scopes: string[]
51
+ }
57
52
 
58
- const entityId = requireEntityKey(req)
59
- const profileId = req.auth?.profileId
60
- const userId = req.auth?.userId
61
- if (profileId == null || userId == null) throw new AuthForbidden('profile')
53
+ export interface IssueAccessTokenRequest extends CreateAccessToken {
54
+ /**
55
+ * The resource(s) this token is FOR (RFC 8707), when it is minted through an OAuth grant.
56
+ * Absent for a token minted by hand — the ordinary, unrestricted kind.
57
+ */
58
+ audience?: string[]
59
+ }
62
60
 
63
- const own = req.auth?.scopes ?? []
61
+ /**
62
+ * Mint one token for `subject`, exactly what `createAccessToken` does over an HTTP body — pulled
63
+ * out so a second caller (an OAuth token endpoint) can mint the SAME shape of record over a
64
+ * session it authenticated its own way, without a duplicate copy of the narrowing rule or the
65
+ * TTL clamp silently drifting from this one.
66
+ */
67
+ export const issueAccessToken = async (
68
+ ctx: AuthTokenContext, subject: IssueAccessTokenSubject, payload: IssueAccessTokenRequest
69
+ ): Promise<IssuedAccessToken> => {
70
+ const own = subject.scopes
64
71
  const scopes = payload.scopes ?? own
65
72
  // A token is a delegation, so it can only ever narrow. Asking for a scope the caller does not
66
73
  // hold is refused rather than silently dropped: a token that quietly grants less than it was
@@ -76,12 +83,13 @@ export const createAccessToken = (protocol: AuthTokenEntrypoints['create']) => h
76
83
  hash: minted.hash,
77
84
  display: minted.display,
78
85
  name: payload.name,
79
- userId,
80
- profileId,
81
- entityId,
86
+ userId: subject.userId,
87
+ profileId: subject.profileId,
88
+ entityId: subject.entityId,
82
89
  scopes,
83
- role: req.auth?.role ?? AuthRole.User,
90
+ role: subject.role,
84
91
  createdAt: now,
92
+ ...(payload.audience != null ? { audience: payload.audience } : {}),
85
93
  ...(payload.expiresIn != null
86
94
  ? { expiresAt: new Date(now.getTime() + Math.min(payload.expiresIn * 1000, AUTH_TOKEN_MAX_TTL)) }
87
95
  : {}),
@@ -89,6 +97,35 @@ export const createAccessToken = (protocol: AuthTokenEntrypoints['create']) => h
89
97
 
90
98
  // The only moment the plaintext exists outside the caller's own machine.
91
99
  return { token: minted.token, record: view(record) } satisfies IssuedAccessToken
100
+ }
101
+
102
+ export const listAccessTokens = (protocol: AuthTokenEntrypoints['list']) => handlers<AuthTokenContext>().request(protocol, async (req, context) => {
103
+ const ctx = context as AuthTokenContext
104
+ const entityId = requireEntityKey(req)
105
+ const profileId = req.auth?.profileId
106
+ if (profileId == null) throw new AuthForbidden('profile')
107
+
108
+ const result = await tokens(ctx).list({ entityId, profileId }, { size: 0 })
109
+
110
+ return {
111
+ items: result.items
112
+ .map(view)
113
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
114
+ } satisfies AccessTokenList
115
+ })
116
+
117
+ export const createAccessToken = (protocol: AuthTokenEntrypoints['create']) => handlers<AuthTokenContext>().body(protocol, async (payload, context, req) => {
118
+ const ctx = context as AuthTokenContext
119
+ refuseTokenAuth(req, 'token-mint')
120
+
121
+ const entityId = requireEntityKey(req)
122
+ const profileId = req.auth?.profileId
123
+ const userId = req.auth?.userId
124
+ if (profileId == null || userId == null) throw new AuthForbidden('profile')
125
+
126
+ return issueAccessToken(ctx, {
127
+ entityId, profileId, userId, role: req.auth?.role ?? AuthRole.User, scopes: req.auth?.scopes ?? [],
128
+ }, payload)
92
129
  })
93
130
 
94
131
  export const revokeAccessToken = (protocol: AuthTokenEntrypoints['revoke']) => handlers<AuthTokenContext>().params(protocol, async (payload, context, req) => {
package/src/types.ts CHANGED
@@ -28,6 +28,21 @@ export interface AuthTokenGuardOptions {
28
28
  resourceAlias?: string
29
29
  /** Resource alias holding the identity profiles a token is bound to. */
30
30
  profileAlias?: string
31
+ /**
32
+ * The resource(s) this guard speaks for, opting it into audience admission.
33
+ *
34
+ * Left unset (the default), the guard admits every token whatever its `audience` — full
35
+ * backward compatibility for a deployment that never mints an audience-restricted token. Set
36
+ * it to the canonical resource URI(s) this route surface serves, and a token issued FOR a
37
+ * different resource (an OAuth-issued, MCP-only token, say) is refused here rather than
38
+ * quietly working everywhere the profile's scopes would otherwise reach.
39
+ *
40
+ * A function is resolved with the live context on every request rather than once at
41
+ * registration time — `appendAuthTokenGuard` runs from `makeContext`, before a hostname a
42
+ * Kubernetes secret mount populates is necessarily readable yet, and this guard's `handle`
43
+ * already runs per-request regardless.
44
+ */
45
+ resources?: string[] | ((context: AuthTokenContext) => string[])
31
46
  }
32
47
 
33
48
  export interface AuthTokenConfig extends ServerConfig {
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { GUARD_AUTH_TOKEN } from '@owlmeans/auth-token'
3
- import { openProtocol } from '@owlmeans/entrypoint'
3
+ import { openProtocol, protocols } from '@owlmeans/entrypoint'
4
4
  import { route } from '@owlmeans/route'
5
5
  import { withAuthTokenCoguard } from '../src/coguard.js'
6
6
 
7
7
  describe('@owlmeans/server-auth-token — the coguard', () => {
8
8
  test('decorates a guarded protocol and leaves the primary guard first', () => {
9
9
  const guarded = openProtocol(route('guarded', '/guarded'), { guards: 'auth' })
10
- const [decorated] = withAuthTokenCoguard([guarded])
10
+ const [decorated] = protocols(withAuthTokenCoguard({ guarded }))
11
11
 
12
12
  expect(decorated.guards).toEqual(['auth', GUARD_AUTH_TOKEN])
13
13
  expect(guarded.guards).toEqual(['auth'])
@@ -15,20 +15,20 @@ describe('@owlmeans/server-auth-token — the coguard', () => {
15
15
 
16
16
  test('leaves an unguarded protocol unchanged', () => {
17
17
  const open = openProtocol(route('open', '/open'))
18
- expect(withAuthTokenCoguard([open])[0]).toBe(open)
18
+ expect(protocols(withAuthTokenCoguard({ open }))[0]).toBe(open)
19
19
  })
20
20
 
21
21
  test('is idempotent', () => {
22
22
  const guarded = openProtocol(route('guarded', '/guarded'), { guards: 'auth' })
23
- const [once] = withAuthTokenCoguard([guarded])
24
- const [twice] = withAuthTokenCoguard([once])
23
+ const [once] = protocols(withAuthTokenCoguard({ guarded }))
24
+ const [twice] = protocols(withAuthTokenCoguard({ once }))
25
25
 
26
26
  expect(twice.guards).toEqual(['auth', GUARD_AUTH_TOKEN])
27
27
  })
28
28
 
29
29
  test('honours a custom guard alias', () => {
30
30
  const guarded = openProtocol(route('guarded', '/guarded'), { guards: 'auth' })
31
- expect(withAuthTokenCoguard([guarded], 'other-guard')[0].guards)
31
+ expect(protocols(withAuthTokenCoguard({ guarded }, 'other-guard'))[0].guards)
32
32
  .toEqual(['auth', 'other-guard'])
33
33
  })
34
34
  })
@@ -116,6 +116,50 @@ describe('@owlmeans/server-auth-token — what the guard resolves', () => {
116
116
  })
117
117
  })
118
118
 
119
+ describe('@owlmeans/server-auth-token — audience admission', () => {
120
+ test('a token with no audience is admitted by a guard that opted into resource checking', async () => {
121
+ const { guard } = await armed({ resources: ['https://api.example.com'] })
122
+
123
+ expect(await guard.handle(request(`Bearer ${TOKEN}`), response())).toBe(true)
124
+ })
125
+
126
+ test('a token scoped to a resource is refused by a guard for a different one', async () => {
127
+ const context = await makeTestContext({ resources: ['https://api.example.com'] })
128
+ await seedProfile(context)
129
+ await seedToken(context, hashAccessToken(TOKEN), { audience: ['https://api.example.com/mcp'] })
130
+ const guard = context.service<GuardService>(GUARD_AUTH_TOKEN)
131
+
132
+ expect(await guard.handle(request(`Bearer ${TOKEN}`), response())).toBe(false)
133
+ })
134
+
135
+ test('a token scoped to a resource is admitted by a guard configured for it', async () => {
136
+ const context = await makeTestContext({ resources: ['https://api.example.com/mcp', 'https://api.example.com'] })
137
+ await seedProfile(context)
138
+ await seedToken(context, hashAccessToken(TOKEN), { audience: ['https://api.example.com/mcp'] })
139
+ const guard = context.service<GuardService>(GUARD_AUTH_TOKEN)
140
+
141
+ expect(await guard.handle(request(`Bearer ${TOKEN}`), response())).toBe(true)
142
+ })
143
+
144
+ test('a guard that never opted in ignores audience entirely — full backward compatibility', async () => {
145
+ const context = await makeTestContext()
146
+ await seedProfile(context)
147
+ await seedToken(context, hashAccessToken(TOKEN), { audience: ['https://api.example.com/mcp'] })
148
+ const guard = context.service<GuardService>(GUARD_AUTH_TOKEN)
149
+
150
+ expect(await guard.handle(request(`Bearer ${TOKEN}`), response())).toBe(true)
151
+ })
152
+
153
+ test('a function `resources` option is resolved with the live context, not cached from registration', async () => {
154
+ const context = await makeTestContext({ resources: () => ['https://api.example.com'] })
155
+ await seedProfile(context)
156
+ await seedToken(context, hashAccessToken(TOKEN), { audience: ['https://api.example.com'] })
157
+ const guard = context.service<GuardService>(GUARD_AUTH_TOKEN)
158
+
159
+ expect(await guard.handle(request(`Bearer ${TOKEN}`), response())).toBe(true)
160
+ })
161
+ })
162
+
119
163
  describe('@owlmeans/server-auth-token — the hash at rest', () => {
120
164
  test('the stored value is not the token', () => {
121
165
  const hash = hashAccessToken(TOKEN)
@@ -8,6 +8,7 @@ import {
8
8
  revokeAccessToken as bindRevokeAccessToken,
9
9
  } from '../src/handlers/index.js'
10
10
  import { hashAccessToken } from '../src/hash.js'
11
+ import { issueAccessToken } from '../src/handlers/index.js'
11
12
  import { makeTestContext, seedProfile, seedToken, TEST_ENTITY, TEST_PREFIX, TEST_PROFILE, TEST_USER } from './context.js'
12
13
 
13
14
  /**
@@ -104,6 +105,30 @@ describe('@owlmeans/server-auth-token — minting', () => {
104
105
  })
105
106
  })
106
107
 
108
+ describe('@owlmeans/server-auth-token — issueAccessToken (the OAuth server\'s entry point)', () => {
109
+ test('stores the audience it was given, and omits the field when there is none', async () => {
110
+ const context = await makeTestContext()
111
+ const subject = { entityId: TEST_ENTITY, profileId: TEST_PROFILE, userId: TEST_USER, role: AuthRole.User, scopes: ['*'] }
112
+
113
+ const withAudience = await issueAccessToken(context, subject, { name: 'mcp', audience: ['https://api.example.com/mcp'] })
114
+ expect(withAudience.record.audience).toEqual(['https://api.example.com/mcp'])
115
+
116
+ const withoutAudience = await issueAccessToken(context, subject, { name: 'hand-minted' })
117
+ expect(withoutAudience.record.audience).toBeUndefined()
118
+ })
119
+
120
+ test('still narrows scopes and clamps the TTL the same way the HTTP handler does', async () => {
121
+ const context = await makeTestContext()
122
+ const subject = { entityId: TEST_ENTITY, profileId: TEST_PROFILE, userId: TEST_USER, role: AuthRole.User, scopes: ['projects:read'] }
123
+
124
+ await expect(issueAccessToken(context, subject, { name: 'x', scopes: ['projects:write'] })).rejects.toThrow()
125
+
126
+ const issued = await issueAccessToken(context, subject, { name: 'x', expiresIn: 60 * 60 * 24 * 400 })
127
+ const ttlMs = new Date(issued.record.expiresAt!).getTime() - Date.now()
128
+ expect(ttlMs).toBeLessThanOrEqual(366 * 24 * 60 * 60 * 1000)
129
+ })
130
+ })
131
+
107
132
  describe('@owlmeans/server-auth-token — listing and revoking', () => {
108
133
  test('a list shows the caller\'s own tokens and never a hash', async () => {
109
134
  const context = await makeTestContext()