@owlmeans/server-auth 0.1.18-rc.35 → 0.1.18-rc.36
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth/SKILL.md +9 -1
- package/build/manager/entrypoints.d.ts +1 -7
- package/build/manager/entrypoints.d.ts.map +1 -1
- package/build/manager/model.d.ts.map +1 -1
- package/build/manager/model.js +12 -9
- package/build/manager/model.js.map +1 -1
- package/build/manager/plugins/export.d.ts +1 -0
- package/build/manager/plugins/export.d.ts.map +1 -1
- package/build/manager/plugins/export.js +1 -0
- package/build/manager/plugins/export.js.map +1 -1
- package/build/manager/plugins/replay-policy.d.ts +8 -0
- package/build/manager/plugins/replay-policy.d.ts.map +1 -0
- package/build/manager/plugins/replay-policy.js +9 -0
- package/build/manager/plugins/replay-policy.js.map +1 -0
- package/build/manager/plugins/types.d.ts +6 -0
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +115 -8
- package/build/service.js.map +1 -1
- package/package.json +27 -25
- package/src/manager/README.md +1 -1
- package/src/manager/model.ts +12 -9
- package/src/manager/plugins/export.ts +1 -0
- package/src/manager/plugins/replay-policy.ts +7 -0
- package/src/manager/plugins/types.ts +6 -0
- package/src/service.ts +113 -10
- package/tests/context.ts +2 -0
- package/tests/replay-policy.spec.ts +36 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ account/profile records `@owlmeans/server-auth-identity`, and the browser side `
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
bun add @owlmeans/server-auth@^0.1.18-rc.
|
|
16
|
+
bun add @owlmeans/server-auth@^0.1.18-rc.36
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Concepts
|
|
@@ -222,7 +222,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
222
222
|
your project's skill store (`.agents/skills/`):
|
|
223
223
|
|
|
224
224
|
```sh
|
|
225
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
225
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.30
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
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",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.36",
|
|
5
|
+
"generatedAt": "2026-09-19T13:42:56.244Z",
|
|
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
|
|
9
9
|
|
|
10
10
|
**Layer:** Server
|
|
11
|
-
**Install:** `"@owlmeans/server-auth": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/server-auth": "^0.1.18-rc.36"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
Two halves, deliberately split by subpath:
|
|
14
14
|
|
|
@@ -30,6 +30,7 @@ An ordinary service imports the root. Only the auth manager imports `./manager`.
|
|
|
30
30
|
| `AUTH_CACHE` | `'auth-cache'` — the single-use challenge store |
|
|
31
31
|
| `AUTH_SRV_KEY` | `'auth-service'` — the TRUSTED record whose key signs credential envelopes |
|
|
32
32
|
| `AUTHEN_TIMEFRAME` | `15 * 60 * 1000` — challenge lifetime and anti-replay window, in ms |
|
|
33
|
+
| `AUTH_SESSION_MANAGER` / `appendMemoryAuthSessionManager` | Seven-day session registry contract and the process-local default; use the Redis implementation from `@owlmeans/server-auth-session` for a shared authority |
|
|
33
34
|
| `AuthService`, `AuthServiceAppend`, `AuthSpent` | Types |
|
|
34
35
|
| `makeRelyModel`, `makeProviderRely`, `makeConsumerRely`, `RelyOptions` | The rely (wallet handshake) models |
|
|
35
36
|
|
|
@@ -103,6 +104,13 @@ registerPlugin('my-method', context => ({
|
|
|
103
104
|
register a Redis resource under the same alias before calling it in any scaled deployment.
|
|
104
105
|
- A plugin's `init` must return a challenge that is unique per request. A challenge that repeats
|
|
105
106
|
across independent attempts collides in `AUTH_CACHE` and surfaces as `AuthenFailed('challenge')`.
|
|
107
|
+
- Bearers issued by this service carry an absolute seven-day expiry and an opaque session id. In a
|
|
108
|
+
context with `AUTH_SESSION_MANAGER`, the guard rejects an absent, expired, pending or revoked
|
|
109
|
+
session and re-signs fresh profile claims after a registry revision. Registry or identity-store
|
|
110
|
+
failure is `AuthUnavailable` (503), never a false 401/logout.
|
|
111
|
+
- `appendAuthService` installs the memory session manager only as a single-process default. A
|
|
112
|
+
central or multi-replica issuer registers `appendRedisAuthSessionManager(context)` before it;
|
|
113
|
+
the manager is the authority that fences, refreshes or revokes an organization profile's sessions.
|
|
106
114
|
- The manager canonicalizes the organization entity: whatever slug (current, retired, or the frozen
|
|
107
115
|
key) a plugin leaves on `credential.entitySlug` is resolved through `ENTITY_RESOLVER` and replaced
|
|
108
116
|
with the current slug before the envelope is signed. An unresolvable value throws
|
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
/** Server entrypoints for the full authentication manager. */
|
|
2
|
-
export declare const entrypoints:
|
|
3
|
-
body: import("@owlmeans/auth").AllowanceRequest;
|
|
4
|
-
}, import("@owlmeans/auth").AllowanceResponse>> | import("@owlmeans/server-entrypoint").ServerProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
|
|
5
|
-
body: import("@owlmeans/auth").AuthCredentials;
|
|
6
|
-
}, import("@owlmeans/auth").AuthToken>> | import("@owlmeans/server-entrypoint").ServerProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
|
|
7
|
-
query: Partial<import("@owlmeans/auth").AuthToken>;
|
|
8
|
-
}, undefined>>)[];
|
|
2
|
+
export declare const entrypoints: any[];
|
|
9
3
|
//# sourceMappingURL=entrypoints.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../../src/manager/entrypoints.ts"],"names":[],"mappings":"AAiBA,8DAA8D;AAC9D,eAAO,MAAM,WAAW
|
|
1
|
+
{"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../../src/manager/entrypoints.ts"],"names":[],"mappings":"AAiBA,8DAA8D;AAC9D,eAAO,MAAM,WAAW,OAOvB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/manager/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/manager/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAmBlE,eAAO,MAAM,aAAa,YAAa,UAAU,CAAC,SAAS,CAAC,KAAG,SA2F9D,CAAA"}
|
package/build/manager/model.js
CHANGED
|
@@ -5,6 +5,7 @@ import { AUTH_CACHE, AUTHEN_TIMEFRAME } from '../consts.js';
|
|
|
5
5
|
import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope';
|
|
6
6
|
import { createRelyFlow } from './rely/flow.js';
|
|
7
7
|
import { trusted } from './utils/trusted.js';
|
|
8
|
+
import { AuthChallengeReplayPolicy } from './plugins/replay-policy.js';
|
|
8
9
|
export const makeAuthModel = (context) => {
|
|
9
10
|
const cache = (context) => context.resource(AUTH_CACHE);
|
|
10
11
|
const model = {
|
|
@@ -31,20 +32,22 @@ export const makeAuthModel = (context) => {
|
|
|
31
32
|
throw new AuthenFailed('challenge');
|
|
32
33
|
}
|
|
33
34
|
const msg = envelope.message(true);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
error
|
|
35
|
+
const plugin = await getPlugin(envelope.type(), context);
|
|
36
|
+
if (plugin.challengeReplayPolicy !== AuthChallengeReplayPolicy.Plugin) {
|
|
37
|
+
try {
|
|
38
|
+
await cache(context).create({ id: msg }, { ttl: (envelope.envelope.ttl ?? AUTHEN_TIMEFRAME) / 1000 });
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const error = new AuthenFailed('challenge');
|
|
42
|
+
if (e instanceof Error) {
|
|
43
|
+
error.oiriginalStack = `${e} : ${e.stack}`;
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
41
46
|
}
|
|
42
|
-
throw error;
|
|
43
47
|
}
|
|
44
48
|
if (credential.userId == null) {
|
|
45
49
|
throw new AuthenPayloadError('userId');
|
|
46
50
|
}
|
|
47
|
-
const plugin = await getPlugin(envelope.type(), context);
|
|
48
51
|
// @TODO this token PROBABLY needs to be registered somewhere and rechecked
|
|
49
52
|
// from time to time to make sure that the session is not deleted
|
|
50
53
|
// or permission hasn't changed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/manager/model.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAG/E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAE1E,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/manager/model.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.js","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AAEA,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../../../src/manager/plugins/export.ts"],"names":[],"mappings":"AAEA,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Ownership of atomic replay protection for a verified authentication challenge. */
|
|
2
|
+
export declare enum AuthChallengeReplayPolicy {
|
|
3
|
+
/** Default: the auth manager consumes the challenge before plugin authentication. */
|
|
4
|
+
Manager = "manager",
|
|
5
|
+
/** The plugin owns bounded attempts and atomic success consumption. */
|
|
6
|
+
Plugin = "plugin"
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=replay-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-policy.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/replay-policy.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,oBAAY,yBAAyB;IACnC,qFAAqF;IACrF,OAAO,YAAY;IACnB,uEAAuE;IACvE,MAAM,WAAW;CAClB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Ownership of atomic replay protection for a verified authentication challenge. */
|
|
2
|
+
export var AuthChallengeReplayPolicy;
|
|
3
|
+
(function (AuthChallengeReplayPolicy) {
|
|
4
|
+
/** Default: the auth manager consumes the challenge before plugin authentication. */
|
|
5
|
+
AuthChallengeReplayPolicy["Manager"] = "manager";
|
|
6
|
+
/** The plugin owns bounded attempts and atomic success consumption. */
|
|
7
|
+
AuthChallengeReplayPolicy["Plugin"] = "plugin";
|
|
8
|
+
})(AuthChallengeReplayPolicy || (AuthChallengeReplayPolicy = {}));
|
|
9
|
+
//# sourceMappingURL=replay-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-policy.js","sourceRoot":"","sources":["../../../src/manager/plugins/replay-policy.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,CAAN,IAAY,yBAKX;AALD,WAAY,yBAAyB;IACnC,qFAAqF;IACrF,gDAAmB,CAAA;IACnB,uEAAuE;IACvE,8CAAiB,CAAA;AACnB,CAAC,EALW,yBAAyB,KAAzB,yBAAyB,QAKpC"}
|
|
@@ -3,8 +3,14 @@ import type { AuthModel } from '../types.js';
|
|
|
3
3
|
import type { AbstractRequest } from '@owlmeans/entrypoint';
|
|
4
4
|
import type { RedisResource } from '@owlmeans/redis-resource';
|
|
5
5
|
import type { ResourceRecord } from '@owlmeans/resource';
|
|
6
|
+
import type { AuthChallengeReplayPolicy } from './replay-policy.js';
|
|
6
7
|
export interface AuthPlugin extends Omit<AuthModel, "rely"> {
|
|
7
8
|
type: string;
|
|
9
|
+
/**
|
|
10
|
+
* Who atomically consumes a verified challenge. The manager remains the default; a plugin may
|
|
11
|
+
* opt in only when its own credential protocol needs a bounded retry budget before consumption.
|
|
12
|
+
*/
|
|
13
|
+
challengeReplayPolicy?: AuthChallengeReplayPolicy;
|
|
8
14
|
}
|
|
9
15
|
export interface RecpatchaResponse {
|
|
10
16
|
success: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AAEnE,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;IACzD,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,qBAAqB,CAAC,EAAE,yBAAyB,CAAA;CAClD;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe,CAAC;IACxD,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAC;CAAI;AAEN,MAAM,WAAW,UAAW,SAAQ,cAAc,EAAE,iBAAiB;CACpE;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa,CAAC,UAAU,CAAC;CAAG"}
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG3E,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAa,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG3E,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAa,MAAM,YAAY,CAAA;AAoB3E,KAAK,MAAM,GAAG,YAAY,CAAA;AAG1B,eAAO,MAAM,eAAe,WAAW,MAAM,KAAmB,WAqL/D,CAAA;AAED,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,OACvE,CAAC,UAAS,MAAM,KACpB,CAAC,GAAG,iBAiBN,CAAA"}
|
package/build/service.js
CHANGED
|
@@ -2,12 +2,61 @@ import { TRUSTED } from '@owlmeans/config';
|
|
|
2
2
|
import { AUTH_CACHE, AUTH_SRV_KEY, AUTHEN_TIMEFRAME, DEFAULT_ALIAS } from './consts.js';
|
|
3
3
|
import { assertContext, createService } from '@owlmeans/context';
|
|
4
4
|
import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope';
|
|
5
|
-
import { AuthenFailed, AuthorizationError, AuthroizationType } from '@owlmeans/auth';
|
|
5
|
+
import { AuthenFailed, AuthorizationError, AuthroizationType, AuthUnavailable } from '@owlmeans/auth';
|
|
6
6
|
import { createStaticResource } from '@owlmeans/static-resource';
|
|
7
7
|
import { trust, extractAuthToken } from '@owlmeans/auth-common/utils';
|
|
8
|
+
import { ENTITY_RESOLVER } from '@owlmeans/auth-common';
|
|
9
|
+
import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity';
|
|
10
|
+
import { appendMemoryAuthSessionManager, AUTH_SESSION_MANAGER, AUTH_SESSION_TTL } from '@owlmeans/server-auth-session';
|
|
11
|
+
import { uuid } from '@owlmeans/basic-ids';
|
|
12
|
+
import { TOKEN_UPDATE } from '@owlmeans/auth-common';
|
|
8
13
|
export const makeAuthService = (alias = DEFAULT_ALIAS) => {
|
|
9
14
|
const location = `server-auth:${alias}`;
|
|
10
15
|
const cache = (context) => context.resource(AUTH_CACHE);
|
|
16
|
+
const sessions = (context) => context.hasService(AUTH_SESSION_MANAGER)
|
|
17
|
+
? context.service(AUTH_SESSION_MANAGER)
|
|
18
|
+
: null;
|
|
19
|
+
const entityIdFor = async (context, entitySlug) => {
|
|
20
|
+
if (entitySlug == null || entitySlug === '')
|
|
21
|
+
return null;
|
|
22
|
+
if (!context.hasService(ENTITY_RESOLVER))
|
|
23
|
+
return entitySlug;
|
|
24
|
+
const entity = await context.service(ENTITY_RESOLVER).resolve(entitySlug);
|
|
25
|
+
return entity?.id ?? null;
|
|
26
|
+
};
|
|
27
|
+
const refresh = async (context, auth, version) => {
|
|
28
|
+
const entityId = await entityIdFor(context, auth.entitySlug);
|
|
29
|
+
if (entityId == null || auth.profileId == null || !context.hasResource(AUTH_IDENTITY_PROFILE))
|
|
30
|
+
return null;
|
|
31
|
+
const profiles = context.resource(AUTH_IDENTITY_PROFILE);
|
|
32
|
+
let profile = null;
|
|
33
|
+
try {
|
|
34
|
+
profile = await profiles.load({ entityId, profileId: auth.profileId });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// A missing profile is a definitive authentication refusal; a failed profile store is an
|
|
38
|
+
// availability dependency and must not look like a revoked browser session.
|
|
39
|
+
throw new AuthUnavailable('identity-profile');
|
|
40
|
+
}
|
|
41
|
+
if (profile == null || (profile.expiresAt != null && new Date(profile.expiresAt).getTime() <= Date.now()))
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
...auth,
|
|
45
|
+
userId: profile.userId ?? auth.userId,
|
|
46
|
+
role: profile.role,
|
|
47
|
+
scopes: profile.scopes ?? [],
|
|
48
|
+
...(profile.permissions != null ? { permissions: profile.permissions } : {}),
|
|
49
|
+
authorizationVersion: version,
|
|
50
|
+
createdAt: new Date()
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const sign = async (context, auth) => {
|
|
54
|
+
const trusted = await trust(context, TRUSTED, context.cfg.alias ?? context.cfg.service);
|
|
55
|
+
const ttl = auth.expiresAt == null ? AUTH_SESSION_TTL : Math.max(0, new Date(auth.expiresAt).getTime() - Date.now());
|
|
56
|
+
const authorization = await makeEnvelopeModel(AuthroizationType.Ed25519BasicToken)
|
|
57
|
+
.send(auth, ttl).sign(trusted.key, EnvelopeKind.Token);
|
|
58
|
+
return `${AuthroizationType.Ed25519BasicToken.toUpperCase()} ${authorization}`;
|
|
59
|
+
};
|
|
11
60
|
const service = createService(alias, {
|
|
12
61
|
match: async (req) => extractAuthToken(req, AuthroizationType.Ed25519BasicToken) != null,
|
|
13
62
|
handle: async (req, res) => {
|
|
@@ -21,7 +70,32 @@ export const makeAuthService = (alias = DEFAULT_ALIAS) => {
|
|
|
21
70
|
if (!await envelope.verify(trusted.key)) {
|
|
22
71
|
return false;
|
|
23
72
|
}
|
|
24
|
-
|
|
73
|
+
let auth = envelope.message();
|
|
74
|
+
const manager = sessions(context);
|
|
75
|
+
if (manager != null) {
|
|
76
|
+
if (auth.sessionId == null)
|
|
77
|
+
return false;
|
|
78
|
+
let decision;
|
|
79
|
+
try {
|
|
80
|
+
decision = await manager.inspect(auth.sessionId);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Registry availability is a security dependency. A guard cannot authenticate through it.
|
|
84
|
+
throw new AuthUnavailable('session-registry');
|
|
85
|
+
}
|
|
86
|
+
if (decision.state !== 'active' && decision.state !== 'refresh')
|
|
87
|
+
return false;
|
|
88
|
+
if (decision.state === 'refresh') {
|
|
89
|
+
const refreshed = await refresh(context, auth, decision.version);
|
|
90
|
+
if (refreshed == null)
|
|
91
|
+
return false;
|
|
92
|
+
auth = refreshed;
|
|
93
|
+
const replacement = await sign(context, auth);
|
|
94
|
+
res.responseProvider?.header(TOKEN_UPDATE, replacement);
|
|
95
|
+
req.headers = { ...req.headers, authorization: replacement };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
res.resolve(auth);
|
|
25
99
|
return true;
|
|
26
100
|
},
|
|
27
101
|
unpack: async (token) => {
|
|
@@ -32,7 +106,22 @@ export const makeAuthService = (alias = DEFAULT_ALIAS) => {
|
|
|
32
106
|
if (!await envelope.verify(trusted.key)) {
|
|
33
107
|
throw new AuthorizationError('unpack');
|
|
34
108
|
}
|
|
35
|
-
|
|
109
|
+
const auth = envelope.message();
|
|
110
|
+
const manager = sessions(context);
|
|
111
|
+
if (manager != null) {
|
|
112
|
+
if (auth.sessionId == null)
|
|
113
|
+
throw new AuthorizationError('session');
|
|
114
|
+
let decision;
|
|
115
|
+
try {
|
|
116
|
+
decision = await manager.inspect(auth.sessionId);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new AuthUnavailable('session-registry');
|
|
120
|
+
}
|
|
121
|
+
if (decision.state !== 'active')
|
|
122
|
+
throw new AuthorizationError('session');
|
|
123
|
+
}
|
|
124
|
+
return auth;
|
|
36
125
|
},
|
|
37
126
|
authenticated: async () => {
|
|
38
127
|
return null;
|
|
@@ -57,6 +146,8 @@ export const makeAuthService = (alias = DEFAULT_ALIAS) => {
|
|
|
57
146
|
throw new AuthenFailed();
|
|
58
147
|
}
|
|
59
148
|
// @TODO Move to a plugin
|
|
149
|
+
const issuedAt = Date.now();
|
|
150
|
+
const expiresAt = new Date(issuedAt + AUTH_SESSION_TTL);
|
|
60
151
|
const auth = {
|
|
61
152
|
token: credentials.challenge,
|
|
62
153
|
userId: credentials.userId,
|
|
@@ -67,12 +158,25 @@ export const makeAuthService = (alias = DEFAULT_ALIAS) => {
|
|
|
67
158
|
profileId: credentials.profileId,
|
|
68
159
|
entitySlug: credentials.entitySlug,
|
|
69
160
|
isUser: true,
|
|
70
|
-
createdAt: new Date()
|
|
161
|
+
createdAt: new Date(issuedAt),
|
|
162
|
+
expiresAt,
|
|
163
|
+
sessionId: uuid(),
|
|
164
|
+
authorizationVersion: 1
|
|
71
165
|
};
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
166
|
+
const manager = sessions(context);
|
|
167
|
+
if (manager != null) {
|
|
168
|
+
const entityId = await entityIdFor(context, auth.entitySlug);
|
|
169
|
+
if (entityId == null || auth.profileId == null)
|
|
170
|
+
throw new AuthenFailed('session');
|
|
171
|
+
const decision = await manager.register({
|
|
172
|
+
id: auth.sessionId, kind: 'bearer', entityId, profileId: auth.profileId,
|
|
173
|
+
issuedAt, expiresAt: expiresAt.getTime()
|
|
174
|
+
});
|
|
175
|
+
if (decision.state !== 'active')
|
|
176
|
+
throw new AuthenFailed('session');
|
|
177
|
+
auth.authorizationVersion = decision.version;
|
|
178
|
+
}
|
|
179
|
+
return { token: await sign(context, auth) };
|
|
76
180
|
}
|
|
77
181
|
});
|
|
78
182
|
return service;
|
|
@@ -81,6 +185,9 @@ export const appendAuthService = (ctx, alias = DEFAULT_ALIAS) => {
|
|
|
81
185
|
if (!ctx.hasResource(AUTH_CACHE)) {
|
|
82
186
|
ctx.registerResource(createStaticResource(AUTH_CACHE));
|
|
83
187
|
}
|
|
188
|
+
// Every standalone server gets an expiring process-local registry. Scaled deployments replace
|
|
189
|
+
// it with appendRedisAuthSessionManager before calling this helper.
|
|
190
|
+
appendMemoryAuthSessionManager(ctx);
|
|
84
191
|
const service = makeAuthService(alias);
|
|
85
192
|
const context = ctx;
|
|
86
193
|
context.registerService(service);
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEvF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAE1E,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-auth",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.36",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,29 +37,31 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@noble/hashes": "^1.5.0",
|
|
40
|
-
"@owlmeans/api": "^0.1.18-rc.
|
|
41
|
-
"@owlmeans/api-config-server": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
43
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
44
|
-
"@owlmeans/basic-envelope": "^0.1.18-rc.
|
|
45
|
-
"@owlmeans/basic-ids": "^0.1.18-rc.
|
|
46
|
-
"@owlmeans/basic-keys": "^0.1.18-rc.
|
|
47
|
-
"@owlmeans/client-config": "^0.1.18-rc.
|
|
48
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
49
|
-
"@owlmeans/config": "^0.1.18-rc.
|
|
50
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
51
|
-
"@owlmeans/kluster": "^0.1.18-rc.
|
|
52
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
53
|
-
"@owlmeans/redis-resource": "^0.1.18-rc.
|
|
54
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
55
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
56
|
-
"@owlmeans/server-api": "^0.1.18-rc.
|
|
57
|
-
"@owlmeans/server-
|
|
58
|
-
"@owlmeans/server-
|
|
59
|
-
"@owlmeans/server-
|
|
60
|
-
"@owlmeans/server-
|
|
61
|
-
"@owlmeans/
|
|
62
|
-
"@owlmeans/
|
|
40
|
+
"@owlmeans/api": "^0.1.18-rc.30",
|
|
41
|
+
"@owlmeans/api-config-server": "^0.1.18-rc.34",
|
|
42
|
+
"@owlmeans/auth": "^0.1.18-rc.28",
|
|
43
|
+
"@owlmeans/auth-common": "^0.1.18-rc.30",
|
|
44
|
+
"@owlmeans/basic-envelope": "^0.1.18-rc.29",
|
|
45
|
+
"@owlmeans/basic-ids": "^0.1.18-rc.27",
|
|
46
|
+
"@owlmeans/basic-keys": "^0.1.18-rc.31",
|
|
47
|
+
"@owlmeans/client-config": "^0.1.18-rc.30",
|
|
48
|
+
"@owlmeans/client-entrypoint": "^0.1.18-rc.30",
|
|
49
|
+
"@owlmeans/config": "^0.1.18-rc.30",
|
|
50
|
+
"@owlmeans/context": "^0.1.18-rc.26",
|
|
51
|
+
"@owlmeans/kluster": "^0.1.18-rc.29",
|
|
52
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.29",
|
|
53
|
+
"@owlmeans/redis-resource": "^0.1.18-rc.29",
|
|
54
|
+
"@owlmeans/resource": "^0.1.18-rc.28",
|
|
55
|
+
"@owlmeans/route": "^0.1.18-rc.27",
|
|
56
|
+
"@owlmeans/server-api": "^0.1.18-rc.34",
|
|
57
|
+
"@owlmeans/server-auth-identity": "^0.1.18-rc.30",
|
|
58
|
+
"@owlmeans/server-auth-session": "^0.1.18-rc.1",
|
|
59
|
+
"@owlmeans/server-context": "^0.1.18-rc.29",
|
|
60
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.28",
|
|
61
|
+
"@owlmeans/server-route": "^0.1.18-rc.26",
|
|
62
|
+
"@owlmeans/server-socket": "^0.1.18-rc.36",
|
|
63
|
+
"@owlmeans/socket": "^0.1.18-rc.26",
|
|
64
|
+
"@owlmeans/static-resource": "^0.1.18-rc.26",
|
|
63
65
|
"@scure/base": "^2.3.0"
|
|
64
66
|
},
|
|
65
67
|
"peerDependencies": {
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
},
|
|
68
70
|
"devDependencies": {
|
|
69
71
|
"@owlmeans/dep-config": "workspace:*",
|
|
70
|
-
"@owlmeans/test-auth": "^0.1.18-rc.
|
|
72
|
+
"@owlmeans/test-auth": "^0.1.18-rc.29",
|
|
71
73
|
"@types/bun": "^1.4.0",
|
|
72
74
|
"@types/node": "^26.1.0",
|
|
73
75
|
"nodemon": "^3.1.14",
|
package/src/manager/README.md
CHANGED
|
@@ -38,7 +38,7 @@ The manager is built on the OwlMeans entrypoint system, providing:
|
|
|
38
38
|
This manager app is part of the `@owlmeans/server-auth` package:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
npm install @owlmeans/server-auth@^0.1.18-rc.
|
|
41
|
+
npm install @owlmeans/server-auth@^0.1.18-rc.36
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
## API Reference
|
package/src/manager/model.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { trusted } from './utils/trusted.js'
|
|
|
12
12
|
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
13
13
|
import type { Resource } from '@owlmeans/resource'
|
|
14
14
|
import type { AuthSpent } from '../types.js'
|
|
15
|
+
import { AuthChallengeReplayPolicy } from './plugins/replay-policy.js'
|
|
15
16
|
|
|
16
17
|
type Config = ServerConfig
|
|
17
18
|
type Context = ServerContext<Config>
|
|
@@ -51,22 +52,24 @@ export const makeAuthModel = (context: AppContext<AppConfig>): AuthModel => {
|
|
|
51
52
|
|
|
52
53
|
const msg: string = envelope.message(true)
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
const plugin = await getPlugin(envelope.type(), context)
|
|
56
|
+
|
|
57
|
+
if (plugin.challengeReplayPolicy !== AuthChallengeReplayPolicy.Plugin) {
|
|
58
|
+
try {
|
|
59
|
+
await cache(context).create({ id: msg }, { ttl: (envelope.envelope.ttl ?? AUTHEN_TIMEFRAME) / 1000 })
|
|
60
|
+
} catch (e) {
|
|
61
|
+
const error = new AuthenFailed('challenge')
|
|
62
|
+
if (e instanceof Error) {
|
|
63
|
+
error.oiriginalStack = `${e} : ${e.stack}`
|
|
64
|
+
}
|
|
65
|
+
throw error
|
|
60
66
|
}
|
|
61
|
-
throw error
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
if (credential.userId == null) {
|
|
65
70
|
throw new AuthenPayloadError('userId')
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
const plugin = await getPlugin(envelope.type(), context)
|
|
69
|
-
|
|
70
73
|
// @TODO this token PROBABLY needs to be registered somewhere and rechecked
|
|
71
74
|
// from time to time to make sure that the session is not deleted
|
|
72
75
|
// or permission hasn't changed.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Ownership of atomic replay protection for a verified authentication challenge. */
|
|
2
|
+
export enum AuthChallengeReplayPolicy {
|
|
3
|
+
/** Default: the auth manager consumes the challenge before plugin authentication. */
|
|
4
|
+
Manager = 'manager',
|
|
5
|
+
/** The plugin owns bounded attempts and atomic success consumption. */
|
|
6
|
+
Plugin = 'plugin',
|
|
7
|
+
}
|
|
@@ -3,9 +3,15 @@ import type { AuthModel } from '../types.js'
|
|
|
3
3
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
4
4
|
import type { RedisResource } from '@owlmeans/redis-resource'
|
|
5
5
|
import type { ResourceRecord } from '@owlmeans/resource'
|
|
6
|
+
import type { AuthChallengeReplayPolicy } from './replay-policy.js'
|
|
6
7
|
|
|
7
8
|
export interface AuthPlugin extends Omit<AuthModel, "rely"> {
|
|
8
9
|
type: string
|
|
10
|
+
/**
|
|
11
|
+
* Who atomically consumes a verified challenge. The manager remains the default; a plugin may
|
|
12
|
+
* opt in only when its own credential protocol needs a bounded retry budget before consumption.
|
|
13
|
+
*/
|
|
14
|
+
challengeReplayPolicy?: AuthChallengeReplayPolicy
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export interface RecpatchaResponse {
|
package/src/service.ts
CHANGED
|
@@ -5,11 +5,21 @@ import type { AuthServiceAppend, AuthService, AuthSpent } from './types.js'
|
|
|
5
5
|
import { assertContext, createService } from '@owlmeans/context'
|
|
6
6
|
import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
|
|
7
7
|
import type { Auth, AuthCredentials } from '@owlmeans/auth'
|
|
8
|
-
import { AuthenFailed, AuthorizationError, AuthroizationType } from '@owlmeans/auth'
|
|
8
|
+
import { AuthenFailed, AuthorizationError, AuthroizationType, AuthUnavailable } from '@owlmeans/auth'
|
|
9
9
|
import type { AbstractRequest, AbstractResponse } from '@owlmeans/entrypoint'
|
|
10
10
|
import type { Resource } from '@owlmeans/resource'
|
|
11
11
|
import { createStaticResource } from '@owlmeans/static-resource'
|
|
12
12
|
import { trust, extractAuthToken } from '@owlmeans/auth-common/utils'
|
|
13
|
+
import { ENTITY_RESOLVER } from '@owlmeans/auth-common'
|
|
14
|
+
import type { EntityResolverService } from '@owlmeans/auth-common'
|
|
15
|
+
import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
|
|
16
|
+
import type { IdentityProfile, IdentityProfileResource } from '@owlmeans/server-auth-identity'
|
|
17
|
+
import {
|
|
18
|
+
appendMemoryAuthSessionManager, AUTH_SESSION_MANAGER, AUTH_SESSION_TTL
|
|
19
|
+
} from '@owlmeans/server-auth-session'
|
|
20
|
+
import type { AuthSessionManager } from '@owlmeans/server-auth-session'
|
|
21
|
+
import { uuid } from '@owlmeans/basic-ids'
|
|
22
|
+
import { TOKEN_UPDATE } from '@owlmeans/auth-common'
|
|
13
23
|
|
|
14
24
|
type Config = ServerConfig
|
|
15
25
|
type Context = ServerContext<Config>
|
|
@@ -20,6 +30,50 @@ export const makeAuthService = (alias: string = DEFAULT_ALIAS): AuthService => {
|
|
|
20
30
|
const cache = (context: Context): Resource<AuthSpent> =>
|
|
21
31
|
context.resource<Resource<AuthSpent>>(AUTH_CACHE)
|
|
22
32
|
|
|
33
|
+
const sessions = (context: Context): AuthSessionManager | null =>
|
|
34
|
+
context.hasService(AUTH_SESSION_MANAGER)
|
|
35
|
+
? context.service<AuthSessionManager>(AUTH_SESSION_MANAGER)
|
|
36
|
+
: null
|
|
37
|
+
|
|
38
|
+
const entityIdFor = async (context: Context, entitySlug: string | undefined): Promise<string | null> => {
|
|
39
|
+
if (entitySlug == null || entitySlug === '') return null
|
|
40
|
+
if (!context.hasService(ENTITY_RESOLVER)) return entitySlug
|
|
41
|
+
const entity = await context.service<EntityResolverService>(ENTITY_RESOLVER).resolve(entitySlug)
|
|
42
|
+
return entity?.id ?? null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const refresh = async (context: Context, auth: Auth, version: number): Promise<Auth | null> => {
|
|
46
|
+
const entityId = await entityIdFor(context, auth.entitySlug)
|
|
47
|
+
if (entityId == null || auth.profileId == null || !context.hasResource(AUTH_IDENTITY_PROFILE)) return null
|
|
48
|
+
const profiles = context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
|
|
49
|
+
let profile: IdentityProfile | null = null
|
|
50
|
+
try {
|
|
51
|
+
profile = await profiles.load({ entityId, profileId: auth.profileId })
|
|
52
|
+
} catch {
|
|
53
|
+
// A missing profile is a definitive authentication refusal; a failed profile store is an
|
|
54
|
+
// availability dependency and must not look like a revoked browser session.
|
|
55
|
+
throw new AuthUnavailable('identity-profile')
|
|
56
|
+
}
|
|
57
|
+
if (profile == null || (profile.expiresAt != null && new Date(profile.expiresAt).getTime() <= Date.now())) return null
|
|
58
|
+
return {
|
|
59
|
+
...auth,
|
|
60
|
+
userId: profile.userId ?? auth.userId,
|
|
61
|
+
role: profile.role,
|
|
62
|
+
scopes: profile.scopes ?? [],
|
|
63
|
+
...(profile.permissions != null ? { permissions: profile.permissions } : {}),
|
|
64
|
+
authorizationVersion: version,
|
|
65
|
+
createdAt: new Date()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sign = async (context: Context, auth: Auth): Promise<string> => {
|
|
70
|
+
const trusted = await trust<Config, Context>(context, TRUSTED, context.cfg.alias ?? context.cfg.service)
|
|
71
|
+
const ttl = auth.expiresAt == null ? AUTH_SESSION_TTL : Math.max(0, new Date(auth.expiresAt).getTime() - Date.now())
|
|
72
|
+
const authorization = await makeEnvelopeModel<Auth>(AuthroizationType.Ed25519BasicToken)
|
|
73
|
+
.send(auth, ttl).sign(trusted.key, EnvelopeKind.Token)
|
|
74
|
+
return `${AuthroizationType.Ed25519BasicToken.toUpperCase()} ${authorization}`
|
|
75
|
+
}
|
|
76
|
+
|
|
23
77
|
const service: AuthService = createService<AuthService>(alias, {
|
|
24
78
|
match: async req => extractAuthToken(req, AuthroizationType.Ed25519BasicToken) != null,
|
|
25
79
|
|
|
@@ -37,7 +91,29 @@ export const makeAuthService = (alias: string = DEFAULT_ALIAS): AuthService => {
|
|
|
37
91
|
return false as T
|
|
38
92
|
}
|
|
39
93
|
|
|
40
|
-
|
|
94
|
+
let auth = envelope.message()
|
|
95
|
+
const manager = sessions(context)
|
|
96
|
+
if (manager != null) {
|
|
97
|
+
if (auth.sessionId == null) return false as T
|
|
98
|
+
let decision
|
|
99
|
+
try {
|
|
100
|
+
decision = await manager.inspect(auth.sessionId)
|
|
101
|
+
} catch {
|
|
102
|
+
// Registry availability is a security dependency. A guard cannot authenticate through it.
|
|
103
|
+
throw new AuthUnavailable('session-registry')
|
|
104
|
+
}
|
|
105
|
+
if (decision.state !== 'active' && decision.state !== 'refresh') return false as T
|
|
106
|
+
if (decision.state === 'refresh') {
|
|
107
|
+
const refreshed = await refresh(context, auth, decision.version)
|
|
108
|
+
if (refreshed == null) return false as T
|
|
109
|
+
auth = refreshed
|
|
110
|
+
const replacement = await sign(context, auth)
|
|
111
|
+
res.responseProvider?.header(TOKEN_UPDATE, replacement)
|
|
112
|
+
req.headers = { ...req.headers, authorization: replacement }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
res.resolve(auth)
|
|
41
117
|
|
|
42
118
|
return true as T
|
|
43
119
|
},
|
|
@@ -51,8 +127,19 @@ export const makeAuthService = (alias: string = DEFAULT_ALIAS): AuthService => {
|
|
|
51
127
|
if (!await envelope.verify(trusted.key)) {
|
|
52
128
|
throw new AuthorizationError('unpack')
|
|
53
129
|
}
|
|
54
|
-
|
|
55
|
-
|
|
130
|
+
const auth = envelope.message()
|
|
131
|
+
const manager = sessions(context)
|
|
132
|
+
if (manager != null) {
|
|
133
|
+
if (auth.sessionId == null) throw new AuthorizationError('session')
|
|
134
|
+
let decision
|
|
135
|
+
try {
|
|
136
|
+
decision = await manager.inspect(auth.sessionId)
|
|
137
|
+
} catch {
|
|
138
|
+
throw new AuthUnavailable('session-registry')
|
|
139
|
+
}
|
|
140
|
+
if (decision.state !== 'active') throw new AuthorizationError('session')
|
|
141
|
+
}
|
|
142
|
+
return auth
|
|
56
143
|
},
|
|
57
144
|
|
|
58
145
|
authenticated: async () => {
|
|
@@ -84,6 +171,8 @@ export const makeAuthService = (alias: string = DEFAULT_ALIAS): AuthService => {
|
|
|
84
171
|
}
|
|
85
172
|
|
|
86
173
|
// @TODO Move to a plugin
|
|
174
|
+
const issuedAt = Date.now()
|
|
175
|
+
const expiresAt = new Date(issuedAt + AUTH_SESSION_TTL)
|
|
87
176
|
const auth: Auth = {
|
|
88
177
|
token: credentials.challenge,
|
|
89
178
|
userId: credentials.userId,
|
|
@@ -94,14 +183,24 @@ export const makeAuthService = (alias: string = DEFAULT_ALIAS): AuthService => {
|
|
|
94
183
|
profileId: credentials.profileId,
|
|
95
184
|
entitySlug: credentials.entitySlug,
|
|
96
185
|
isUser: true,
|
|
97
|
-
createdAt: new Date()
|
|
186
|
+
createdAt: new Date(issuedAt),
|
|
187
|
+
expiresAt,
|
|
188
|
+
sessionId: uuid(),
|
|
189
|
+
authorizationVersion: 1
|
|
190
|
+
}
|
|
191
|
+
const manager = sessions(context)
|
|
192
|
+
if (manager != null) {
|
|
193
|
+
const entityId = await entityIdFor(context, auth.entitySlug)
|
|
194
|
+
if (entityId == null || auth.profileId == null) throw new AuthenFailed('session')
|
|
195
|
+
const decision = await manager.register({
|
|
196
|
+
id: auth.sessionId!, kind: 'bearer', entityId, profileId: auth.profileId,
|
|
197
|
+
issuedAt, expiresAt: expiresAt.getTime()
|
|
198
|
+
})
|
|
199
|
+
if (decision.state !== 'active') throw new AuthenFailed('session')
|
|
200
|
+
auth.authorizationVersion = decision.version
|
|
98
201
|
}
|
|
99
202
|
|
|
100
|
-
|
|
101
|
-
const authorization = await makeEnvelopeModel<Auth>(AuthroizationType.Ed25519BasicToken)
|
|
102
|
-
.send(auth, null).sign(trusted.key, EnvelopeKind.Token)
|
|
103
|
-
|
|
104
|
-
return { token: `${AuthroizationType.Ed25519BasicToken.toUpperCase()} ${authorization}` }
|
|
203
|
+
return { token: await sign(context, auth) }
|
|
105
204
|
}
|
|
106
205
|
})
|
|
107
206
|
|
|
@@ -116,6 +215,10 @@ export const appendAuthService = <C extends Config, T extends ServerContext<C>>(
|
|
|
116
215
|
ctx.registerResource(createStaticResource(AUTH_CACHE))
|
|
117
216
|
}
|
|
118
217
|
|
|
218
|
+
// Every standalone server gets an expiring process-local registry. Scaled deployments replace
|
|
219
|
+
// it with appendRedisAuthSessionManager before calling this helper.
|
|
220
|
+
appendMemoryAuthSessionManager(ctx)
|
|
221
|
+
|
|
119
222
|
const service = makeAuthService(alias)
|
|
120
223
|
const context = ctx as T & AuthServiceAppend
|
|
121
224
|
|
package/tests/context.ts
CHANGED
|
@@ -25,6 +25,8 @@ export const makeTestContext = () => {
|
|
|
25
25
|
id: authServiceKP.exportAddress(),
|
|
26
26
|
name: AUTH_SRV_KEY,
|
|
27
27
|
credential: authServiceKP.exportPublic(),
|
|
28
|
+
// Local, deterministic test-only key: the manager signs its synthetic exchange envelope.
|
|
29
|
+
secret: authServiceKP.export(),
|
|
28
30
|
scopes: ['*'],
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AuthenFailed, AuthRole } from '@owlmeans/auth'
|
|
3
|
+
import { makeAuthModel } from '../src/manager/model.js'
|
|
4
|
+
import { AuthChallengeReplayPolicy } from '../src/manager/plugins/replay-policy.js'
|
|
5
|
+
import { registerPlugin } from '../src/manager/plugins/index.js'
|
|
6
|
+
import { makeTestContext } from './context.js'
|
|
7
|
+
|
|
8
|
+
describe('@owlmeans/server-auth — plugin-owned challenge replay', () => {
|
|
9
|
+
test('lets a bounded-retry plugin receive a second credential attempt', async () => {
|
|
10
|
+
const type = `replay-policy-${Date.now()}`
|
|
11
|
+
let attempts = 0
|
|
12
|
+
registerPlugin(type, () => ({
|
|
13
|
+
type,
|
|
14
|
+
challengeReplayPolicy: AuthChallengeReplayPolicy.Plugin,
|
|
15
|
+
init: async () => ({ challenge: 'opaque-issuance' }),
|
|
16
|
+
authenticate: async () => {
|
|
17
|
+
attempts += 1
|
|
18
|
+
if (attempts === 1) throw new AuthenFailed('first-guess')
|
|
19
|
+
return { token: '' }
|
|
20
|
+
},
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
const { context } = makeTestContext()
|
|
24
|
+
await context.configure().init()
|
|
25
|
+
const model = makeAuthModel(context as never)
|
|
26
|
+
const { challenge } = await model.init({ type, userId: 'retry@example.com' })
|
|
27
|
+
const credential = () => ({
|
|
28
|
+
type, challenge, credential: 'guess', userId: 'retry@example.com',
|
|
29
|
+
role: AuthRole.User, scopes: ['*'],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
await expect(model.authenticate(credential())).rejects.toBeInstanceOf(AuthenFailed)
|
|
33
|
+
await expect(model.authenticate(credential())).resolves.toHaveProperty('token')
|
|
34
|
+
expect(attempts).toBe(2)
|
|
35
|
+
})
|
|
36
|
+
})
|