@owlmeans/server-auth-token 0.1.18-rc.29 → 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 +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-token/SKILL.md +1 -1
- package/build/guard.d.ts +0 -18
- package/build/guard.d.ts.map +1 -1
- package/build/guard.js +22 -8
- package/build/guard.js.map +1 -1
- package/package.json +9 -8
- package/src/guard.ts +24 -8
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.
|
|
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
|
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-10-
|
|
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
|
{
|
|
@@ -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.30"` in `dependencies`
|
|
12
12
|
**Contracts:** `@owlmeans/auth-token` — the record, the routes, the format helpers
|
|
13
13
|
|
|
14
14
|
## Key Exports
|
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
|
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;
|
|
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,11 @@ 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
|
|
75
|
+
return refuse('unknown');
|
|
62
76
|
if (record.revokedAt != null)
|
|
63
|
-
return
|
|
77
|
+
return refuse('revoked', record);
|
|
64
78
|
if (record.expiresAt != null && new Date(record.expiresAt) < new Date())
|
|
65
|
-
return
|
|
79
|
+
return refuse('expired', record);
|
|
66
80
|
// A token minted through an OAuth grant carries the resource(s) it was issued FOR. A token
|
|
67
81
|
// with no audience is the ordinary hand-minted kind and is admitted everywhere its scopes
|
|
68
82
|
// reach, exactly as before this check existed — the guard has to OPT IN with `resources` to
|
|
@@ -71,7 +85,7 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
|
71
85
|
if (record.audience != null && record.audience.length > 0
|
|
72
86
|
&& resources != null && resources.length > 0
|
|
73
87
|
&& !record.audience.some(resource => resources.includes(resource))) {
|
|
74
|
-
return
|
|
88
|
+
return refuse('audience', record);
|
|
75
89
|
}
|
|
76
90
|
// A token is only ever as good as the profile behind it. Reading the profile per request is
|
|
77
91
|
// what makes revoking a person's access revoke every token they ever minted, without any
|
|
@@ -82,12 +96,12 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
|
82
96
|
profile = await profiles.load({ entityId: record.entityId, profileId: record.profileId });
|
|
83
97
|
}
|
|
84
98
|
catch {
|
|
85
|
-
return
|
|
99
|
+
return refuse('profile-unreadable', record);
|
|
86
100
|
}
|
|
87
101
|
if (profile == null)
|
|
88
|
-
return
|
|
102
|
+
return refuse('profile', record);
|
|
89
103
|
if (profile.expiresAt != null && new Date(profile.expiresAt) < new Date())
|
|
90
|
-
return
|
|
104
|
+
return refuse('profile-expired', record);
|
|
91
105
|
const profileScopes = profile.scopes ?? [];
|
|
92
106
|
const scopes = profileScopes.includes(ALL_SCOPES)
|
|
93
107
|
? record.scopes
|
|
@@ -124,7 +138,7 @@ export const makeAuthTokenGuard = (alias = GUARD_AUTH_TOKEN, opts = {}) => {
|
|
|
124
138
|
// Fire and forget: a usage timestamp is never a reason to fail a request, and awaiting it
|
|
125
139
|
// would put a database write on the critical path of every authenticated call.
|
|
126
140
|
void tokens.save({ ...record, lastUsedAt: new Date() })
|
|
127
|
-
.catch((e) =>
|
|
141
|
+
.catch((e) => { log.warn('Access token lastUsedAt not saved', { tokenId: record.id, error: e }); });
|
|
128
142
|
}
|
|
129
143
|
return true;
|
|
130
144
|
},
|
package/build/guard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EACL,yBAAyB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,yBAAyB,EAC5F,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,wBAAwB,EACzE,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAA;AAEtE,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EACL,yBAAyB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,yBAAyB,EAC5F,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,wBAAwB,EACzE,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAA;AAEtE,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE3C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEnD,oGAAoG;AACpG,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAyB,EAAE,IAA4B,EAAU,EAAE,CAC1F,IAAI,EAAE,MAAM,IAAK,OAAO,CAAC,GAAuB,CAAC,SAAS,EAAE,MAAM,IAAI,yBAAyB,CAAA;AAEjG;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,GAAG,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAA;AAEvC;;;GAGG;AACH,MAAM,MAAM,GAAG,CAAC,MAAc,EAAE,MAA2C,EAAS,EAAE;IACpF,IAAI,WAAW,CAAC,cAAc,MAAM,IAAI,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC;QACpE,GAAG,CAAC,IAAI,CAAC,sBAAsB,EAAE;YAC/B,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ;SACxD,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAA;IAC/B,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,KAAK,GAAW,gBAAgB,EAAE,IAAI,GAA0B,EAAE,EACpD,EAAE;IAChB,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,yBAAyB,CAAA;IAErE,MAAM,OAAO,GAAG,CAAC,GAA6B,EAAE,MAAc,EAAiB,EAAE;QAC/E,MAAM,MAAM,GAAG,wBAAwB,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,CAAC,CAAA;QACnE,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,IAAI,CAAA;QAC/B,IAAI,MAAM,CAAC,MAAM,KAAK,iBAAiB,IAAI,MAAM,CAAC,MAAM,KAAK,aAAa;YAAE,OAAO,IAAI,CAAA;QAEvF,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IAClE,CAAC,CAAA;IAED,MAAM,OAAO,GAAiB,aAAa,CAAe,KAAK,EAAE;QAC/D,KAAK,EAAE,KAAK,EAAC,GAAG,EAAC,EAAE;YACjB,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,EAAqC,CAAA;YACtE,IAAI,GAAG,IAAI,IAAI;gBAAE,OAAO,KAAK,CAAA;YAC7B,wFAAwF;YACxF,0FAA0F;YAC1F,2FAA2F;YAC3F,uEAAuE;YACvE,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,IAAI,GAAG,CAAC,KAAK,IAAI,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC1F,OAAO,KAAK,CAAA;YACd,CAAC;YAED,OAAO,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI,CAAA;QACtD,CAAC;QAED,MAAM,EAAE,KAAK,EAAK,GAAoB,EAAE,GAA2B,EAAE,EAAE;YACrE,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,EAAqC,CAAA;YACtE,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YACtC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;YACtC,IAAI,SAAS,IAAI,IAAI;gBAAE,OAAO,KAAU,CAAA;YAExC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAsB,IAAI,CAAC,aAAa,IAAI,mBAAmB,CAAC,CAAA;YAC/F,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;YACtE,IAAI,MAAM,IAAI,IAAI;gBAAE,OAAO,MAAM,CAAC,SAAS,CAAM,CAAA;YACjD,IAAI,MAAM,CAAC,SAAS,IAAI,IAAI;gBAAE,OAAO,MAAM,CAAC,SAAS,EAAE,MAAM,CAAM,CAAA;YACnE,IAAI,MAAM,CAAC,SAAS,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,IAAI,EAAE;gBAAE,OAAO,MAAM,CAAC,SAAS,EAAE,MAAM,CAAM,CAAA;YAC9G,2FAA2F;YAC3F,0FAA0F;YAC1F,4FAA4F;YAC5F,yFAAyF;YACzF,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAA;YACjG,IAAI,MAAM,CAAC,QAAQ,IAAI,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;mBACpD,SAAS,IAAI,IAAI,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;mBACzC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;gBACrE,OAAO,MAAM,CAAC,UAAU,EAAE,MAAM,CAAM,CAAA;YACxC,CAAC;YAED,4FAA4F;YAC5F,yFAAyF;YACzF,8BAA8B;YAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAC/B,IAAI,CAAC,YAAY,IAAI,qBAAqB,CAC3C,CAAA;YACD,IAAI,OAAO,GAA2B,IAAI,CAAA;YAC1C,IAAI,CAAC;gBACH,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;YAC3F,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAM,CAAA;YAClD,CAAC;YACD,IAAI,OAAO,IAAI,IAAI;gBAAE,OAAO,MAAM,CAAC,SAAS,EAAE,MAAM,CAAM,CAAA;YAC1D,IAAI,OAAO,CAAC,SAAS,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,IAAI,EAAE;gBAAE,OAAO,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAM,CAAA;YAExH,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;YAC1C,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAC/C,CAAC,CAAC,MAAM,CAAC,MAAM;gBACf,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;oBAClC,CAAC,CAAC,aAAa;oBACf,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;YAElE,0FAA0F;YAC1F,2FAA2F;YAC3F,4BAA4B;YAC5B,IAAI,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAA;YAChC,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;gBACxC,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAwB,eAAe,CAAC,CAAA;gBACxE,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;gBACnD,IAAI,MAAM,IAAI,IAAI;oBAAE,UAAU,GAAG,MAAM,CAAC,IAAI,CAAA;YAC9C,CAAC;YAED,GAAG,CAAC,OAAO,CAAC;gBACV,uFAAuF;gBACvF,0CAA0C;gBAC1C,KAAK,EAAE,MAAM,CAAC,OAAO;gBACrB,IAAI,EAAE,iBAAiB,CAAC,SAAS;gBACjC,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,UAAU;gBACV,MAAM;gBACN,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO;gBAC3B,SAAS,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC;gBACrC,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACvE,CAAC,CAAA;YAEV,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAA;YAClF,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,aAAa,EAAE,CAAC;gBACtC,0FAA0F;gBAC1F,+EAA+E;gBAC/E,KAAK,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;qBACpD,KAAK,CAAC,CAAC,CAAU,EAAE,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,mCAAmC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;YAC/G,CAAC;YAED,OAAO,IAAS,CAAA;QAClB,CAAC;QAED,mFAAmF;QACnF,aAAa,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI;KAChC,CAAC,CAAA;IAEF,OAAO,OAAO,CAAA;AAChB,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.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",
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@noble/hashes": "^1.5.0",
|
|
27
27
|
"@owlmeans/auth": "^0.1.18-rc.38",
|
|
28
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
29
|
-
"@owlmeans/auth-token": "^0.1.18-rc.
|
|
28
|
+
"@owlmeans/auth-common": "^0.1.18-rc.45",
|
|
29
|
+
"@owlmeans/auth-token": "^0.1.18-rc.30",
|
|
30
30
|
"@owlmeans/context": "^0.1.18-rc.32",
|
|
31
31
|
"@owlmeans/entrypoint": "^0.1.18-rc.39",
|
|
32
|
-
"@owlmeans/
|
|
33
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
34
|
-
"@owlmeans/
|
|
35
|
-
"@owlmeans/server-
|
|
36
|
-
"@owlmeans/server-
|
|
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/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,9 @@ 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
|
|
76
|
-
if (record.revokedAt != null) return
|
|
77
|
-
if (record.expiresAt != null && new Date(record.expiresAt) < new Date()) return
|
|
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
|
|
78
94
|
// A token minted through an OAuth grant carries the resource(s) it was issued FOR. A token
|
|
79
95
|
// with no audience is the ordinary hand-minted kind and is admitted everywhere its scopes
|
|
80
96
|
// reach, exactly as before this check existed — the guard has to OPT IN with `resources` to
|
|
@@ -83,7 +99,7 @@ export const makeAuthTokenGuard = (
|
|
|
83
99
|
if (record.audience != null && record.audience.length > 0
|
|
84
100
|
&& resources != null && resources.length > 0
|
|
85
101
|
&& !record.audience.some(resource => resources.includes(resource))) {
|
|
86
|
-
return
|
|
102
|
+
return refuse('audience', record) as T
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
// A token is only ever as good as the profile behind it. Reading the profile per request is
|
|
@@ -96,10 +112,10 @@ export const makeAuthTokenGuard = (
|
|
|
96
112
|
try {
|
|
97
113
|
profile = await profiles.load({ entityId: record.entityId, profileId: record.profileId })
|
|
98
114
|
} catch {
|
|
99
|
-
return
|
|
115
|
+
return refuse('profile-unreadable', record) as T
|
|
100
116
|
}
|
|
101
|
-
if (profile == null) return
|
|
102
|
-
if (profile.expiresAt != null && new Date(profile.expiresAt) < new Date()) return
|
|
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
|
|
103
119
|
|
|
104
120
|
const profileScopes = profile.scopes ?? []
|
|
105
121
|
const scopes = profileScopes.includes(ALL_SCOPES)
|
|
@@ -139,7 +155,7 @@ export const makeAuthTokenGuard = (
|
|
|
139
155
|
// Fire and forget: a usage timestamp is never a reason to fail a request, and awaiting it
|
|
140
156
|
// would put a database write on the critical path of every authenticated call.
|
|
141
157
|
void tokens.save({ ...record, lastUsedAt: new Date() })
|
|
142
|
-
.catch((e: unknown) =>
|
|
158
|
+
.catch((e: unknown) => { log.warn('Access token lastUsedAt not saved', { tokenId: record.id, error: e }) })
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
return true as T
|