@cosmicdrift/kumiko-bundled-features 0.149.2 → 0.150.0
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.json +7 -6
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +1 -1
- package/src/audit/__tests__/audit-security.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/auth.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/seed-admin.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +1 -1
- package/src/auth-email-password/handlers/change-password.write.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -2
- package/src/auth-email-password/handlers/login.write.ts +1 -1
- package/src/auth-email-password/handlers/reset-password.write.ts +1 -1
- package/src/auth-email-password/i18n.ts +4 -0
- package/src/auth-email-password/index.ts +6 -1
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/auth-gate.tsx +2 -0
- package/src/auth-email-password/web/login-screen.tsx +2 -0
- package/src/auth-mfa/__tests__/base32.test.ts +28 -0
- package/src/auth-mfa/__tests__/disable-and-regenerate.integration.test.ts +16 -0
- package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +60 -0
- package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +2 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +2 -0
- package/src/auth-mfa/__tests__/session-auto-revoke.integration.test.ts +2 -1
- package/src/auth-mfa/__tests__/totp.test.ts +11 -2
- package/src/auth-mfa/__tests__/verify.integration.test.ts +67 -1
- package/src/auth-mfa/base32.ts +7 -1
- package/src/auth-mfa/constants.ts +14 -0
- package/src/auth-mfa/errors.ts +9 -16
- package/src/auth-mfa/feature.ts +4 -0
- package/src/auth-mfa/handlers/enable-confirm.write.ts +20 -1
- package/src/auth-mfa/handlers/verify.write.ts +26 -3
- package/src/auth-mfa/index.ts +0 -1
- package/src/auth-mfa/recovery-codes.ts +12 -2
- package/src/auth-mfa/schema/user-mfa.ts +3 -0
- package/src/auth-mfa/testing.ts +5 -0
- package/src/auth-mfa/web/__tests__/mfa-error-keys.test.ts +20 -0
- package/src/auth-mfa/web/i18n.ts +2 -0
- package/src/auth-mfa/web/mfa-enable-screen.tsx +37 -28
- package/src/auth-mfa/web/mfa-error-keys.ts +7 -5
- package/src/auth-mfa/web/mfa-verify-screen.tsx +11 -0
- package/src/auth-mfa/web/qrcode-browser.d.ts +7 -0
- package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +2 -0
- package/src/config/handlers/cascade.query.ts +2 -1
- package/src/config/handlers/values.query.ts +2 -1
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +15 -2
- package/src/legal-pages/feature.ts +2 -1
- package/src/managed-pages/feature.ts +1 -6
- package/src/seo/feature.ts +1 -6
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +51 -0
- package/src/sessions/__tests__/sessions.integration.test.ts +44 -0
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/constants.ts +5 -0
- package/src/sessions/feature.ts +67 -1
- package/src/sessions/handlers/detail.query.ts +36 -0
- package/src/sessions/i18n.ts +23 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/system-query.ts +5 -0
- package/src/tenant/__tests__/tenant-security.integration.test.ts +1 -1
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +163 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +39 -0
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +1 -1
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +1 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +5 -2
- package/src/user-data-rights/handlers/download-by-token.query.ts +5 -4
- package/src/user-data-rights/lib/tenant-file-provider.ts +62 -0
- package/src/user-profile/__tests__/change-email.integration.test.ts +1 -1
- package/src/user-profile/handlers/change-email.write.ts +2 -2
- package/src/auth-email-password/password-hashing.ts +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.150.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"./auth-mfa": "./src/auth-mfa/index.ts",
|
|
92
92
|
"./auth-mfa-user-data": "./src/auth-mfa-user-data/index.ts",
|
|
93
93
|
"./auth-mfa/web": "./src/auth-mfa/web/index.ts",
|
|
94
|
+
"./auth-mfa/testing": "./src/auth-mfa/testing.ts",
|
|
94
95
|
"./sessions/testing": "./src/sessions/testing.ts",
|
|
95
96
|
"./personal-access-tokens": "./src/personal-access-tokens/index.ts",
|
|
96
97
|
"./personal-access-tokens/web": "./src/personal-access-tokens/web/index.ts",
|
|
@@ -114,11 +115,11 @@
|
|
|
114
115
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
115
116
|
},
|
|
116
117
|
"dependencies": {
|
|
117
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
118
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
119
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
120
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
121
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
118
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.150.0",
|
|
119
|
+
"@cosmicdrift/kumiko-framework": "0.150.0",
|
|
120
|
+
"@cosmicdrift/kumiko-headless": "0.150.0",
|
|
121
|
+
"@cosmicdrift/kumiko-renderer": "0.150.0",
|
|
122
|
+
"@cosmicdrift/kumiko-renderer-web": "0.150.0",
|
|
122
123
|
"@mollie/api-client": "^4.5.0",
|
|
123
124
|
"imapflow": "^1.3.3",
|
|
124
125
|
"mailparser": "^3.9.8",
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
unsafePushTables,
|
|
17
17
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
18
18
|
import { createAuditFeature } from "../../audit/feature";
|
|
19
|
-
import { hashPassword } from "../../auth-email-password/password-hashing";
|
|
20
19
|
import { ConfigQueries } from "../../config/constants";
|
|
21
20
|
import { createConfigFeature } from "../../config/feature";
|
|
22
21
|
import { createConfigResolver } from "../../config/resolver";
|
|
@@ -26,6 +25,7 @@ import { globalFeatureStateTable } from "../../feature-toggles/global-feature-st
|
|
|
26
25
|
import { JobQueries } from "../../jobs/constants";
|
|
27
26
|
import { createJobsFeature } from "../../jobs/feature";
|
|
28
27
|
import { jobRunLogsTable, jobRunsTable } from "../../jobs/job-run-table";
|
|
28
|
+
import { hashPassword } from "../../shared";
|
|
29
29
|
import { TenantQueries } from "../../tenant/constants";
|
|
30
30
|
import { createTenantFeature } from "../../tenant/feature";
|
|
31
31
|
import { tenantInvitationEntity } from "../../tenant/invitation-table";
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
unsafePushTables,
|
|
24
24
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
25
25
|
import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
|
|
26
|
-
import { hashPassword } from "../../auth-email-password/password-hashing";
|
|
27
26
|
import { createConfigFeature } from "../../config/feature";
|
|
27
|
+
import { hashPassword } from "../../shared";
|
|
28
28
|
import { createTenantFeature } from "../../tenant/feature";
|
|
29
29
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
30
30
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -25,6 +25,7 @@ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing"
|
|
|
25
25
|
import { createConfigFeature } from "../../config";
|
|
26
26
|
import { createConfigResolver } from "../../config/resolver";
|
|
27
27
|
import { configValuesTable } from "../../config/table";
|
|
28
|
+
import { hashPassword } from "../../shared";
|
|
28
29
|
import { createTenantFeature } from "../../tenant";
|
|
29
30
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
30
31
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -34,7 +35,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
34
35
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
35
36
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
36
37
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
37
|
-
import { hashPassword } from "../password-hashing";
|
|
38
38
|
|
|
39
39
|
let stack: TestStack;
|
|
40
40
|
|
|
@@ -24,6 +24,7 @@ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing"
|
|
|
24
24
|
import { createConfigFeature } from "../../config";
|
|
25
25
|
import { createConfigResolver } from "../../config/resolver";
|
|
26
26
|
import { configValuesTable } from "../../config/table";
|
|
27
|
+
import { hashPassword } from "../../shared";
|
|
27
28
|
import { createTenantFeature } from "../../tenant";
|
|
28
29
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
29
30
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -34,7 +35,6 @@ import { userEntity, userTable } from "../../user/schema/user";
|
|
|
34
35
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
35
36
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
36
37
|
import { getLockoutState, type LockoutState } from "../lockout-store";
|
|
37
|
-
import { hashPassword } from "../password-hashing";
|
|
38
38
|
|
|
39
39
|
let stack: TestStack;
|
|
40
40
|
|
|
@@ -15,6 +15,7 @@ import { createTestEnvelopeCipher, seedRow } from "@cosmicdrift/kumiko-framework
|
|
|
15
15
|
import { createConfigFeature } from "../../config";
|
|
16
16
|
import { createConfigResolver } from "../../config/resolver";
|
|
17
17
|
import { configValuesTable } from "../../config/table";
|
|
18
|
+
import { hashPassword } from "../../shared";
|
|
18
19
|
import { createTenantFeature } from "../../tenant";
|
|
19
20
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
20
21
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -24,7 +25,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
24
25
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
25
26
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
26
27
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
27
|
-
import { hashPassword } from "../password-hashing";
|
|
28
28
|
|
|
29
29
|
// Sample-style extension feature that shows the real-world shape of a claims
|
|
30
30
|
// hook: look something up in a tenant-scoped table and stuff it into the JWT.
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { createConfigFeature } from "../../config";
|
|
21
21
|
import { createConfigResolver } from "../../config/resolver";
|
|
22
22
|
import { configValuesTable } from "../../config/table";
|
|
23
|
+
import { hashPassword } from "../../shared";
|
|
23
24
|
import { createTenantFeature } from "../../tenant";
|
|
24
25
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
25
26
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -29,7 +30,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
29
30
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
30
31
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
31
32
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
32
|
-
import { hashPassword } from "../password-hashing";
|
|
33
33
|
|
|
34
34
|
let stack: TestStack;
|
|
35
35
|
|
|
@@ -23,6 +23,7 @@ import { createDeliveryFeature, createDeliveryTestContext } from "../../delivery
|
|
|
23
23
|
import { notificationPreferencesTable } from "../../delivery/tables";
|
|
24
24
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
25
25
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
26
|
+
import { hashPassword } from "../../shared";
|
|
26
27
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
27
28
|
import { createTenantFeature } from "../../tenant";
|
|
28
29
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
@@ -33,7 +34,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
33
34
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
34
35
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
35
36
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
36
|
-
import { hashPassword } from "../password-hashing";
|
|
37
37
|
import { signResetToken } from "../reset-token";
|
|
38
38
|
import { signVerificationToken } from "../verification-token";
|
|
39
39
|
|
|
@@ -31,6 +31,7 @@ import { createDeliveryFeature, createDeliveryTestContext } from "../../delivery
|
|
|
31
31
|
import { notificationPreferencesTable } from "../../delivery/tables";
|
|
32
32
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
33
33
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
34
|
+
import { hashPassword } from "../../shared";
|
|
34
35
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
35
36
|
import { createTenantFeature } from "../../tenant";
|
|
36
37
|
import { tenantInvitationEntity, tenantInvitationsTable } from "../../tenant/invitation-table";
|
|
@@ -41,7 +42,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
41
42
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
42
43
|
import { AuthHandlers } from "../constants";
|
|
43
44
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
44
|
-
import { hashPassword } from "../password-hashing";
|
|
45
45
|
import { seedUser } from "../seeding";
|
|
46
46
|
|
|
47
47
|
const APP_ACCEPT_URL = "https://app.example.com/invite/accept";
|
|
@@ -37,6 +37,7 @@ import { createDeliveryFeature, createDeliveryTestContext } from "../../delivery
|
|
|
37
37
|
import { notificationPreferencesTable } from "../../delivery/tables";
|
|
38
38
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
39
39
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
40
|
+
import { hashPassword } from "../../shared";
|
|
40
41
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
41
42
|
import { createTenantFeature } from "../../tenant";
|
|
42
43
|
import {
|
|
@@ -52,7 +53,6 @@ import { userEntity, userTable } from "../../user/schema/user";
|
|
|
52
53
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
53
54
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
54
55
|
import { storeInviteToken } from "../invite-token-store";
|
|
55
|
-
import { hashPassword } from "../password-hashing";
|
|
56
56
|
import { seedUser } from "../seeding";
|
|
57
57
|
|
|
58
58
|
const APP_ACCEPT_URL = "https://app.example.com/invite/accept";
|
|
@@ -23,6 +23,7 @@ import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
|
23
23
|
import { createConfigFeature } from "../../config";
|
|
24
24
|
import { createConfigResolver } from "../../config/resolver";
|
|
25
25
|
import { configValuesTable } from "../../config/table";
|
|
26
|
+
import { hashPassword } from "../../shared";
|
|
26
27
|
import { createTenantFeature } from "../../tenant";
|
|
27
28
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
28
29
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -31,7 +32,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
31
32
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
32
33
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
33
34
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
34
|
-
import { hashPassword } from "../password-hashing";
|
|
35
35
|
|
|
36
36
|
let stack: TestStack;
|
|
37
37
|
const systemAdmin = TestUsers.systemAdmin;
|
|
@@ -20,6 +20,7 @@ import { notificationPreferencesTable } from "../../delivery/tables";
|
|
|
20
20
|
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
21
21
|
import { createRendererSimpleFeature, simpleRenderer } from "../../renderer-simple";
|
|
22
22
|
import { createSessionsFeature, userSessionTable } from "../../sessions";
|
|
23
|
+
import { hashPassword, verifyPassword } from "../../shared";
|
|
23
24
|
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
24
25
|
import { createTenantFeature } from "../../tenant";
|
|
25
26
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
@@ -30,7 +31,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
30
31
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
31
32
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
32
33
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
33
|
-
import { hashPassword, verifyPassword } from "../password-hashing";
|
|
34
34
|
import { signResetToken } from "../reset-token";
|
|
35
35
|
|
|
36
36
|
// Reset mails now go through delivery (ctx.notify → channel-email). The
|
|
@@ -23,12 +23,12 @@ import {
|
|
|
23
23
|
import { createConfigFeature } from "../../config/feature";
|
|
24
24
|
import { createConfigResolver } from "../../config/resolver";
|
|
25
25
|
import { configValuesTable } from "../../config/table";
|
|
26
|
+
import { verifyPassword } from "../../shared";
|
|
26
27
|
import { createTenantFeature } from "../../tenant/feature";
|
|
27
28
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
28
29
|
import { tenantEntity, tenantTable } from "../../tenant/schema/tenant";
|
|
29
30
|
import { createUserFeature } from "../../user/feature";
|
|
30
31
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
31
|
-
import { verifyPassword } from "../password-hashing";
|
|
32
32
|
import { seedAdmin } from "../testing";
|
|
33
33
|
|
|
34
34
|
let stack: TestStack;
|
|
@@ -14,6 +14,7 @@ import * as jose from "jose";
|
|
|
14
14
|
import { createConfigFeature } from "../../config";
|
|
15
15
|
import { createConfigResolver } from "../../config/resolver";
|
|
16
16
|
import { configValuesTable } from "../../config/table";
|
|
17
|
+
import { hashPassword } from "../../shared";
|
|
17
18
|
import { createTenantFeature } from "../../tenant";
|
|
18
19
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
19
20
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -23,7 +24,6 @@ import { createUserFeature } from "../../user/feature";
|
|
|
23
24
|
import { userEntity, userTable } from "../../user/schema/user";
|
|
24
25
|
import { AuthErrors, AuthHandlers } from "../constants";
|
|
25
26
|
import { createAuthEmailPasswordFeature } from "../feature";
|
|
26
|
-
import { hashPassword } from "../password-hashing";
|
|
27
27
|
|
|
28
28
|
// In-memory fake of a real sessions-store — just enough to observe that the
|
|
29
29
|
// framework calls the callbacks at the right moments and threads the sid back
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
SYSTEM_TENANT_ID,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { hashPassword, verifyPassword } from "../../shared";
|
|
8
9
|
import { UserHandlers, UserQueries } from "../../user";
|
|
9
10
|
import { invalidCredentials } from "../errors";
|
|
10
|
-
import { hashPassword, verifyPassword } from "../password-hashing";
|
|
11
11
|
|
|
12
12
|
// Change-password — authenticated. The user supplies their current password
|
|
13
13
|
// (re-auth) and the new one. The new hash is written via ctx.writeAs(system)
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
29
29
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
30
30
|
import { z } from "zod";
|
|
31
|
-
import { decryptStoredPii } from "../../shared";
|
|
31
|
+
import { decryptStoredPii, verifyPassword } from "../../shared";
|
|
32
32
|
// kumiko-lint-ignore cross-feature-import invite-flow
|
|
33
33
|
import {
|
|
34
34
|
INVITATION_STATUS,
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
getInvitationIdForToken,
|
|
47
47
|
unburnInviteToken,
|
|
48
48
|
} from "../invite-token-store";
|
|
49
|
-
import { verifyPassword } from "../password-hashing";
|
|
50
49
|
|
|
51
50
|
const InviteAcceptWithLoginSchema = z.object({
|
|
52
51
|
token: z.string().min(1),
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
10
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
import { verifyDummyPassword, verifyPassword } from "../../shared";
|
|
12
13
|
import { USER_STATUS, UserQueries } from "../../user";
|
|
13
14
|
import { parseAuthUserRow } from "../auth-user-row";
|
|
14
15
|
import {
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
noMembership,
|
|
24
25
|
} from "../errors";
|
|
25
26
|
import { clearLockoutState, getLockoutState, recordFailedAttempt } from "../lockout-store";
|
|
26
|
-
import { verifyDummyPassword, verifyPassword } from "../password-hashing";
|
|
27
27
|
|
|
28
28
|
export type LoginHandlerOptions = {
|
|
29
29
|
// When true, a valid (email + password) login fails with email_not_verified
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { hashPassword } from "../../shared";
|
|
4
5
|
import { AuthErrors } from "../constants";
|
|
5
6
|
import { invalidResetToken } from "../errors";
|
|
6
|
-
import { hashPassword } from "../password-hashing";
|
|
7
7
|
import { verifyResetToken } from "../reset-token";
|
|
8
8
|
import { runConfirmTokenFlow } from "./confirm-token-flow";
|
|
9
9
|
|
|
@@ -32,6 +32,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
32
32
|
"auth.errors.rateLimited": "Zu viele Login-Versuche. Bitte kurz warten.",
|
|
33
33
|
"auth.errors.invalidBody": "Ungültige Eingabe.",
|
|
34
34
|
"auth.errors.loginFailed": "Login fehlgeschlagen.",
|
|
35
|
+
"auth.errors.mfaNotSupported":
|
|
36
|
+
"Diese App unterstützt keine Zwei-Faktor-Bestätigung. Bitte an den Support wenden.",
|
|
35
37
|
"auth.errors.invalidResetToken":
|
|
36
38
|
"Der Link ist ungültig oder abgelaufen. Bitte fordere einen neuen an.",
|
|
37
39
|
"auth.errors.invalidVerificationToken": "Der Bestätigungs-Link ist ungültig oder abgelaufen.",
|
|
@@ -134,6 +136,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
134
136
|
"auth.errors.rateLimited": "Too many login attempts. Please wait briefly.",
|
|
135
137
|
"auth.errors.invalidBody": "Invalid input.",
|
|
136
138
|
"auth.errors.loginFailed": "Login failed.",
|
|
139
|
+
"auth.errors.mfaNotSupported":
|
|
140
|
+
"This app doesn't support two-factor verification. Please contact support.",
|
|
137
141
|
"auth.errors.invalidResetToken": "Link is invalid or expired. Please request a new one.",
|
|
138
142
|
"auth.errors.invalidVerificationToken": "Verification link is invalid or expired.",
|
|
139
143
|
"auth.errors.invalidSignupToken":
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Convention paths for the auth pages (reset/verify/signup/invite); apps build
|
|
2
2
|
// their magic-link appUrls from these. All mail now goes through delivery.
|
|
3
|
+
|
|
4
|
+
// Kept as a public re-export (not just internal call sites) — this feature's
|
|
5
|
+
// barrel is where consumers of the published package have always found
|
|
6
|
+
// hashPassword/verifyPassword; removing it is a breaking change that needs
|
|
7
|
+
// its own changeset + deliberate deprecation window, not a silent drop.
|
|
8
|
+
export { hashPassword, verifyPassword } from "../shared/password-hashing";
|
|
3
9
|
export { type AuthPaths, DEFAULT_AUTH_PATHS, makeAuthPaths } from "./auth-paths";
|
|
4
10
|
export { AUTH_EMAIL_PASSWORD_FEATURE, AuthErrors, AuthHandlers } from "./constants";
|
|
5
11
|
// Renderers for the auth mails. All four magic-link flows (reset, verify,
|
|
@@ -27,7 +33,6 @@ export type {
|
|
|
27
33
|
SignupOptions,
|
|
28
34
|
} from "./feature";
|
|
29
35
|
export { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "./feature";
|
|
30
|
-
export { hashPassword, verifyPassword } from "./password-hashing";
|
|
31
36
|
// Generic HMAC-signed single-purpose token helpers. Re-exported damit
|
|
32
37
|
// app-spezifische out-of-band-Flows (subscriber-confirm, magic-links,
|
|
33
38
|
// invite-tokens) denselben battle-tested signer/verifier nutzen können
|
|
@@ -17,13 +17,13 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
|
17
17
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
18
|
import { ConflictError } from "@cosmicdrift/kumiko-framework/errors";
|
|
19
19
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
20
|
+
import { hashPassword } from "../shared";
|
|
20
21
|
// kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
|
|
21
22
|
import { seedTenant, seedTenantMembership } from "../tenant/seeding";
|
|
22
23
|
// kumiko-lint-ignore cross-feature-import signup create-only guard reads the user projection by email
|
|
23
24
|
import { userTable } from "../user/schema/user";
|
|
24
25
|
// kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
|
|
25
26
|
import { seedUser } from "../user/seeding";
|
|
26
|
-
import { hashPassword } from "./password-hashing";
|
|
27
27
|
|
|
28
28
|
// Re-export für ergonomische Single-Import-Site in tests/seed-scripts.
|
|
29
29
|
// Das Auth-Feature ist der natürliche Aufrufer für "seed admin user mit
|
|
@@ -20,6 +20,7 @@ import { SessionProvider, useSession } from "./session";
|
|
|
20
20
|
export type MfaVerifyComponentProps = {
|
|
21
21
|
readonly challengeToken: string;
|
|
22
22
|
readonly onSuccess?: () => void;
|
|
23
|
+
readonly onCancel?: () => void;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export function makeAuthGate(
|
|
@@ -45,6 +46,7 @@ export function makeAuthGate(
|
|
|
45
46
|
<MfaVerifyComponent
|
|
46
47
|
challengeToken={challengeToken}
|
|
47
48
|
onSuccess={() => setChallengeToken(null)}
|
|
49
|
+
onCancel={() => setChallengeToken(null)}
|
|
48
50
|
/>
|
|
49
51
|
);
|
|
50
52
|
}
|
|
@@ -88,6 +88,8 @@ function reasonToKey(failure: LoginFailure): {
|
|
|
88
88
|
return { key: "auth.errors.rateLimited" };
|
|
89
89
|
case "invalid_body":
|
|
90
90
|
return { key: "auth.errors.invalidBody" };
|
|
91
|
+
case "mfa_not_supported":
|
|
92
|
+
return { key: "auth.errors.mfaNotSupported" };
|
|
91
93
|
default:
|
|
92
94
|
return { key: "auth.errors.loginFailed" };
|
|
93
95
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { base32Decode, base32Encode } from "../base32";
|
|
3
|
+
|
|
4
|
+
describe("base32 encode/decode", () => {
|
|
5
|
+
test("round-trips arbitrary bytes", () => {
|
|
6
|
+
const bytes = Buffer.from("12345678901234567890", "ascii");
|
|
7
|
+
expect(base32Decode(base32Encode(bytes))).toEqual(bytes);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("decodes lowercase and RFC 4648 '=' padding", () => {
|
|
11
|
+
const bytes = Buffer.from("12345678901234567890", "ascii");
|
|
12
|
+
const encoded = base32Encode(bytes);
|
|
13
|
+
const paddingLength = (8 - (encoded.length % 8)) % 8;
|
|
14
|
+
const withPadding = encoded + "=".repeat(paddingLength);
|
|
15
|
+
expect(base32Decode(withPadding.toLowerCase())).toEqual(bytes);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("rejects a non-canonical length with non-zero residual bits", () => {
|
|
19
|
+
// "AA" decodes to 10 residual bits of all-zero value — canonical (padding
|
|
20
|
+
// zeros). A single stray char (5 bits, below the byte boundary but not a
|
|
21
|
+
// clean multiple) with non-zero value must be rejected, not truncated.
|
|
22
|
+
expect(() => base32Decode("B")).toThrow(/invalid length/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("throws on a genuinely invalid character", () => {
|
|
26
|
+
expect(() => base32Decode("01")).toThrow(/invalid character/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { createConfigFeature } from "../../config";
|
|
15
15
|
import { createConfigResolver } from "../../config/resolver";
|
|
16
16
|
import { configValuesTable } from "../../config/table";
|
|
17
|
+
import { createTenantFeature } from "../../tenant";
|
|
17
18
|
import { createUserFeature } from "../../user/feature";
|
|
18
19
|
import { userEntity } from "../../user/schema/user";
|
|
19
20
|
import { base32Decode } from "../base32";
|
|
@@ -34,6 +35,7 @@ beforeAll(async () => {
|
|
|
34
35
|
features: [
|
|
35
36
|
createConfigFeature(),
|
|
36
37
|
createUserFeature(),
|
|
38
|
+
createTenantFeature(),
|
|
37
39
|
createAuthMfaFeature({
|
|
38
40
|
setupTokenSecret: SETUP_TOKEN_SECRET,
|
|
39
41
|
issuer: "Kumiko Test",
|
|
@@ -139,6 +141,20 @@ describe("disable", () => {
|
|
|
139
141
|
expect(res.disabled).toBe(true);
|
|
140
142
|
});
|
|
141
143
|
|
|
144
|
+
test("a recovery code typed lowercase and without the dash still matches", async () => {
|
|
145
|
+
const { user, recoveryCodes } = await enableMfaFor("disable-recovery-normalize-1");
|
|
146
|
+
const code = recoveryCodes[0];
|
|
147
|
+
if (code === undefined) throw new Error("test setup produced no recovery code");
|
|
148
|
+
|
|
149
|
+
const sloppy = code.toLowerCase().replace("-", "");
|
|
150
|
+
const res = await stack.http.writeOk<{ disabled: boolean }>(
|
|
151
|
+
AuthMfaHandlers.disable,
|
|
152
|
+
{ code: sloppy },
|
|
153
|
+
user,
|
|
154
|
+
);
|
|
155
|
+
expect(res.disabled).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
142
158
|
test("a wrong code is rejected and MFA stays enabled", async () => {
|
|
143
159
|
const { user, secret } = await enableMfaFor("disable-wrong-1");
|
|
144
160
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { createConfigFeature } from "../../config";
|
|
15
15
|
import { createConfigResolver } from "../../config/resolver";
|
|
16
16
|
import { configValuesTable } from "../../config/table";
|
|
17
|
+
import { createTenantFeature } from "../../tenant";
|
|
17
18
|
import { createUserFeature } from "../../user/feature";
|
|
18
19
|
import { userEntity } from "../../user/schema/user";
|
|
19
20
|
import { base32Decode } from "../base32";
|
|
@@ -36,6 +37,7 @@ beforeAll(async () => {
|
|
|
36
37
|
features: [
|
|
37
38
|
createConfigFeature(),
|
|
38
39
|
createUserFeature(),
|
|
40
|
+
createTenantFeature(),
|
|
39
41
|
createAuthMfaFeature({
|
|
40
42
|
setupTokenSecret: SETUP_TOKEN_SECRET,
|
|
41
43
|
issuer: "Kumiko Test",
|
|
@@ -171,3 +173,61 @@ describe("enable-start + enable-confirm round trip", () => {
|
|
|
171
173
|
expect(after.enabled).toBe(true);
|
|
172
174
|
});
|
|
173
175
|
});
|
|
176
|
+
|
|
177
|
+
describe("enable-confirm burns the setup token on first success", () => {
|
|
178
|
+
test("replaying the same setup token after a disable does not silently re-enable MFA", async () => {
|
|
179
|
+
const user = createTestUser({ id: "setup-token-replay-1", roles: ["User"] });
|
|
180
|
+
|
|
181
|
+
const start = await stack.http.writeOk<{ setupToken: string; otpauthUri: string }>(
|
|
182
|
+
AuthMfaHandlers.enableStart,
|
|
183
|
+
{ accountLabel: "replay@example.com" },
|
|
184
|
+
user,
|
|
185
|
+
);
|
|
186
|
+
const secretParam = new URLSearchParams(start.otpauthUri.split("?")[1]).get("secret") ?? "";
|
|
187
|
+
const code = currentTotpCode(base32Decode(secretParam));
|
|
188
|
+
|
|
189
|
+
await stack.http.writeOk(
|
|
190
|
+
AuthMfaHandlers.enableConfirm,
|
|
191
|
+
{ setupToken: start.setupToken, code },
|
|
192
|
+
user,
|
|
193
|
+
);
|
|
194
|
+
await stack.http.writeOk(AuthMfaHandlers.disable, { code }, user);
|
|
195
|
+
|
|
196
|
+
// Same setupToken + a still-computable code for the disabled secret —
|
|
197
|
+
// without burning the token on first confirm this would re-create the
|
|
198
|
+
// row with the secret the user just discarded.
|
|
199
|
+
const replay = await stack.http.writeErr(
|
|
200
|
+
AuthMfaHandlers.enableConfirm,
|
|
201
|
+
{ setupToken: start.setupToken, code: currentTotpCode(base32Decode(secretParam)) },
|
|
202
|
+
user,
|
|
203
|
+
);
|
|
204
|
+
expectErrorIncludes(replay, "invalid_setup_token");
|
|
205
|
+
|
|
206
|
+
const after = await stack.http.queryOk<{ enabled: boolean }>(AuthMfaQueries.status, {}, user);
|
|
207
|
+
expect(after.enabled).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("a second confirm of the same setup token (already enabled) is rejected, not a raw db error", async () => {
|
|
211
|
+
const user = createTestUser({ id: "setup-token-double-confirm-1", roles: ["User"] });
|
|
212
|
+
|
|
213
|
+
const start = await stack.http.writeOk<{ setupToken: string; otpauthUri: string }>(
|
|
214
|
+
AuthMfaHandlers.enableStart,
|
|
215
|
+
{ accountLabel: "double@example.com" },
|
|
216
|
+
user,
|
|
217
|
+
);
|
|
218
|
+
const secretParam = new URLSearchParams(start.otpauthUri.split("?")[1]).get("secret") ?? "";
|
|
219
|
+
const code = currentTotpCode(base32Decode(secretParam));
|
|
220
|
+
|
|
221
|
+
await stack.http.writeOk(
|
|
222
|
+
AuthMfaHandlers.enableConfirm,
|
|
223
|
+
{ setupToken: start.setupToken, code },
|
|
224
|
+
user,
|
|
225
|
+
);
|
|
226
|
+
const second = await stack.http.writeErr(
|
|
227
|
+
AuthMfaHandlers.enableConfirm,
|
|
228
|
+
{ setupToken: start.setupToken, code: currentTotpCode(base32Decode(secretParam)) },
|
|
229
|
+
user,
|
|
230
|
+
);
|
|
231
|
+
expectErrorIncludes(second, "invalid_setup_token");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -27,6 +27,7 @@ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing"
|
|
|
27
27
|
import { createConfigFeature } from "../../config";
|
|
28
28
|
import { createConfigResolver } from "../../config/resolver";
|
|
29
29
|
import { configValuesTable } from "../../config/table";
|
|
30
|
+
import { createTenantFeature } from "../../tenant";
|
|
30
31
|
import { createUserFeature } from "../../user/feature";
|
|
31
32
|
import { userEntity } from "../../user/schema/user";
|
|
32
33
|
import { base32Decode } from "../base32";
|
|
@@ -47,6 +48,7 @@ beforeAll(async () => {
|
|
|
47
48
|
features: [
|
|
48
49
|
createConfigFeature(),
|
|
49
50
|
createUserFeature(),
|
|
51
|
+
createTenantFeature(),
|
|
50
52
|
createAuthMfaFeature({
|
|
51
53
|
setupTokenSecret: SETUP_TOKEN_SECRET,
|
|
52
54
|
issuer: "Kumiko Test",
|
|
@@ -32,6 +32,7 @@ import { createMutableMasterKeyProvider } from "@cosmicdrift/kumiko-framework/te
|
|
|
32
32
|
import { createConfigFeature } from "../../config";
|
|
33
33
|
import { createConfigResolver } from "../../config/resolver";
|
|
34
34
|
import { configValuesTable } from "../../config/table";
|
|
35
|
+
import { createTenantFeature } from "../../tenant";
|
|
35
36
|
import { createUserFeature } from "../../user/feature";
|
|
36
37
|
import { userEntity } from "../../user/schema/user";
|
|
37
38
|
import { base32Decode } from "../base32";
|
|
@@ -64,6 +65,7 @@ beforeAll(async () => {
|
|
|
64
65
|
features: [
|
|
65
66
|
createConfigFeature(),
|
|
66
67
|
createUserFeature(),
|
|
68
|
+
createTenantFeature(),
|
|
67
69
|
createAuthMfaFeature({
|
|
68
70
|
setupTokenSecret: SETUP_TOKEN_SECRET,
|
|
69
71
|
issuer: "Kumiko Test",
|
|
@@ -17,6 +17,7 @@ import { createConfigResolver } from "../../config/resolver";
|
|
|
17
17
|
import { configValuesTable } from "../../config/table";
|
|
18
18
|
import { userSessionEntity, userSessionTable } from "../../sessions/schema/user-session";
|
|
19
19
|
import { createSessionCallbacks } from "../../sessions/session-callbacks";
|
|
20
|
+
import { createTenantFeature } from "../../tenant";
|
|
20
21
|
import { createUserFeature } from "../../user/feature";
|
|
21
22
|
import { userEntity } from "../../user/schema/user";
|
|
22
23
|
import { base32Decode } from "../base32";
|
|
@@ -44,7 +45,7 @@ beforeAll(async () => {
|
|
|
44
45
|
challengeTokenSecret: "test-mfa-challenge-secret-at-least-32-bytes!!",
|
|
45
46
|
});
|
|
46
47
|
stack = await setupTestStack({
|
|
47
|
-
features: [createConfigFeature(), createUserFeature(), mfaFeature],
|
|
48
|
+
features: [createConfigFeature(), createUserFeature(), createTenantFeature(), mfaFeature],
|
|
48
49
|
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
49
50
|
});
|
|
50
51
|
|
|
@@ -37,12 +37,20 @@ describe("TOTP — RFC 6238 vectors", () => {
|
|
|
37
37
|
test("accepts a code one step in the past or future (clock drift)", () => {
|
|
38
38
|
const secret = generateTotpSecret();
|
|
39
39
|
const now = 1_700_000_000_000;
|
|
40
|
-
//
|
|
41
|
-
//
|
|
40
|
+
// now + 25s crosses the 30s step boundary (deliberately) — this exercises
|
|
41
|
+
// the ±1 window, not same-step reuse. See the ±2-steps negative test below
|
|
42
|
+
// for the window's upper bound.
|
|
42
43
|
const codeAtNow = deriveCodeForTest(secret, now);
|
|
43
44
|
expect(verifyTotp(secret, codeAtNow, now + 25_000)).toBe(true);
|
|
44
45
|
});
|
|
45
46
|
|
|
47
|
+
test("rejects a code two steps in the future (outside the ±1 window)", () => {
|
|
48
|
+
const secret = generateTotpSecret();
|
|
49
|
+
const now = 1_700_000_000_000;
|
|
50
|
+
const codeAtNow = deriveCodeForTest(secret, now);
|
|
51
|
+
expect(verifyTotp(secret, codeAtNow, now + 2 * 30_000 + 5_000)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
46
54
|
test("rejects wrong-length or non-numeric input without throwing", () => {
|
|
47
55
|
const secret = generateTotpSecret();
|
|
48
56
|
expect(verifyTotp(secret, "12345")).toBe(false);
|
|
@@ -56,6 +64,7 @@ describe("TOTP — RFC 6238 vectors", () => {
|
|
|
56
64
|
const b = generateTotpSecret();
|
|
57
65
|
expect(a.length).toBe(20);
|
|
58
66
|
expect(a.equals(b)).toBe(false);
|
|
67
|
+
expect(a.some((byte) => byte !== 0)).toBe(true);
|
|
59
68
|
});
|
|
60
69
|
});
|
|
61
70
|
|