@jskit-ai/auth-core 0.1.99 → 0.1.100
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/package.descriptor.mjs +9 -4
- package/package.json +9 -2
- package/src/server/actions/auth.contributor.js +263 -0
- package/src/server/authActor.js +96 -0
- package/src/server/lib/index.js +12 -0
- package/src/server/providers/AuthActionsServiceProvider.js +67 -0
- package/src/server/providers/FastifyAuthPolicyServiceProvider.js +7 -6
- package/src/server/services/authSessionEventsService.js +30 -0
- package/src/server/unsupportedOperation.js +28 -0
- package/src/shared/authCapabilities.js +184 -0
- package/src/shared/authSecurityStatus.js +111 -0
- package/src/shared/commands/authCommandValidators.js +115 -0
- package/src/shared/index.js +12 -0
- package/test/authActions.test.js +174 -0
- package/test/authContract.test.js +171 -0
- package/test/commandValidators.test.js +5 -0
- package/test/providerRuntime.test.js +43 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { normalizeOAuthProviderId } from "./oauthProviders.js";
|
|
2
|
+
|
|
3
|
+
const AUTH_PASSWORD_RECOVERY_DELIVERIES = Object.freeze(["smtp", "dev-log", "dev-response", "disabled"]);
|
|
4
|
+
|
|
5
|
+
const AUTH_OPTIONAL_OPERATION_FEATURES = Object.freeze({
|
|
6
|
+
register: "password.register",
|
|
7
|
+
resendRegisterConfirmation: "emailConfirmation",
|
|
8
|
+
login: "password.login",
|
|
9
|
+
requestOtpLogin: "otp.login",
|
|
10
|
+
verifyOtpLogin: "otp.login",
|
|
11
|
+
oauthStart: "oauthLogin.enabled",
|
|
12
|
+
oauthComplete: "oauthLogin.enabled",
|
|
13
|
+
requestPasswordReset: "passwordRecovery.request",
|
|
14
|
+
completePasswordRecovery: "passwordRecovery.complete",
|
|
15
|
+
resetPassword: "passwordRecovery.complete",
|
|
16
|
+
changePassword: "password.change",
|
|
17
|
+
updateDisplayName: "profileUpdate",
|
|
18
|
+
getSecurityStatus: "securityStatus",
|
|
19
|
+
setPasswordSignInEnabled: "password.methodToggle",
|
|
20
|
+
startProviderLink: "providerLinking.start",
|
|
21
|
+
unlinkProvider: "providerLinking.unlink",
|
|
22
|
+
signOutOtherSessions: "signOutOtherSessions",
|
|
23
|
+
devLoginAs: "devLoginAs"
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function normalizeBoolean(value, fallback = false) {
|
|
27
|
+
if (typeof value === "boolean") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return fallback === true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeAuthProviderId(value, { fallback = "unknown" } = {}) {
|
|
34
|
+
const normalized = String(value || "")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase();
|
|
37
|
+
if (/^[a-z][a-z0-9_-]{1,63}$/.test(normalized)) {
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeProviderLabel(value, providerId) {
|
|
45
|
+
const label = String(value || "").trim();
|
|
46
|
+
if (label) {
|
|
47
|
+
return label;
|
|
48
|
+
}
|
|
49
|
+
return providerId.charAt(0).toUpperCase() + providerId.slice(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeOAuthProviderEntries(value) {
|
|
53
|
+
const source = Array.isArray(value) ? value : [];
|
|
54
|
+
const providers = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
|
|
57
|
+
for (const entry of source) {
|
|
58
|
+
const rawId = typeof entry === "string" ? entry : entry?.id;
|
|
59
|
+
const id = normalizeOAuthProviderId(rawId, { fallback: null });
|
|
60
|
+
if (!id || seen.has(id)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
seen.add(id);
|
|
64
|
+
providers.push(Object.freeze({
|
|
65
|
+
id,
|
|
66
|
+
label: String((typeof entry === "object" && entry ? entry.label : "") || id).trim() || id
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Object.freeze(providers);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeRecoveryDelivery(value, { request = false, complete = false } = {}) {
|
|
74
|
+
const normalized = String(value || "")
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase();
|
|
77
|
+
if (AUTH_PASSWORD_RECOVERY_DELIVERIES.includes(normalized)) {
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
return request || complete ? "disabled" : "disabled";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeAuthCapabilities(value = {}) {
|
|
84
|
+
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
85
|
+
const providerSource = source.provider && typeof source.provider === "object" ? source.provider : {};
|
|
86
|
+
const providerId = normalizeAuthProviderId(providerSource.id || source.providerId, { fallback: "unknown" });
|
|
87
|
+
const featuresSource = source.features && typeof source.features === "object" ? source.features : {};
|
|
88
|
+
|
|
89
|
+
const passwordSource = featuresSource.password && typeof featuresSource.password === "object"
|
|
90
|
+
? featuresSource.password
|
|
91
|
+
: {};
|
|
92
|
+
const recoverySource = featuresSource.passwordRecovery && typeof featuresSource.passwordRecovery === "object"
|
|
93
|
+
? featuresSource.passwordRecovery
|
|
94
|
+
: {};
|
|
95
|
+
const otpSource = featuresSource.otp && typeof featuresSource.otp === "object"
|
|
96
|
+
? featuresSource.otp
|
|
97
|
+
: {};
|
|
98
|
+
const oauthSource = featuresSource.oauthLogin && typeof featuresSource.oauthLogin === "object"
|
|
99
|
+
? featuresSource.oauthLogin
|
|
100
|
+
: {};
|
|
101
|
+
const providerLinkingSource = featuresSource.providerLinking && typeof featuresSource.providerLinking === "object"
|
|
102
|
+
? featuresSource.providerLinking
|
|
103
|
+
: {};
|
|
104
|
+
const oauthProviders = normalizeOAuthProviderEntries(oauthSource.providers);
|
|
105
|
+
const oauthDefaultProvider = normalizeOAuthProviderId(oauthSource.defaultProvider, { fallback: null });
|
|
106
|
+
const recoveryRequest = normalizeBoolean(recoverySource.request);
|
|
107
|
+
const recoveryComplete = normalizeBoolean(recoverySource.complete);
|
|
108
|
+
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
provider: Object.freeze({
|
|
111
|
+
id: providerId,
|
|
112
|
+
label: normalizeProviderLabel(providerSource.label, providerId)
|
|
113
|
+
}),
|
|
114
|
+
features: Object.freeze({
|
|
115
|
+
password: Object.freeze({
|
|
116
|
+
login: normalizeBoolean(passwordSource.login),
|
|
117
|
+
register: normalizeBoolean(passwordSource.register),
|
|
118
|
+
change: normalizeBoolean(passwordSource.change),
|
|
119
|
+
methodToggle: normalizeBoolean(passwordSource.methodToggle)
|
|
120
|
+
}),
|
|
121
|
+
passwordRecovery: Object.freeze({
|
|
122
|
+
request: recoveryRequest,
|
|
123
|
+
complete: recoveryComplete,
|
|
124
|
+
delivery: normalizeRecoveryDelivery(recoverySource.delivery, {
|
|
125
|
+
request: recoveryRequest,
|
|
126
|
+
complete: recoveryComplete
|
|
127
|
+
})
|
|
128
|
+
}),
|
|
129
|
+
otp: Object.freeze({
|
|
130
|
+
login: normalizeBoolean(otpSource.login)
|
|
131
|
+
}),
|
|
132
|
+
oauthLogin: Object.freeze({
|
|
133
|
+
enabled: normalizeBoolean(oauthSource.enabled) && oauthProviders.length > 0,
|
|
134
|
+
providers: oauthProviders,
|
|
135
|
+
defaultProvider: oauthProviders.some((provider) => provider.id === oauthDefaultProvider)
|
|
136
|
+
? oauthDefaultProvider
|
|
137
|
+
: null
|
|
138
|
+
}),
|
|
139
|
+
emailConfirmation: normalizeBoolean(featuresSource.emailConfirmation),
|
|
140
|
+
profileUpdate: normalizeBoolean(featuresSource.profileUpdate),
|
|
141
|
+
providerLinking: Object.freeze({
|
|
142
|
+
start: normalizeBoolean(providerLinkingSource.start),
|
|
143
|
+
unlink: normalizeBoolean(providerLinkingSource.unlink)
|
|
144
|
+
}),
|
|
145
|
+
securityStatus: normalizeBoolean(featuresSource.securityStatus),
|
|
146
|
+
signOutOtherSessions: normalizeBoolean(featuresSource.signOutOtherSessions),
|
|
147
|
+
appProfileProjection: normalizeBoolean(featuresSource.appProfileProjection),
|
|
148
|
+
devLoginAs: normalizeBoolean(featuresSource.devLoginAs)
|
|
149
|
+
})
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getCapabilityFeature(capabilities, path) {
|
|
154
|
+
const normalizedPath = String(path || "").trim();
|
|
155
|
+
if (!normalizedPath) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let current = normalizeAuthCapabilities(capabilities).features;
|
|
160
|
+
for (const segment of normalizedPath.split(".")) {
|
|
161
|
+
if (!current || typeof current !== "object" || !Object.hasOwn(current, segment)) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
current = current[segment];
|
|
165
|
+
}
|
|
166
|
+
return current;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isAuthOperationSupported(capabilities, operationName) {
|
|
170
|
+
const path = AUTH_OPTIONAL_OPERATION_FEATURES[String(operationName || "").trim()];
|
|
171
|
+
if (!path) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return getCapabilityFeature(capabilities, path) === true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
AUTH_PASSWORD_RECOVERY_DELIVERIES,
|
|
179
|
+
AUTH_OPTIONAL_OPERATION_FEATURES,
|
|
180
|
+
normalizeAuthProviderId,
|
|
181
|
+
normalizeAuthCapabilities,
|
|
182
|
+
getCapabilityFeature,
|
|
183
|
+
isAuthOperationSupported
|
|
184
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { AUTH_METHOD_MINIMUM_ENABLED } from "./authMethods.js";
|
|
2
|
+
|
|
3
|
+
function normalizeBoolean(value, fallback = false) {
|
|
4
|
+
return typeof value === "boolean" ? value : fallback === true;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function countEnabledMethods(methods) {
|
|
8
|
+
return methods.filter((method) => method?.enabled === true).length;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeAuthMethodStatus(entry) {
|
|
12
|
+
const source = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {};
|
|
13
|
+
const id = String(source.id || "").trim();
|
|
14
|
+
const kind = String(source.kind || "").trim();
|
|
15
|
+
if (!id || !kind) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const method = {
|
|
20
|
+
id,
|
|
21
|
+
kind,
|
|
22
|
+
configured: normalizeBoolean(source.configured),
|
|
23
|
+
enabled: normalizeBoolean(source.enabled),
|
|
24
|
+
canDisable: normalizeBoolean(source.canDisable)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (const key of [
|
|
28
|
+
"provider",
|
|
29
|
+
"label",
|
|
30
|
+
"canEnable",
|
|
31
|
+
"canUnlink",
|
|
32
|
+
"supportsSecretUpdate",
|
|
33
|
+
"requiresCurrentPassword"
|
|
34
|
+
]) {
|
|
35
|
+
if (Object.hasOwn(source, key)) {
|
|
36
|
+
method[key] = source[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Object.freeze(method);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeSecurityActions(value = {}) {
|
|
44
|
+
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
changePassword: normalizeBoolean(source.changePassword),
|
|
47
|
+
setPasswordEnabled: normalizeBoolean(source.setPasswordEnabled),
|
|
48
|
+
linkProvider: normalizeBoolean(source.linkProvider),
|
|
49
|
+
unlinkProvider: normalizeBoolean(source.unlinkProvider),
|
|
50
|
+
signOutOtherSessions: normalizeBoolean(source.signOutOtherSessions)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeAuthSecurityStatus(value = {}) {
|
|
55
|
+
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
56
|
+
const authMethods = (Array.isArray(source.authMethods) ? source.authMethods : [])
|
|
57
|
+
.map((entry) => normalizeAuthMethodStatus(entry))
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
const policySource =
|
|
60
|
+
source.policy && typeof source.policy === "object"
|
|
61
|
+
? source.policy
|
|
62
|
+
: source.authPolicy && typeof source.authPolicy === "object"
|
|
63
|
+
? source.authPolicy
|
|
64
|
+
: {};
|
|
65
|
+
const minimumEnabledMethods = Number.isInteger(Number(policySource.minimumEnabledMethods))
|
|
66
|
+
? Math.max(1, Number(policySource.minimumEnabledMethods))
|
|
67
|
+
: AUTH_METHOD_MINIMUM_ENABLED;
|
|
68
|
+
const enabledMethodsCount = Number.isInteger(Number(policySource.enabledMethodsCount))
|
|
69
|
+
? Math.max(0, Number(policySource.enabledMethodsCount))
|
|
70
|
+
: countEnabledMethods(authMethods);
|
|
71
|
+
const policy = Object.freeze({
|
|
72
|
+
minimumEnabledMethods,
|
|
73
|
+
enabledMethodsCount
|
|
74
|
+
});
|
|
75
|
+
const actions = normalizeSecurityActions(source.actions);
|
|
76
|
+
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
mfa: source.mfa && typeof source.mfa === "object"
|
|
79
|
+
? Object.freeze({
|
|
80
|
+
status: String(source.mfa.status || "not_enabled"),
|
|
81
|
+
enrolled: normalizeBoolean(source.mfa.enrolled),
|
|
82
|
+
methods: Object.freeze(Array.isArray(source.mfa.methods) ? [...source.mfa.methods] : [])
|
|
83
|
+
})
|
|
84
|
+
: Object.freeze({
|
|
85
|
+
status: "not_enabled",
|
|
86
|
+
enrolled: false,
|
|
87
|
+
methods: Object.freeze([])
|
|
88
|
+
}),
|
|
89
|
+
policy,
|
|
90
|
+
authPolicy: policy,
|
|
91
|
+
actions,
|
|
92
|
+
authMethods: Object.freeze(authMethods)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildSecurityStatusFromAuthMethodsStatus(authMethodsStatus, { actions = {} } = {}) {
|
|
97
|
+
const source = authMethodsStatus && typeof authMethodsStatus === "object" ? authMethodsStatus : {};
|
|
98
|
+
return normalizeAuthSecurityStatus({
|
|
99
|
+
authMethods: Array.isArray(source.methods) ? source.methods : [],
|
|
100
|
+
policy: {
|
|
101
|
+
minimumEnabledMethods: source.minimumEnabledMethods,
|
|
102
|
+
enabledMethodsCount: source.enabledMethodsCount
|
|
103
|
+
},
|
|
104
|
+
actions
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
normalizeAuthSecurityStatus,
|
|
110
|
+
buildSecurityStatusFromAuthMethodsStatus
|
|
111
|
+
};
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
AUTH_REFRESH_TOKEN_MAX_LENGTH
|
|
18
18
|
} from "../authConstraints.js";
|
|
19
19
|
import { AUTH_METHOD_IDS, AUTH_METHOD_KINDS } from "../authMethods.js";
|
|
20
|
+
import { AUTH_PASSWORD_RECOVERY_DELIVERIES } from "../authCapabilities.js";
|
|
20
21
|
import { OAUTH_PROVIDER_ID_PATTERN } from "../oauthProviders.js";
|
|
21
22
|
|
|
22
23
|
const oauthProviderFieldDefinition = deepFreeze({
|
|
@@ -230,6 +231,109 @@ const authDeniedOutputSchema = createSchema({
|
|
|
230
231
|
}
|
|
231
232
|
});
|
|
232
233
|
|
|
234
|
+
const authCapabilityProviderOutputSchema = createSchema({
|
|
235
|
+
id: {
|
|
236
|
+
type: "string",
|
|
237
|
+
required: true,
|
|
238
|
+
minLength: 2,
|
|
239
|
+
maxLength: 64,
|
|
240
|
+
pattern: "^[a-z][a-z0-9_-]{1,63}$"
|
|
241
|
+
},
|
|
242
|
+
label: {
|
|
243
|
+
type: "string",
|
|
244
|
+
required: true,
|
|
245
|
+
minLength: 1,
|
|
246
|
+
maxLength: 120
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const authPasswordCapabilitiesOutputSchema = createSchema({
|
|
251
|
+
login: { type: "boolean", required: true },
|
|
252
|
+
register: { type: "boolean", required: true },
|
|
253
|
+
change: { type: "boolean", required: true },
|
|
254
|
+
methodToggle: { type: "boolean", required: true }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const authPasswordRecoveryCapabilitiesOutputSchema = createSchema({
|
|
258
|
+
request: { type: "boolean", required: true },
|
|
259
|
+
complete: { type: "boolean", required: true },
|
|
260
|
+
delivery: {
|
|
261
|
+
type: "string",
|
|
262
|
+
required: true,
|
|
263
|
+
enum: AUTH_PASSWORD_RECOVERY_DELIVERIES
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const authOtpCapabilitiesOutputSchema = createSchema({
|
|
268
|
+
login: { type: "boolean", required: true }
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const authOAuthLoginCapabilitiesOutputSchema = createSchema({
|
|
272
|
+
enabled: { type: "boolean", required: true },
|
|
273
|
+
providers: {
|
|
274
|
+
type: "array",
|
|
275
|
+
required: true,
|
|
276
|
+
items: oauthProviderCatalogEntrySchema
|
|
277
|
+
},
|
|
278
|
+
defaultProvider: {
|
|
279
|
+
...oauthProviderFieldDefinition,
|
|
280
|
+
required: true,
|
|
281
|
+
nullable: true
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const authProviderLinkingCapabilitiesOutputSchema = createSchema({
|
|
286
|
+
start: { type: "boolean", required: true },
|
|
287
|
+
unlink: { type: "boolean", required: true }
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const authCapabilitiesFeaturesOutputSchema = createSchema({
|
|
291
|
+
password: {
|
|
292
|
+
type: "object",
|
|
293
|
+
required: true,
|
|
294
|
+
schema: authPasswordCapabilitiesOutputSchema
|
|
295
|
+
},
|
|
296
|
+
passwordRecovery: {
|
|
297
|
+
type: "object",
|
|
298
|
+
required: true,
|
|
299
|
+
schema: authPasswordRecoveryCapabilitiesOutputSchema
|
|
300
|
+
},
|
|
301
|
+
otp: {
|
|
302
|
+
type: "object",
|
|
303
|
+
required: true,
|
|
304
|
+
schema: authOtpCapabilitiesOutputSchema
|
|
305
|
+
},
|
|
306
|
+
oauthLogin: {
|
|
307
|
+
type: "object",
|
|
308
|
+
required: true,
|
|
309
|
+
schema: authOAuthLoginCapabilitiesOutputSchema
|
|
310
|
+
},
|
|
311
|
+
emailConfirmation: { type: "boolean", required: true },
|
|
312
|
+
profileUpdate: { type: "boolean", required: true },
|
|
313
|
+
providerLinking: {
|
|
314
|
+
type: "object",
|
|
315
|
+
required: true,
|
|
316
|
+
schema: authProviderLinkingCapabilitiesOutputSchema
|
|
317
|
+
},
|
|
318
|
+
securityStatus: { type: "boolean", required: true },
|
|
319
|
+
signOutOtherSessions: { type: "boolean", required: true },
|
|
320
|
+
appProfileProjection: { type: "boolean", required: true },
|
|
321
|
+
devLoginAs: { type: "boolean", required: true }
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const authCapabilitiesOutputSchema = createSchema({
|
|
325
|
+
provider: {
|
|
326
|
+
type: "object",
|
|
327
|
+
required: true,
|
|
328
|
+
schema: authCapabilityProviderOutputSchema
|
|
329
|
+
},
|
|
330
|
+
features: {
|
|
331
|
+
type: "object",
|
|
332
|
+
required: true,
|
|
333
|
+
schema: authCapabilitiesFeaturesOutputSchema
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
233
337
|
const sessionOutputSchema = createSchema({
|
|
234
338
|
authenticated: { type: "boolean", required: true },
|
|
235
339
|
username: { type: "string", required: false, minLength: 1, maxLength: 120 },
|
|
@@ -248,6 +352,11 @@ const sessionOutputSchema = createSchema({
|
|
|
248
352
|
required: false,
|
|
249
353
|
schema: authDeniedOutputSchema
|
|
250
354
|
},
|
|
355
|
+
authCapabilities: {
|
|
356
|
+
type: "object",
|
|
357
|
+
required: true,
|
|
358
|
+
schema: authCapabilitiesOutputSchema
|
|
359
|
+
},
|
|
251
360
|
csrfToken: { type: "string", required: true, minLength: 1 },
|
|
252
361
|
oauthProviders: {
|
|
253
362
|
type: "array",
|
|
@@ -269,6 +378,11 @@ const sessionOutputValidator = deepFreeze({
|
|
|
269
378
|
const sessionUnavailableOutputSchema = createSchema({
|
|
270
379
|
error: { type: "string", required: true, minLength: 1 },
|
|
271
380
|
csrfToken: { type: "string", required: true, minLength: 1 },
|
|
381
|
+
authCapabilities: {
|
|
382
|
+
type: "object",
|
|
383
|
+
required: true,
|
|
384
|
+
schema: authCapabilitiesOutputSchema
|
|
385
|
+
},
|
|
272
386
|
oauthProviders: {
|
|
273
387
|
type: "array",
|
|
274
388
|
required: true,
|
|
@@ -333,6 +447,7 @@ export {
|
|
|
333
447
|
logoutOutputValidator,
|
|
334
448
|
oauthProviderCatalogEntryOutputValidator,
|
|
335
449
|
authDeniedOutputSchema,
|
|
450
|
+
authCapabilitiesOutputSchema,
|
|
336
451
|
sessionOutputValidator,
|
|
337
452
|
sessionUnavailableOutputValidator,
|
|
338
453
|
createCommandMessages
|
package/src/shared/index.js
CHANGED
|
@@ -8,3 +8,15 @@ export {
|
|
|
8
8
|
resolveAuthDeniedLoginMessage
|
|
9
9
|
} from "./authDenied.js";
|
|
10
10
|
export { AUTH_PATHS, buildAuthOauthStartPath } from "./authPaths.js";
|
|
11
|
+
export {
|
|
12
|
+
AUTH_PASSWORD_RECOVERY_DELIVERIES,
|
|
13
|
+
AUTH_OPTIONAL_OPERATION_FEATURES,
|
|
14
|
+
normalizeAuthProviderId,
|
|
15
|
+
normalizeAuthCapabilities,
|
|
16
|
+
getCapabilityFeature,
|
|
17
|
+
isAuthOperationSupported
|
|
18
|
+
} from "./authCapabilities.js";
|
|
19
|
+
export {
|
|
20
|
+
normalizeAuthSecurityStatus,
|
|
21
|
+
buildSecurityStatusFromAuthMethodsStatus
|
|
22
|
+
} from "./authSecurityStatus.js";
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createApplication } from "@jskit-ai/kernel/_testable";
|
|
4
|
+
import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
|
|
5
|
+
import { AuthActionsServiceProvider } from "../src/server/providers/AuthActionsServiceProvider.js";
|
|
6
|
+
import { buildAuthActions } from "../src/server/actions/auth.contributor.js";
|
|
7
|
+
|
|
8
|
+
function createAppConfigFixture() {
|
|
9
|
+
return {
|
|
10
|
+
surfaceModeAll: "all",
|
|
11
|
+
surfaceDefaultId: "home",
|
|
12
|
+
surfaceDefinitions: {
|
|
13
|
+
home: { id: "home", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false },
|
|
14
|
+
console: {
|
|
15
|
+
id: "console",
|
|
16
|
+
pagesRoot: "console",
|
|
17
|
+
enabled: true,
|
|
18
|
+
requiresAuth: true,
|
|
19
|
+
requiresWorkspace: false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("auth logout action delegates to selected auth provider and notifies session changes", async () => {
|
|
26
|
+
const action = buildAuthActions().find((definition) => definition.id === "auth.logout");
|
|
27
|
+
const request = {
|
|
28
|
+
id: "request-1"
|
|
29
|
+
};
|
|
30
|
+
const calls = [];
|
|
31
|
+
|
|
32
|
+
const result = await action.execute(
|
|
33
|
+
{},
|
|
34
|
+
{
|
|
35
|
+
requestMeta: {
|
|
36
|
+
request
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
authService: {
|
|
41
|
+
async logout(receivedRequest) {
|
|
42
|
+
calls.push({
|
|
43
|
+
type: "logout",
|
|
44
|
+
request: receivedRequest
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
clearSession: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
authSessionEventsService: {
|
|
53
|
+
async notifySessionChanged(payload) {
|
|
54
|
+
calls.push({
|
|
55
|
+
type: "notify",
|
|
56
|
+
context: payload.context
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(result, {
|
|
64
|
+
ok: true,
|
|
65
|
+
clearSession: true
|
|
66
|
+
});
|
|
67
|
+
assert.deepEqual(calls, [
|
|
68
|
+
{
|
|
69
|
+
type: "logout",
|
|
70
|
+
request
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: "notify",
|
|
74
|
+
context: {
|
|
75
|
+
requestMeta: {
|
|
76
|
+
request
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("AuthActionsServiceProvider registers shared auth actions against auth.provider", async () => {
|
|
84
|
+
const app = createApplication();
|
|
85
|
+
const logoutCalls = [];
|
|
86
|
+
const published = [];
|
|
87
|
+
class SelectedAuthProvider {
|
|
88
|
+
static id = "auth.provider";
|
|
89
|
+
|
|
90
|
+
register(targetApp) {
|
|
91
|
+
targetApp.singleton("authService", () => ({
|
|
92
|
+
async logout(request) {
|
|
93
|
+
logoutCalls.push(request);
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
clearSession: true
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
async authenticateRequest() {
|
|
100
|
+
return {
|
|
101
|
+
authenticated: false,
|
|
102
|
+
actor: null,
|
|
103
|
+
transientFailure: false
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
111
|
+
app.instance("domainEvents", {
|
|
112
|
+
async publish(payload) {
|
|
113
|
+
published.push(payload);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await app.start({
|
|
118
|
+
providers: [ActionRuntimeServiceProvider, SelectedAuthProvider, AuthActionsServiceProvider]
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const actionExecutor = app.make("actionExecutor");
|
|
122
|
+
const definitions = actionExecutor.listDefinitions();
|
|
123
|
+
assert.equal(definitions.some((definition) => definition.id === "auth.login.password"), true);
|
|
124
|
+
assert.equal(definitions.some((definition) => definition.id === "auth.register"), true);
|
|
125
|
+
assert.equal(definitions.some((definition) => definition.id === "auth.dev.loginAs"), false);
|
|
126
|
+
assert.deepEqual(definitions.find((definition) => definition.id === "auth.session.read")?.surfaces, [
|
|
127
|
+
"home",
|
|
128
|
+
"console"
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const request = { id: "request-2" };
|
|
132
|
+
const result = await actionExecutor.execute({
|
|
133
|
+
actionId: "auth.logout",
|
|
134
|
+
input: {},
|
|
135
|
+
context: {
|
|
136
|
+
channel: "internal",
|
|
137
|
+
surface: "home",
|
|
138
|
+
requestMeta: { request },
|
|
139
|
+
actor: { id: 42 }
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(result, {
|
|
144
|
+
ok: true,
|
|
145
|
+
clearSession: true
|
|
146
|
+
});
|
|
147
|
+
assert.deepEqual(logoutCalls, [request]);
|
|
148
|
+
assert.equal(published.length, 2);
|
|
149
|
+
assert.deepEqual(
|
|
150
|
+
published.map((event) => ({
|
|
151
|
+
source: event.source,
|
|
152
|
+
entity: event.entity,
|
|
153
|
+
operation: event.operation,
|
|
154
|
+
entityId: event.entityId,
|
|
155
|
+
realtimeEvent: event.meta?.realtime?.event
|
|
156
|
+
})),
|
|
157
|
+
[
|
|
158
|
+
{
|
|
159
|
+
source: "auth",
|
|
160
|
+
entity: "session",
|
|
161
|
+
operation: "updated",
|
|
162
|
+
entityId: "42",
|
|
163
|
+
realtimeEvent: "auth.session.changed"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
source: "users",
|
|
167
|
+
entity: "bootstrap",
|
|
168
|
+
operation: "updated",
|
|
169
|
+
entityId: "42",
|
|
170
|
+
realtimeEvent: "users.bootstrap.changed"
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
);
|
|
174
|
+
});
|