@owlmeans/server-auth 0.1.10 → 0.1.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/instructions/server-auth.instructions.md +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth/SKILL.md +1 -1
- package/build/manager/index.d.ts +1 -0
- package/build/manager/index.d.ts.map +1 -1
- package/build/manager/index.js +1 -0
- package/build/manager/index.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/supervisor.d.ts +18 -0
- package/build/manager/plugins/supervisor.d.ts.map +1 -0
- package/build/manager/plugins/supervisor.js +75 -0
- package/build/manager/plugins/supervisor.js.map +1 -0
- package/build/manager/supervisor.d.ts +63 -0
- package/build/manager/supervisor.d.ts.map +1 -0
- package/build/manager/supervisor.js +46 -0
- package/build/manager/supervisor.js.map +1 -0
- package/package.json +29 -30
- package/src/manager/index.ts +1 -0
- package/src/manager/plugins/export.ts +1 -0
- package/src/manager/plugins/supervisor.ts +89 -0
- package/src/manager/supervisor.ts +111 -0
- package/tests/supervisor.spec.ts +108 -0
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@owlmeans/server-auth",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.14",
|
|
5
|
+
"generatedAt": "2026-08-05T16:56:53.374Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
package/build/manager/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA"}
|
package/build/manager/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/manager/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA"}
|
|
@@ -2,6 +2,7 @@ export type * from './types.js';
|
|
|
2
2
|
export * from './basic-ed25519.js';
|
|
3
3
|
export * from './re-captcha.js';
|
|
4
4
|
export * from './basic-rely.js';
|
|
5
|
+
export * from './supervisor.js';
|
|
5
6
|
export * from './utils.js';
|
|
6
7
|
export { plugins, registerPlugin } from './index.js';
|
|
7
8
|
//# sourceMappingURL=export.d.ts.map
|
|
@@ -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,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,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,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,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,18 @@
|
|
|
1
|
+
import type { AuthPlugin } from './types.js';
|
|
2
|
+
import type { AppConfig, AppContext } from '../types.js';
|
|
3
|
+
import type { SupervisorPluginOptions } from '../supervisor.js';
|
|
4
|
+
/**
|
|
5
|
+
* PK-based supervisor authentication plugin. A privileged operator (or an e2e
|
|
6
|
+
* test) holds one of the project's trusted private keys (the allowlisted
|
|
7
|
+
* `supervisors`) and signs `buildSupervisorPayload` to mint a token for an
|
|
8
|
+
* arbitrary user id / email - bypassing external IdPs. Development-only by
|
|
9
|
+
* design (see `appendSupervisorAuth`).
|
|
10
|
+
*
|
|
11
|
+
* The plugin issues NO token itself: it only verifies the supervisor signature
|
|
12
|
+
* and resolves/registers the target user, then hands the mutated credential back
|
|
13
|
+
* to the existing `makeAuthModel` flow which signs the credential envelope, and
|
|
14
|
+
* to the project's auth service which exchanges it for the final
|
|
15
|
+
* `Ed25519BasicToken` bearer.
|
|
16
|
+
*/
|
|
17
|
+
export declare const makeSupervisorPlugin: (context: AppContext<AppConfig>, opts: SupervisorPluginOptions) => AuthPlugin;
|
|
18
|
+
//# sourceMappingURL=supervisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../../src/manager/plugins/supervisor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAM5C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAE/D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAS,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,uBAAuB,KAC5D,UA8DF,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AuthenticationType, AuthenFailed, AuthRole, ALL_SCOPES, buildSupervisorPayload } from '@owlmeans/auth';
|
|
2
|
+
import { base64 } from '@scure/base';
|
|
3
|
+
import { randomBytes } from '@noble/hashes/utils';
|
|
4
|
+
import { fromPubKey } from '@owlmeans/basic-keys';
|
|
5
|
+
import { TRUSTED } from '@owlmeans/config';
|
|
6
|
+
/**
|
|
7
|
+
* PK-based supervisor authentication plugin. A privileged operator (or an e2e
|
|
8
|
+
* test) holds one of the project's trusted private keys (the allowlisted
|
|
9
|
+
* `supervisors`) and signs `buildSupervisorPayload` to mint a token for an
|
|
10
|
+
* arbitrary user id / email - bypassing external IdPs. Development-only by
|
|
11
|
+
* design (see `appendSupervisorAuth`).
|
|
12
|
+
*
|
|
13
|
+
* The plugin issues NO token itself: it only verifies the supervisor signature
|
|
14
|
+
* and resolves/registers the target user, then hands the mutated credential back
|
|
15
|
+
* to the existing `makeAuthModel` flow which signs the credential envelope, and
|
|
16
|
+
* to the project's auth service which exchanges it for the final
|
|
17
|
+
* `Ed25519BasicToken` bearer.
|
|
18
|
+
*/
|
|
19
|
+
export const makeSupervisorPlugin = (context, opts) => {
|
|
20
|
+
const plugin = {
|
|
21
|
+
type: AuthenticationType.Supervisor,
|
|
22
|
+
init: async () => ({ challenge: base64.encode(randomBytes(32)) }),
|
|
23
|
+
authenticate: async (credential) => {
|
|
24
|
+
// `credential.challenge` has already been replaced by the model with the
|
|
25
|
+
// unwrapped, single-use server challenge (`msg`).
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(credential.credential);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new AuthenFailed('supervisor:credential');
|
|
32
|
+
}
|
|
33
|
+
if (parsed?.signature == null || parsed?.salt == null || credential.userId == null) {
|
|
34
|
+
throw new AuthenFailed('supervisor:payload');
|
|
35
|
+
}
|
|
36
|
+
const payload = buildSupervisorPayload(credential.challenge, credential.userId, parsed.salt);
|
|
37
|
+
let matched = null;
|
|
38
|
+
for (const alias of opts.supervisors) {
|
|
39
|
+
try {
|
|
40
|
+
const record = await context.getConfigResource(TRUSTED)
|
|
41
|
+
.load(alias, 'name');
|
|
42
|
+
if (record?.credential == null) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (await fromPubKey(record.credential).verify(payload, parsed.signature)) {
|
|
46
|
+
matched = alias;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// unknown trusted alias or verification error - try the next supervisor
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (matched == null) {
|
|
55
|
+
throw new AuthenFailed('supervisor:signature');
|
|
56
|
+
}
|
|
57
|
+
const resolution = opts.resolveUser != null
|
|
58
|
+
? await opts.resolveUser(credential.userId, context, { register: opts.allowRegistration })
|
|
59
|
+
: { userId: credential.userId };
|
|
60
|
+
credential.userId = resolution.userId;
|
|
61
|
+
credential.profileId = resolution.profileId ?? resolution.userId;
|
|
62
|
+
if (resolution.entityId != null) {
|
|
63
|
+
credential.entityId = resolution.entityId;
|
|
64
|
+
}
|
|
65
|
+
credential.scopes = resolution.scopes ?? credential.scopes ?? [ALL_SCOPES];
|
|
66
|
+
credential.role = resolution.role ?? AuthRole.User;
|
|
67
|
+
const token = base64.encode(randomBytes(32));
|
|
68
|
+
credential.challenge = token;
|
|
69
|
+
credential.type = AuthenticationType.OneTimeToken;
|
|
70
|
+
return { token };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return plugin;
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../../src/manager/plugins/supervisor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AuthPayload } from '@owlmeans/auth';
|
|
2
|
+
import type { AppConfig, AppContext } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a `SupervisorUserResolver` returns: the identity the supervisor-minted
|
|
5
|
+
* token will represent. Only `userId` is required - the rest default sensibly.
|
|
6
|
+
*/
|
|
7
|
+
export interface SupervisorUserResolution extends Partial<Pick<AuthPayload, 'profileId' | 'entityId' | 'role' | 'scopes'>> {
|
|
8
|
+
userId: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Find-or-create the target identity for a supervisor login. `register` reflects
|
|
12
|
+
* `allowRegistration`; when false the resolver should only look existing users up.
|
|
13
|
+
* Wire this to the project's identity store (e.g. `@owlmeans/server-auth-identity`).
|
|
14
|
+
*/
|
|
15
|
+
export interface SupervisorUserResolver {
|
|
16
|
+
<C extends AppConfig, T extends AppContext<C>>(userId: string, context: T, opts: {
|
|
17
|
+
register: boolean;
|
|
18
|
+
}): Promise<SupervisorUserResolution>;
|
|
19
|
+
}
|
|
20
|
+
/** Resolved options handed to the plugin factory. */
|
|
21
|
+
export interface SupervisorPluginOptions {
|
|
22
|
+
supervisors: string[];
|
|
23
|
+
allowRegistration: boolean;
|
|
24
|
+
resolveUser?: SupervisorUserResolver;
|
|
25
|
+
}
|
|
26
|
+
export interface SupervisorAuthOptions {
|
|
27
|
+
/** Trusted-record aliases authorized to act as supervisor. Default: master + superuser. */
|
|
28
|
+
supervisors?: string[];
|
|
29
|
+
/** Find-or-create the target user by id/email. Default: trust the id as-is. */
|
|
30
|
+
resolveUser?: SupervisorUserResolver;
|
|
31
|
+
/** Allow minting a token for an unknown user (registration). Default: true. */
|
|
32
|
+
allowRegistration?: boolean;
|
|
33
|
+
/** Force enable/disable. Default: development only (cfg.debug.all || cfg.debug.supervisor). */
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Also accept internal owlmeans `Ed25519BasicToken`s even when another guard
|
|
37
|
+
* (e.g. OIDC) is the primary guard on protected modules. Default: true.
|
|
38
|
+
*/
|
|
39
|
+
acceptInternalTokens?: boolean;
|
|
40
|
+
/** The internal-token guard to add as a coguard. Default: DEFAULT_GUARD ('auth'). */
|
|
41
|
+
guard?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Ensure the internal `Ed25519BasicToken` guard is accepted as a coguard on every
|
|
45
|
+
* already-guarded backend module, so internal owlmeans tokens keep working even
|
|
46
|
+
* when another guard (e.g. OIDC) is the primary guard. The primary guard stays
|
|
47
|
+
* first; the internal guard is appended as a fallback (its `match` only fires for
|
|
48
|
+
* an `Ed25519BasicToken` authorization header).
|
|
49
|
+
*/
|
|
50
|
+
export declare const setupInternalTokenCoguard: (modules: Array<{
|
|
51
|
+
guards?: string[];
|
|
52
|
+
}>, guard?: string) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Unified, explicit append for PK-based supervisor authentication. Call it once
|
|
55
|
+
* on the auth-manager server context. Development-only by default.
|
|
56
|
+
*
|
|
57
|
+
* It registers the supervisor auth plugin (verifies a front-end signature against
|
|
58
|
+
* the allowlisted trusted keys, then resolves/registers the target user) and -
|
|
59
|
+
* unless disabled - makes protected modules also accept internal owlmeans tokens
|
|
60
|
+
* (requirement: understand internal tokens even when OIDC is the primary guard).
|
|
61
|
+
*/
|
|
62
|
+
export declare const appendSupervisorAuth: <C extends AppConfig, T extends AppContext<C>>(context: T, opts?: SupervisorAuthOptions) => T;
|
|
63
|
+
//# sourceMappingURL=supervisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/manager/supervisor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAIvD;;;GAGG;AACH,MAAM,WAAW,wBAAyB,SAAQ,OAAO,CAAC,IAAI,CAAC,WAAW,EACxE,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;IAC9C,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,EAC3C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,GACtD,OAAO,CAAC,wBAAwB,CAAC,CAAA;CACrC;AAED,qDAAqD;AACrD,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,WAAW,CAAC,EAAE,sBAAsB,CAAA;CACrC;AAED,MAAM,WAAW,qBAAqB;IACpC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,+FAA+F;IAC/F,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAOD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,KAAK,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EAAE,QAAO,MAAsB,KACnE,IAMF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,EAC/E,SAAS,CAAC,EAAE,OAAO,qBAAqB,KACvC,CAyBF,CAAA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AuthenticationType } from '@owlmeans/auth';
|
|
2
|
+
import { DEFAULT_GUARD } from '@owlmeans/auth-common';
|
|
3
|
+
import { registerPlugin } from './plugins/index.js';
|
|
4
|
+
import { makeSupervisorPlugin } from './plugins/supervisor.js';
|
|
5
|
+
const DEFAULT_SUPERVISORS = ['master', 'superuser'];
|
|
6
|
+
const isDevelopment = (context) => context.cfg.debug?.all === true || context.cfg.debug?.supervisor === true;
|
|
7
|
+
/**
|
|
8
|
+
* Ensure the internal `Ed25519BasicToken` guard is accepted as a coguard on every
|
|
9
|
+
* already-guarded backend module, so internal owlmeans tokens keep working even
|
|
10
|
+
* when another guard (e.g. OIDC) is the primary guard. The primary guard stays
|
|
11
|
+
* first; the internal guard is appended as a fallback (its `match` only fires for
|
|
12
|
+
* an `Ed25519BasicToken` authorization header).
|
|
13
|
+
*/
|
|
14
|
+
export const setupInternalTokenCoguard = (modules, guard = DEFAULT_GUARD) => {
|
|
15
|
+
modules.forEach(module => {
|
|
16
|
+
if (module.guards != null && module.guards.length > 0 && !module.guards.includes(guard)) {
|
|
17
|
+
module.guards.push(guard);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Unified, explicit append for PK-based supervisor authentication. Call it once
|
|
23
|
+
* on the auth-manager server context. Development-only by default.
|
|
24
|
+
*
|
|
25
|
+
* It registers the supervisor auth plugin (verifies a front-end signature against
|
|
26
|
+
* the allowlisted trusted keys, then resolves/registers the target user) and -
|
|
27
|
+
* unless disabled - makes protected modules also accept internal owlmeans tokens
|
|
28
|
+
* (requirement: understand internal tokens even when OIDC is the primary guard).
|
|
29
|
+
*/
|
|
30
|
+
export const appendSupervisorAuth = (context, opts) => {
|
|
31
|
+
const enabled = opts?.enabled ?? isDevelopment(context);
|
|
32
|
+
if (!enabled) {
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
const resolved = {
|
|
36
|
+
supervisors: opts?.supervisors ?? DEFAULT_SUPERVISORS,
|
|
37
|
+
allowRegistration: opts?.allowRegistration ?? true,
|
|
38
|
+
resolveUser: opts?.resolveUser
|
|
39
|
+
};
|
|
40
|
+
registerPlugin(AuthenticationType.Supervisor, (ctx => makeSupervisorPlugin(ctx, resolved)));
|
|
41
|
+
if (opts?.acceptInternalTokens !== false) {
|
|
42
|
+
setupInternalTokenCoguard(context.entrypoints(), opts?.guard ?? DEFAULT_GUARD);
|
|
43
|
+
}
|
|
44
|
+
return context;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../src/manager/supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAErD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AA+C9D,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;AAEnD,MAAM,aAAa,GAAG,CAAC,OAAqE,EAAW,EAAE,CACvG,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;AAE3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,OAAqC,EAAE,QAAgB,aAAa,EAC9D,EAAE;IACR,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;QACvB,IAAI,MAAM,CAAC,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACxF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,OAAU,EAAE,IAA4B,EACrC,EAAE;IACL,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,CAAA;IACvD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,MAAM,QAAQ,GAA4B;QACxC,WAAW,EAAE,IAAI,EAAE,WAAW,IAAI,mBAAmB;QACrD,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,IAAI,IAAI;QAClD,WAAW,EAAE,IAAI,EAAE,WAAW;KAC/B,CAAA;IAED,cAAc,CACZ,kBAAkB,CAAC,UAAU,EAC7B,CAAC,GAAG,CAAC,EAAE,CAAC,oBAAoB,CAAC,GAAuC,EAAE,QAAQ,CAAC,CAAyC,CACzH,CAAA;IAED,IAAI,IAAI,EAAE,oBAAoB,KAAK,KAAK,EAAE,CAAC;QACzC,yBAAyB,CACvB,OAAO,CAAC,WAAW,EAA6C,EAChE,IAAI,EAAE,KAAK,IAAI,aAAa,CAC7B,CAAA;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,29 +37,29 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@noble/hashes": "^1.5.0",
|
|
40
|
-
"@owlmeans/api": "^0.1.
|
|
41
|
-
"@owlmeans/api-config-server": "^0.1.
|
|
42
|
-
"@owlmeans/auth": "^0.1.
|
|
43
|
-
"@owlmeans/auth-common": "^0.1.
|
|
44
|
-
"@owlmeans/basic-envelope": "^0.1.
|
|
45
|
-
"@owlmeans/basic-ids": "^0.1.
|
|
46
|
-
"@owlmeans/basic-keys": "^0.1.
|
|
47
|
-
"@owlmeans/client-config": "^0.1.
|
|
48
|
-
"@owlmeans/client-entrypoint": "^0.1.
|
|
49
|
-
"@owlmeans/config": "^0.1.
|
|
50
|
-
"@owlmeans/context": "^0.1.
|
|
51
|
-
"@owlmeans/kluster": "^0.1.
|
|
52
|
-
"@owlmeans/entrypoint": "^0.1.
|
|
53
|
-
"@owlmeans/redis-resource": "^0.1.
|
|
54
|
-
"@owlmeans/resource": "^0.1.
|
|
55
|
-
"@owlmeans/route": "^0.1.
|
|
56
|
-
"@owlmeans/server-api": "^0.1.
|
|
57
|
-
"@owlmeans/server-context": "^0.1.
|
|
58
|
-
"@owlmeans/server-entrypoint": "^0.1.
|
|
59
|
-
"@owlmeans/server-route": "^0.1.
|
|
60
|
-
"@owlmeans/server-socket": "^0.1.
|
|
61
|
-
"@owlmeans/socket": "^0.1.
|
|
62
|
-
"@owlmeans/static-resource": "^0.1.
|
|
40
|
+
"@owlmeans/api": "^0.1.14",
|
|
41
|
+
"@owlmeans/api-config-server": "^0.1.14",
|
|
42
|
+
"@owlmeans/auth": "^0.1.14",
|
|
43
|
+
"@owlmeans/auth-common": "^0.1.14",
|
|
44
|
+
"@owlmeans/basic-envelope": "^0.1.14",
|
|
45
|
+
"@owlmeans/basic-ids": "^0.1.14",
|
|
46
|
+
"@owlmeans/basic-keys": "^0.1.14",
|
|
47
|
+
"@owlmeans/client-config": "^0.1.14",
|
|
48
|
+
"@owlmeans/client-entrypoint": "^0.1.14",
|
|
49
|
+
"@owlmeans/config": "^0.1.14",
|
|
50
|
+
"@owlmeans/context": "^0.1.14",
|
|
51
|
+
"@owlmeans/kluster": "^0.1.14",
|
|
52
|
+
"@owlmeans/entrypoint": "^0.1.14",
|
|
53
|
+
"@owlmeans/redis-resource": "^0.1.14",
|
|
54
|
+
"@owlmeans/resource": "^0.1.14",
|
|
55
|
+
"@owlmeans/route": "^0.1.14",
|
|
56
|
+
"@owlmeans/server-api": "^0.1.14",
|
|
57
|
+
"@owlmeans/server-context": "^0.1.14",
|
|
58
|
+
"@owlmeans/server-entrypoint": "^0.1.14",
|
|
59
|
+
"@owlmeans/server-route": "^0.1.14",
|
|
60
|
+
"@owlmeans/server-socket": "^0.1.14",
|
|
61
|
+
"@owlmeans/socket": "^0.1.14",
|
|
62
|
+
"@owlmeans/static-resource": "^0.1.14",
|
|
63
63
|
"@scure/base": "^1.1.9"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
@@ -67,12 +67,11 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@owlmeans/dep-config": "workspace:*",
|
|
70
|
-
"@owlmeans/test-auth": "^0.1.
|
|
71
|
-
"@types/bun": "^1.3.
|
|
72
|
-
"@types/node": "^
|
|
73
|
-
"nodemon": "^3.1.
|
|
74
|
-
"
|
|
75
|
-
"typescript": "^6.0.2"
|
|
70
|
+
"@owlmeans/test-auth": "^0.1.14",
|
|
71
|
+
"@types/bun": "^1.3.14",
|
|
72
|
+
"@types/node": "^26.1.0",
|
|
73
|
+
"nodemon": "^3.1.14",
|
|
74
|
+
"typescript": "^6.0.3"
|
|
76
75
|
},
|
|
77
76
|
"publishConfig": {
|
|
78
77
|
"access": "public"
|
package/src/manager/index.ts
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AuthenticationType, AuthenFailed, AuthRole, ALL_SCOPES, buildSupervisorPayload } from '@owlmeans/auth'
|
|
2
|
+
import type { SupervisorCredentialPayload } from '@owlmeans/auth'
|
|
3
|
+
import type { AuthPlugin } from './types.js'
|
|
4
|
+
import { base64 } from '@scure/base'
|
|
5
|
+
import { randomBytes } from '@noble/hashes/utils'
|
|
6
|
+
import { fromPubKey } from '@owlmeans/basic-keys'
|
|
7
|
+
import { TRUSTED } from '@owlmeans/config'
|
|
8
|
+
import type { TrustedRecord } from '@owlmeans/auth-common'
|
|
9
|
+
import type { AppConfig, AppContext } from '../types.js'
|
|
10
|
+
import type { SupervisorPluginOptions } from '../supervisor.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* PK-based supervisor authentication plugin. A privileged operator (or an e2e
|
|
14
|
+
* test) holds one of the project's trusted private keys (the allowlisted
|
|
15
|
+
* `supervisors`) and signs `buildSupervisorPayload` to mint a token for an
|
|
16
|
+
* arbitrary user id / email - bypassing external IdPs. Development-only by
|
|
17
|
+
* design (see `appendSupervisorAuth`).
|
|
18
|
+
*
|
|
19
|
+
* The plugin issues NO token itself: it only verifies the supervisor signature
|
|
20
|
+
* and resolves/registers the target user, then hands the mutated credential back
|
|
21
|
+
* to the existing `makeAuthModel` flow which signs the credential envelope, and
|
|
22
|
+
* to the project's auth service which exchanges it for the final
|
|
23
|
+
* `Ed25519BasicToken` bearer.
|
|
24
|
+
*/
|
|
25
|
+
export const makeSupervisorPlugin = (
|
|
26
|
+
context: AppContext<AppConfig>, opts: SupervisorPluginOptions
|
|
27
|
+
): AuthPlugin => {
|
|
28
|
+
const plugin: AuthPlugin = {
|
|
29
|
+
type: AuthenticationType.Supervisor,
|
|
30
|
+
|
|
31
|
+
init: async () => ({ challenge: base64.encode(randomBytes(32)) }),
|
|
32
|
+
|
|
33
|
+
authenticate: async credential => {
|
|
34
|
+
// `credential.challenge` has already been replaced by the model with the
|
|
35
|
+
// unwrapped, single-use server challenge (`msg`).
|
|
36
|
+
let parsed: SupervisorCredentialPayload
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(credential.credential) as SupervisorCredentialPayload
|
|
39
|
+
} catch {
|
|
40
|
+
throw new AuthenFailed('supervisor:credential')
|
|
41
|
+
}
|
|
42
|
+
if (parsed?.signature == null || parsed?.salt == null || credential.userId == null) {
|
|
43
|
+
throw new AuthenFailed('supervisor:payload')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const payload = buildSupervisorPayload(credential.challenge, credential.userId, parsed.salt)
|
|
47
|
+
|
|
48
|
+
let matched: string | null = null
|
|
49
|
+
for (const alias of opts.supervisors) {
|
|
50
|
+
try {
|
|
51
|
+
const record = await context.getConfigResource(TRUSTED)
|
|
52
|
+
.load<TrustedRecord>(alias, 'name')
|
|
53
|
+
if (record?.credential == null) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (await fromPubKey(record.credential).verify(payload, parsed.signature)) {
|
|
57
|
+
matched = alias
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// unknown trusted alias or verification error - try the next supervisor
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (matched == null) {
|
|
65
|
+
throw new AuthenFailed('supervisor:signature')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolution = opts.resolveUser != null
|
|
69
|
+
? await opts.resolveUser(credential.userId, context, { register: opts.allowRegistration })
|
|
70
|
+
: { userId: credential.userId }
|
|
71
|
+
|
|
72
|
+
credential.userId = resolution.userId
|
|
73
|
+
credential.profileId = resolution.profileId ?? resolution.userId
|
|
74
|
+
if (resolution.entityId != null) {
|
|
75
|
+
credential.entityId = resolution.entityId
|
|
76
|
+
}
|
|
77
|
+
credential.scopes = resolution.scopes ?? credential.scopes ?? [ALL_SCOPES]
|
|
78
|
+
credential.role = resolution.role ?? AuthRole.User
|
|
79
|
+
|
|
80
|
+
const token = base64.encode(randomBytes(32))
|
|
81
|
+
credential.challenge = token
|
|
82
|
+
credential.type = AuthenticationType.OneTimeToken
|
|
83
|
+
|
|
84
|
+
return { token }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return plugin
|
|
89
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { AuthenticationType } from '@owlmeans/auth'
|
|
2
|
+
import type { AuthPayload } from '@owlmeans/auth'
|
|
3
|
+
import { DEFAULT_GUARD } from '@owlmeans/auth-common'
|
|
4
|
+
import type { AppConfig, AppContext } from './types.js'
|
|
5
|
+
import { registerPlugin } from './plugins/index.js'
|
|
6
|
+
import { makeSupervisorPlugin } from './plugins/supervisor.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What a `SupervisorUserResolver` returns: the identity the supervisor-minted
|
|
10
|
+
* token will represent. Only `userId` is required - the rest default sensibly.
|
|
11
|
+
*/
|
|
12
|
+
export interface SupervisorUserResolution extends Partial<Pick<AuthPayload,
|
|
13
|
+
'profileId' | 'entityId' | 'role' | 'scopes'>> {
|
|
14
|
+
userId: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find-or-create the target identity for a supervisor login. `register` reflects
|
|
19
|
+
* `allowRegistration`; when false the resolver should only look existing users up.
|
|
20
|
+
* Wire this to the project's identity store (e.g. `@owlmeans/server-auth-identity`).
|
|
21
|
+
*/
|
|
22
|
+
export interface SupervisorUserResolver {
|
|
23
|
+
<C extends AppConfig, T extends AppContext<C>>(
|
|
24
|
+
userId: string, context: T, opts: { register: boolean }
|
|
25
|
+
): Promise<SupervisorUserResolution>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolved options handed to the plugin factory. */
|
|
29
|
+
export interface SupervisorPluginOptions {
|
|
30
|
+
supervisors: string[]
|
|
31
|
+
allowRegistration: boolean
|
|
32
|
+
resolveUser?: SupervisorUserResolver
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SupervisorAuthOptions {
|
|
36
|
+
/** Trusted-record aliases authorized to act as supervisor. Default: master + superuser. */
|
|
37
|
+
supervisors?: string[]
|
|
38
|
+
/** Find-or-create the target user by id/email. Default: trust the id as-is. */
|
|
39
|
+
resolveUser?: SupervisorUserResolver
|
|
40
|
+
/** Allow minting a token for an unknown user (registration). Default: true. */
|
|
41
|
+
allowRegistration?: boolean
|
|
42
|
+
/** Force enable/disable. Default: development only (cfg.debug.all || cfg.debug.supervisor). */
|
|
43
|
+
enabled?: boolean
|
|
44
|
+
/**
|
|
45
|
+
* Also accept internal owlmeans `Ed25519BasicToken`s even when another guard
|
|
46
|
+
* (e.g. OIDC) is the primary guard on protected modules. Default: true.
|
|
47
|
+
*/
|
|
48
|
+
acceptInternalTokens?: boolean
|
|
49
|
+
/** The internal-token guard to add as a coguard. Default: DEFAULT_GUARD ('auth'). */
|
|
50
|
+
guard?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_SUPERVISORS = ['master', 'superuser']
|
|
54
|
+
|
|
55
|
+
const isDevelopment = (context: { cfg: { debug?: { all?: boolean, supervisor?: boolean } } }): boolean =>
|
|
56
|
+
context.cfg.debug?.all === true || context.cfg.debug?.supervisor === true
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ensure the internal `Ed25519BasicToken` guard is accepted as a coguard on every
|
|
60
|
+
* already-guarded backend module, so internal owlmeans tokens keep working even
|
|
61
|
+
* when another guard (e.g. OIDC) is the primary guard. The primary guard stays
|
|
62
|
+
* first; the internal guard is appended as a fallback (its `match` only fires for
|
|
63
|
+
* an `Ed25519BasicToken` authorization header).
|
|
64
|
+
*/
|
|
65
|
+
export const setupInternalTokenCoguard = (
|
|
66
|
+
modules: Array<{ guards?: string[] }>, guard: string = DEFAULT_GUARD
|
|
67
|
+
): void => {
|
|
68
|
+
modules.forEach(module => {
|
|
69
|
+
if (module.guards != null && module.guards.length > 0 && !module.guards.includes(guard)) {
|
|
70
|
+
module.guards.push(guard)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Unified, explicit append for PK-based supervisor authentication. Call it once
|
|
77
|
+
* on the auth-manager server context. Development-only by default.
|
|
78
|
+
*
|
|
79
|
+
* It registers the supervisor auth plugin (verifies a front-end signature against
|
|
80
|
+
* the allowlisted trusted keys, then resolves/registers the target user) and -
|
|
81
|
+
* unless disabled - makes protected modules also accept internal owlmeans tokens
|
|
82
|
+
* (requirement: understand internal tokens even when OIDC is the primary guard).
|
|
83
|
+
*/
|
|
84
|
+
export const appendSupervisorAuth = <C extends AppConfig, T extends AppContext<C>>(
|
|
85
|
+
context: T, opts?: SupervisorAuthOptions
|
|
86
|
+
): T => {
|
|
87
|
+
const enabled = opts?.enabled ?? isDevelopment(context)
|
|
88
|
+
if (!enabled) {
|
|
89
|
+
return context
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const resolved: SupervisorPluginOptions = {
|
|
93
|
+
supervisors: opts?.supervisors ?? DEFAULT_SUPERVISORS,
|
|
94
|
+
allowRegistration: opts?.allowRegistration ?? true,
|
|
95
|
+
resolveUser: opts?.resolveUser
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
registerPlugin(
|
|
99
|
+
AuthenticationType.Supervisor,
|
|
100
|
+
(ctx => makeSupervisorPlugin(ctx as unknown as AppContext<AppConfig>, resolved)) as Parameters<typeof registerPlugin>[1]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if (opts?.acceptInternalTokens !== false) {
|
|
104
|
+
setupInternalTokenCoguard(
|
|
105
|
+
context.entrypoints() as unknown as Array<{ guards?: string[] }>,
|
|
106
|
+
opts?.guard ?? DEFAULT_GUARD
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return context
|
|
111
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { makeFixtureKeyPair } from '@owlmeans/test-auth'
|
|
3
|
+
import { AuthenticationType, AuthRole, buildSupervisorPayload } from '@owlmeans/auth'
|
|
4
|
+
import type { AuthCredentials } from '@owlmeans/auth'
|
|
5
|
+
import type { TrustedRecord } from '@owlmeans/auth-common'
|
|
6
|
+
import { makeSupervisorPlugin } from '../src/manager/plugins/supervisor.js'
|
|
7
|
+
import type { AppConfig, AppContext } from '../src/manager/types.js'
|
|
8
|
+
|
|
9
|
+
const SUPERVISOR = 'master'
|
|
10
|
+
|
|
11
|
+
const makeStubContext = (records: TrustedRecord[]): AppContext<AppConfig> => ({
|
|
12
|
+
getConfigResource: () => ({
|
|
13
|
+
load: async (id: string, field?: string) =>
|
|
14
|
+
records.find(r => (field === 'name' ? r.name === id : r.id === id)) ?? null
|
|
15
|
+
})
|
|
16
|
+
} as unknown as AppContext<AppConfig>)
|
|
17
|
+
|
|
18
|
+
const signCredential = async (
|
|
19
|
+
kp: ReturnType<typeof makeFixtureKeyPair>, challenge: string, userId: string, salt: string
|
|
20
|
+
): Promise<string> => {
|
|
21
|
+
const signature = await kp.sign(buildSupervisorPayload(challenge, userId, salt))
|
|
22
|
+
return JSON.stringify({ salt, signature })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const baseCredential = (challenge: string, userId: string, credential: string): AuthCredentials => ({
|
|
26
|
+
type: AuthenticationType.Supervisor,
|
|
27
|
+
challenge,
|
|
28
|
+
credential,
|
|
29
|
+
userId,
|
|
30
|
+
role: AuthRole.User,
|
|
31
|
+
scopes: ['*'],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('@owlmeans/server-auth — supervisor plugin', () => {
|
|
35
|
+
test('verifies an allowlisted supervisor signature and issues a one-time token', async () => {
|
|
36
|
+
const kp = makeFixtureKeyPair(SUPERVISOR)
|
|
37
|
+
const record: TrustedRecord = { id: kp.exportAddress(), name: SUPERVISOR, credential: kp.exportPublic(), scopes: ['*'] }
|
|
38
|
+
const context = makeStubContext([record])
|
|
39
|
+
|
|
40
|
+
const plugin = makeSupervisorPlugin(context, { supervisors: [SUPERVISOR], allowRegistration: true })
|
|
41
|
+
|
|
42
|
+
const challenge = 'challenge-' + Date.now()
|
|
43
|
+
const userId = 'newuser@example.com'
|
|
44
|
+
const credential = baseCredential(challenge, userId, await signCredential(kp, challenge, userId, 'salt-1'))
|
|
45
|
+
|
|
46
|
+
const result = await plugin.authenticate(credential)
|
|
47
|
+
|
|
48
|
+
expect(result.token).toBeString()
|
|
49
|
+
expect(result.token.length).toBeGreaterThan(0)
|
|
50
|
+
expect(credential.type).toBe(AuthenticationType.OneTimeToken)
|
|
51
|
+
expect(credential.challenge).toBe(result.token)
|
|
52
|
+
expect(credential.userId).toBe(userId)
|
|
53
|
+
expect(credential.role).toBe(AuthRole.User)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('applies the resolveUser mapping (registration / impersonation)', async () => {
|
|
57
|
+
const kp = makeFixtureKeyPair(SUPERVISOR)
|
|
58
|
+
const record: TrustedRecord = { id: kp.exportAddress(), name: SUPERVISOR, credential: kp.exportPublic(), scopes: ['*'] }
|
|
59
|
+
const context = makeStubContext([record])
|
|
60
|
+
|
|
61
|
+
const plugin = makeSupervisorPlugin(context, {
|
|
62
|
+
supervisors: [SUPERVISOR],
|
|
63
|
+
allowRegistration: true,
|
|
64
|
+
resolveUser: async (userId) => ({ userId: 'internal-id', profileId: 'profile-1', entityId: 'entity-1', scopes: ['project'] })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const challenge = 'challenge-' + Date.now()
|
|
68
|
+
const userId = 'someone@example.com'
|
|
69
|
+
const credential = baseCredential(challenge, userId, await signCredential(kp, challenge, userId, 'salt-2'))
|
|
70
|
+
|
|
71
|
+
await plugin.authenticate(credential)
|
|
72
|
+
|
|
73
|
+
expect(credential.userId).toBe('internal-id')
|
|
74
|
+
expect(credential.profileId).toBe('profile-1')
|
|
75
|
+
expect(credential.entityId).toBe('entity-1')
|
|
76
|
+
expect(credential.scopes).toEqual(['project'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('rejects a signature from a non-allowlisted key', async () => {
|
|
80
|
+
const supervisorKp = makeFixtureKeyPair(SUPERVISOR)
|
|
81
|
+
const attackerKp = makeFixtureKeyPair('attacker')
|
|
82
|
+
const record: TrustedRecord = { id: supervisorKp.exportAddress(), name: SUPERVISOR, credential: supervisorKp.exportPublic(), scopes: ['*'] }
|
|
83
|
+
const context = makeStubContext([record])
|
|
84
|
+
|
|
85
|
+
const plugin = makeSupervisorPlugin(context, { supervisors: [SUPERVISOR], allowRegistration: true })
|
|
86
|
+
|
|
87
|
+
const challenge = 'challenge-' + Date.now()
|
|
88
|
+
const userId = 'victim@example.com'
|
|
89
|
+
const credential = baseCredential(challenge, userId, await signCredential(attackerKp, challenge, userId, 'salt-3'))
|
|
90
|
+
|
|
91
|
+
await expect(plugin.authenticate(credential)).rejects.toThrow()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('rejects a signature bound to a different challenge (replay protection)', async () => {
|
|
95
|
+
const kp = makeFixtureKeyPair(SUPERVISOR)
|
|
96
|
+
const record: TrustedRecord = { id: kp.exportAddress(), name: SUPERVISOR, credential: kp.exportPublic(), scopes: ['*'] }
|
|
97
|
+
const context = makeStubContext([record])
|
|
98
|
+
|
|
99
|
+
const plugin = makeSupervisorPlugin(context, { supervisors: [SUPERVISOR], allowRegistration: true })
|
|
100
|
+
|
|
101
|
+
const userId = 'user@example.com'
|
|
102
|
+
// signature made for one challenge, presented with another
|
|
103
|
+
const credentialStr = await signCredential(kp, 'original-challenge', userId, 'salt-4')
|
|
104
|
+
const credential = baseCredential('different-challenge', userId, credentialStr)
|
|
105
|
+
|
|
106
|
+
await expect(plugin.authenticate(credential)).rejects.toThrow()
|
|
107
|
+
})
|
|
108
|
+
})
|