@owlmeans/server-auth-token 0.1.18-rc.13 → 0.1.18-rc.14
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/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-token/SKILL.md +20 -7
- package/build/guard.d.ts.map +1 -1
- package/build/guard.js +10 -0
- package/build/guard.js.map +1 -1
- package/build/handlers/index.d.ts +45 -2
- package/build/handlers/index.d.ts.map +1 -1
- package/build/handlers/index.js +43 -27
- package/build/handlers/index.js.map +1 -1
- package/build/types.d.ts +15 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/guard.ts +10 -0
- package/src/handlers/index.ts +66 -29
- package/src/types.ts +15 -0
- package/tests/guard.spec.ts +44 -0
- package/tests/handlers.spec.ts +25 -0
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-auth-token",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.14",
|
|
5
|
+
"generatedAt": "2026-09-18T18:24:55.977Z",
|
|
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.
|
|
11
|
+
**Install:** `"@owlmeans/server-auth-token": "^0.1.18-rc.14"` in `dependencies`
|
|
12
12
|
**Contracts:** `@owlmeans/auth-token` — the record, the routes, the format helpers
|
|
13
13
|
|
|
14
14
|
## Key Exports
|
|
@@ -23,7 +23,9 @@ user-invocable: false
|
|
|
23
23
|
| `hashAccessToken(token)` · `mintAccessToken(prefix)` | The stored form; one minted `{ token, hash, display }` |
|
|
24
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
|
-
| `
|
|
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
|
-
`
|
|
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
|
|
|
@@ -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]]
|
package/build/guard.d.ts.map
CHANGED
|
@@ -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,
|
|
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,YAgHF,CAAA"}
|
package/build/guard.js
CHANGED
|
@@ -63,6 +63,16 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
|
63
63
|
return false;
|
|
64
64
|
if (record.expiresAt != null && new Date(record.expiresAt) < new Date())
|
|
65
65
|
return false;
|
|
66
|
+
// A token minted through an OAuth grant carries the resource(s) it was issued FOR. A token
|
|
67
|
+
// with no audience is the ordinary hand-minted kind and is admitted everywhere its scopes
|
|
68
|
+
// reach, exactly as before this check existed — the guard has to OPT IN with `resources` to
|
|
69
|
+
// narrow anything, so a deployment that never touches this option sees no change at all.
|
|
70
|
+
const resources = typeof opts.resources === 'function' ? opts.resources(context) : opts.resources;
|
|
71
|
+
if (record.audience != null && record.audience.length > 0
|
|
72
|
+
&& resources != null && resources.length > 0
|
|
73
|
+
&& !record.audience.some(resource => resources.includes(resource))) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
66
76
|
// A token is only ever as good as the profile behind it. Reading the profile per request is
|
|
67
77
|
// what makes revoking a person's access revoke every token they ever minted, without any
|
|
68
78
|
// token record being touched.
|
package/build/guard.js.map
CHANGED
|
@@ -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
|
|
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:
|
|
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":"
|
|
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"}
|
package/build/handlers/index.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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:
|
|
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":"AAAA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACvF,OAAO,EACL,kBAAkB,EAAE,mBAAmB,EACxC,MAAM,sBAAsB,CAAA;AAI7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAItC,MAAM,IAAI,GAAG,CAAC,MAAyB,EAAmB,EAAE;IAC1D,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAA;IAEhC,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED,MAAM,MAAM,GAAG,CAAC,GAAqB,EAAuB,EAAE,CAC5D,GAAG,CAAC,QAAQ,CAAsB,mBAAmB,CAAC,CAAA;AAExD
|
|
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?: {
|
package/build/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
+
"version": "0.1.18-rc.14",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@noble/hashes": "^1.5.0",
|
|
27
27
|
"@owlmeans/auth": "^0.1.18-rc.27",
|
|
28
28
|
"@owlmeans/auth-common": "^0.1.18-rc.29",
|
|
29
|
-
"@owlmeans/auth-token": "^0.1.18-rc.
|
|
29
|
+
"@owlmeans/auth-token": "^0.1.18-rc.17",
|
|
30
30
|
"@owlmeans/context": "^0.1.18-rc.25",
|
|
31
31
|
"@owlmeans/entrypoint": "^0.1.18-rc.28",
|
|
32
32
|
"@owlmeans/mongo-resource": "^0.1.18-rc.28",
|
package/src/guard.ts
CHANGED
|
@@ -75,6 +75,16 @@ export const makeAuthTokenGuard = (
|
|
|
75
75
|
if (record == null) return false as T
|
|
76
76
|
if (record.revokedAt != null) return false as T
|
|
77
77
|
if (record.expiresAt != null && new Date(record.expiresAt) < new Date()) return false as T
|
|
78
|
+
// A token minted through an OAuth grant carries the resource(s) it was issued FOR. A token
|
|
79
|
+
// with no audience is the ordinary hand-minted kind and is admitted everywhere its scopes
|
|
80
|
+
// reach, exactly as before this check existed — the guard has to OPT IN with `resources` to
|
|
81
|
+
// narrow anything, so a deployment that never touches this option sees no change at all.
|
|
82
|
+
const resources = typeof opts.resources === 'function' ? opts.resources(context) : opts.resources
|
|
83
|
+
if (record.audience != null && record.audience.length > 0
|
|
84
|
+
&& resources != null && resources.length > 0
|
|
85
|
+
&& !record.audience.some(resource => resources.includes(resource))) {
|
|
86
|
+
return false as T
|
|
87
|
+
}
|
|
78
88
|
|
|
79
89
|
// A token is only ever as good as the profile behind it. Reading the profile per request is
|
|
80
90
|
// what makes revoking a person's access revoke every token they ever minted, without any
|
package/src/handlers/index.ts
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
package/tests/guard.spec.ts
CHANGED
|
@@ -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)
|
package/tests/handlers.spec.ts
CHANGED
|
@@ -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()
|