@rebasepro/server-core 0.7.0 → 0.9.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/README.md +2 -2
- package/dist/api/ast-schema-editor.d.ts +21 -0
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/api/errors.d.ts +53 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/logs-routes.d.ts +37 -0
- package/dist/api/openapi-generator.d.ts +16 -0
- package/dist/api/rest/api-generator.d.ts +68 -0
- package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
- package/dist/api/rest/query-parser.d.ts +6 -0
- package/dist/api/schema-editor-routes.d.ts +3 -0
- package/dist/api/types.d.ts +56 -0
- package/dist/auth/adapter-middleware.d.ts +43 -0
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
- package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/auth/api-keys/api-key-store.d.ts +35 -0
- package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
- package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
- package/dist/auth/apple-oauth.d.ts +30 -0
- package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
- package/dist/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/auth/builtin-auth-adapter.d.ts +55 -0
- package/dist/auth/cookie-utils.d.ts +32 -0
- package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
- package/dist/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/auth/discord-oauth.d.ts +14 -0
- package/dist/auth/facebook-oauth.d.ts +14 -0
- package/dist/auth/github-oauth.d.ts +15 -0
- package/dist/auth/gitlab-oauth.d.ts +13 -0
- package/dist/auth/google-oauth.d.ts +47 -0
- package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
- package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
- package/dist/auth/jwt.d.ts +67 -0
- package/dist/auth/linkedin-oauth.d.ts +18 -0
- package/dist/auth/magic-link-routes.d.ts +32 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +7 -0
- package/dist/auth/mfa.d.ts +49 -0
- package/dist/auth/microsoft-oauth.d.ts +16 -0
- package/dist/auth/middleware.d.ts +158 -0
- package/dist/auth/password.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/rls-scope.d.ts +31 -0
- package/dist/auth/routes.d.ts +66 -0
- package/dist/auth/session-routes.d.ts +29 -0
- package/dist/auth/slack-oauth.d.ts +12 -0
- package/dist/auth/spotify-oauth.d.ts +12 -0
- package/dist/auth/twitter-oauth.d.ts +18 -0
- package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/collections/loader.d.ts +5 -0
- package/dist/cron/cron-loader.d.ts +18 -0
- package/dist/cron/cron-routes.d.ts +14 -0
- package/dist/cron/cron-scheduler.d.ts +106 -0
- package/dist/cron/cron-store.d.ts +32 -0
- package/dist/cron/define-cron.d.ts +28 -0
- package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
- package/dist/db/interfaces.d.ts +18 -0
- package/dist/dynamic-import-Dvh-K5fl.js +22 -0
- package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
- package/dist/email/index.d.ts +6 -0
- package/dist/email/smtp-email-service.d.ts +30 -0
- package/dist/email/templates.d.ts +50 -0
- package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
- package/dist/env.d.ts +102 -0
- package/dist/functions/define-function.d.ts +55 -0
- package/dist/functions/function-loader.d.ts +18 -0
- package/dist/functions/function-routes.d.ts +10 -0
- package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
- package/dist/history/history-routes.d.ts +23 -0
- package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.es.js +18184 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +48629 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +65 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/init.d.ts +344 -0
- package/dist/jwt-BwIn8xmk.js +4200 -0
- package/dist/jwt-BwIn8xmk.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-DnYXB-Wd.js +162 -0
- package/dist/ms-DnYXB-Wd.js.map +1 -0
- package/dist/openapi-generator-Z9oYWLf_.js +586 -0
- package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
- package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
- package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
- package/dist/serve-spa.d.ts +36 -0
- package/dist/services/driver-registry.d.ts +78 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/services/webhook-service.d.ts +29 -0
- package/dist/singleton.d.ts +51 -0
- package/dist/src-B4OLmNVa.js +437 -0
- package/dist/src-B4OLmNVa.js.map +1 -0
- package/dist/src-B5WkP1Rv.js +24778 -0
- package/dist/src-B5WkP1Rv.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/LocalStorageController.d.ts +46 -0
- package/dist/storage/S3StorageController.d.ts +40 -0
- package/dist/storage/image-transform.d.ts +50 -0
- package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
- package/dist/storage/routes.d.ts +70 -0
- package/dist/storage/storage-registry.d.ts +78 -0
- package/dist/storage/tus-handler.d.ts +53 -0
- package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/dev-port.d.ts +45 -0
- package/dist/utils/dynamic-import.d.ts +25 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logging.d.ts +9 -0
- package/dist/utils/request-id.d.ts +4 -0
- package/dist/utils/request-logger.d.ts +19 -0
- package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
- package/package.json +49 -22
- package/coverage/clover.xml +0 -3739
- package/coverage/coverage-final.json +0 -31
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
- package/coverage/lcov-report/src/api/errors.ts.html +0 -472
- package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
- package/coverage/lcov-report/src/api/graphql/index.html +0 -116
- package/coverage/lcov-report/src/api/index.html +0 -176
- package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
- package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
- package/coverage/lcov-report/src/api/rest/index.html +0 -131
- package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
- package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
- package/coverage/lcov-report/src/api/server.ts.html +0 -823
- package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
- package/coverage/lcov-report/src/auth/index.html +0 -176
- package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
- package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
- package/coverage/lcov-report/src/auth/password.ts.html +0 -310
- package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
- package/coverage/lcov-report/src/collections/index.html +0 -116
- package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
- package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
- package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
- package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
- package/coverage/lcov-report/src/db/index.html +0 -146
- package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
- package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
- package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
- package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
- package/coverage/lcov-report/src/db/services/index.html +0 -176
- package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
- package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
- package/coverage/lcov-report/src/index.html +0 -116
- package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
- package/coverage/lcov-report/src/services/index.html +0 -131
- package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
- package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
- package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
- package/coverage/lcov-report/src/storage/index.html +0 -161
- package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
- package/coverage/lcov-report/src/storage/types.ts.html +0 -451
- package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
- package/coverage/lcov-report/src/utils/index.html +0 -116
- package/coverage/lcov.info +0 -7179
- package/jest.config.cjs +0 -19
- package/src/api/ast-schema-editor.ts +0 -289
- package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
- package/src/api/errors.ts +0 -278
- package/src/api/graphql/graphql-schema-generator.ts +0 -383
- package/src/api/graphql/index.ts +0 -2
- package/src/api/index.ts +0 -11
- package/src/api/logs-routes.ts +0 -132
- package/src/api/openapi-generator.ts +0 -727
- package/src/api/rest/api-generator-count.test.ts +0 -126
- package/src/api/rest/api-generator.ts +0 -747
- package/src/api/rest/query-parser.ts +0 -300
- package/src/api/schema-editor-routes.ts +0 -41
- package/src/api/server.ts +0 -271
- package/src/api/types.ts +0 -104
- package/src/auth/adapter-middleware.ts +0 -114
- package/src/auth/admin-roles-route.ts +0 -36
- package/src/auth/admin-user-ops.ts +0 -236
- package/src/auth/admin-users-route.ts +0 -302
- package/src/auth/api-keys/api-key-middleware.ts +0 -133
- package/src/auth/api-keys/api-key-routes.ts +0 -193
- package/src/auth/api-keys/api-key-store.ts +0 -340
- package/src/auth/apple-oauth.ts +0 -122
- package/src/auth/bitbucket-oauth.ts +0 -84
- package/src/auth/builtin-auth-adapter.ts +0 -380
- package/src/auth/custom-auth-adapter.ts +0 -86
- package/src/auth/discord-oauth.ts +0 -85
- package/src/auth/facebook-oauth.ts +0 -74
- package/src/auth/github-oauth.ts +0 -112
- package/src/auth/gitlab-oauth.ts +0 -72
- package/src/auth/google-oauth.ts +0 -223
- package/src/auth/jwt.ts +0 -208
- package/src/auth/linkedin-oauth.ts +0 -83
- package/src/auth/magic-link-routes.ts +0 -167
- package/src/auth/mfa-crypto.ts +0 -91
- package/src/auth/mfa-routes.ts +0 -323
- package/src/auth/mfa.ts +0 -161
- package/src/auth/microsoft-oauth.ts +0 -90
- package/src/auth/middleware.ts +0 -369
- package/src/auth/password.ts +0 -77
- package/src/auth/rate-limiter.ts +0 -225
- package/src/auth/reset-password-admin.ts +0 -160
- package/src/auth/rls-scope.ts +0 -58
- package/src/auth/routes.ts +0 -802
- package/src/auth/session-routes.ts +0 -353
- package/src/auth/slack-oauth.ts +0 -73
- package/src/auth/spotify-oauth.ts +0 -69
- package/src/auth/twitter-oauth.ts +0 -128
- package/src/collections/BackendCollectionRegistry.ts +0 -20
- package/src/collections/loader.ts +0 -50
- package/src/cron/cron-loader.ts +0 -84
- package/src/cron/cron-routes.test.ts +0 -265
- package/src/cron/cron-routes.ts +0 -85
- package/src/cron/cron-scheduler.test.ts +0 -559
- package/src/cron/cron-scheduler.ts +0 -581
- package/src/cron/cron-store.ts +0 -166
- package/src/db/interfaces.ts +0 -60
- package/src/email/index.ts +0 -19
- package/src/email/smtp-email-service.ts +0 -123
- package/src/email/templates.ts +0 -470
- package/src/env.ts +0 -227
- package/src/functions/function-loader.ts +0 -116
- package/src/functions/function-routes.ts +0 -31
- package/src/history/history-routes.ts +0 -129
- package/src/index.ts +0 -70
- package/src/init/docs.ts +0 -47
- package/src/init/health.ts +0 -37
- package/src/init/middlewares.ts +0 -61
- package/src/init/shutdown.ts +0 -56
- package/src/init/storage.ts +0 -57
- package/src/init.ts +0 -1041
- package/src/serve-spa.ts +0 -89
- package/src/services/driver-registry.ts +0 -183
- package/src/services/routed-realtime-service.ts +0 -113
- package/src/services/webhook-service.ts +0 -155
- package/src/singleton.ts +0 -70
- package/src/storage/LocalStorageController.ts +0 -389
- package/src/storage/S3StorageController.ts +0 -300
- package/src/storage/image-transform.ts +0 -218
- package/src/storage/routes.ts +0 -368
- package/src/storage/storage-registry.ts +0 -188
- package/src/storage/tus-handler.ts +0 -315
- package/src/types/index.ts +0 -27
- package/src/utils/dev-port.ts +0 -196
- package/src/utils/logger.ts +0 -143
- package/src/utils/logging.ts +0 -40
- package/src/utils/request-id.ts +0 -40
- package/src/utils/request-logger.ts +0 -72
- package/test/api-generator.test.ts +0 -588
- package/test/api-key-permission-guard.test.ts +0 -144
- package/test/ast-schema-editor.test.ts +0 -89
- package/test/auth-middleware-hono.test.ts +0 -556
- package/test/auth-routes.test.ts +0 -1098
- package/test/backend-hooks-data.test.ts +0 -477
- package/test/custom-auth-adapter.test.ts +0 -189
- package/test/driver-registry.test.ts +0 -282
- package/test/email-templates.test.ts +0 -174
- package/test/env.test.ts +0 -128
- package/test/error-propagation.test.ts +0 -226
- package/test/errors-hono.test.ts +0 -133
- package/test/errors.test.ts +0 -155
- package/test/function-loader.test.ts +0 -127
- package/test/graphql-schema-generator.test.ts +0 -554
- package/test/jwt-security.test.ts +0 -182
- package/test/jwt.test.ts +0 -326
- package/test/mfa.test.ts +0 -197
- package/test/middleware.test.ts +0 -305
- package/test/multi-datasource-routing.test.ts +0 -113
- package/test/password.test.ts +0 -165
- package/test/query-parser.test.ts +0 -233
- package/test/rate-limiter.test.ts +0 -102
- package/test/reset-password-admin.test.ts +0 -113
- package/test/routed-realtime-service.test.ts +0 -86
- package/test/safe-compare.test.ts +0 -66
- package/test/singleton.test.ts +0 -59
- package/test/smtp-email-service.test.ts +0 -169
- package/test/storage-local.test.ts +0 -271
- package/test/storage-registry.test.ts +0 -282
- package/test/storage-routes.test.ts +0 -222
- package/test/storage-s3.test.ts +0 -304
- package/test/transform-auth-response.test.ts +0 -305
- package/test/webhook-service.test.ts +0 -260
- package/tsconfig.json +0 -54
- package/tsconfig.prod.json +0 -23
- package/vite.config.ts +0 -86
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { logger } from "../utils/logger";
|
|
4
|
-
|
|
5
|
-
export interface LinkedinUserInfo {
|
|
6
|
-
linkedinId: string;
|
|
7
|
-
email: string;
|
|
8
|
-
displayName: string | null;
|
|
9
|
-
photoUrl: string | null;
|
|
10
|
-
emailVerified: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates a LinkedIn OAuth Provider integration
|
|
15
|
-
*/
|
|
16
|
-
export function createLinkedinProvider(config: { clientId: string, clientSecret: string }): OAuthProvider<{ code: string; redirectUri: string }> {
|
|
17
|
-
return {
|
|
18
|
-
id: "linkedin",
|
|
19
|
-
schema: z.object({
|
|
20
|
-
code: z.string().min(1, "Auth code is required"),
|
|
21
|
-
redirectUri: z.string().url("Valid redirect URI is required")
|
|
22
|
-
}),
|
|
23
|
-
verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
|
|
24
|
-
try {
|
|
25
|
-
// Exchange code for access token
|
|
26
|
-
const tokenResponse = await fetch("https://www.linkedin.com/oauth/v2/accessToken", {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: {
|
|
29
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
30
|
-
},
|
|
31
|
-
body: new URLSearchParams({
|
|
32
|
-
grant_type: "authorization_code",
|
|
33
|
-
code: payload.code,
|
|
34
|
-
redirect_uri: payload.redirectUri,
|
|
35
|
-
client_id: config.clientId,
|
|
36
|
-
client_secret: config.clientSecret
|
|
37
|
-
})
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!tokenResponse.ok) {
|
|
41
|
-
const errBody = await tokenResponse.text();
|
|
42
|
-
logger.error("Failed to get LinkedIn access token", { detail: errBody });
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const tokenData = await tokenResponse.json() as { access_token: string; id_token?: string };
|
|
47
|
-
const accessToken = tokenData.access_token;
|
|
48
|
-
|
|
49
|
-
// Fetch User Info using OIDC userinfo endpoint
|
|
50
|
-
const profileResponse = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
51
|
-
headers: {
|
|
52
|
-
"Authorization": `Bearer ${accessToken}`
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (!profileResponse.ok) {
|
|
57
|
-
const errBody = await profileResponse.text();
|
|
58
|
-
logger.error("Failed to get LinkedIn user info", { detail: errBody });
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const profileData = await profileResponse.json() as {
|
|
63
|
-
sub: string;
|
|
64
|
-
email: string;
|
|
65
|
-
name?: string;
|
|
66
|
-
picture?: string;
|
|
67
|
-
email_verified?: boolean;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
providerId: profileData.sub,
|
|
72
|
-
email: profileData.email,
|
|
73
|
-
displayName: profileData.name || null,
|
|
74
|
-
photoUrl: profileData.picture || null,
|
|
75
|
-
emailVerified: profileData.email_verified === true
|
|
76
|
-
};
|
|
77
|
-
} catch (error) {
|
|
78
|
-
logger.error("LinkedIn OAuth error", { error: error });
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import type { AuthModuleConfig } from "./routes";
|
|
3
|
-
import type { ResolvedAuthHooks } from "./auth-hooks";
|
|
4
|
-
import type { HonoEnv } from "../api/types";
|
|
5
|
-
import { ApiError } from "../api/errors";
|
|
6
|
-
import { generateSecureToken, hashToken } from "./admin-user-ops";
|
|
7
|
-
import { getMagicLinkTemplate } from "../email/templates";
|
|
8
|
-
import { strictAuthLimiter } from "./rate-limiter";
|
|
9
|
-
import { z } from "zod";
|
|
10
|
-
import { logger } from "../utils/logger";
|
|
11
|
-
import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Magic link token expiry (15 minutes from now)
|
|
15
|
-
*/
|
|
16
|
-
function getMagicLinkExpiry(): Date {
|
|
17
|
-
return new Date(Date.now() + 15 * 60 * 1000);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Mount magic link routes onto the auth router.
|
|
22
|
-
*
|
|
23
|
-
* Follows the same delegation pattern as `mountMfaRoutes()` and `mountSessionRoutes()`.
|
|
24
|
-
*/
|
|
25
|
-
export function mountMagicLinkRoutes(deps: {
|
|
26
|
-
router: Hono<HonoEnv>;
|
|
27
|
-
config: AuthModuleConfig;
|
|
28
|
-
ops: ResolvedAuthHooks;
|
|
29
|
-
parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;
|
|
30
|
-
buildAuthResponse: (
|
|
31
|
-
user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
|
|
32
|
-
roleIds: string[],
|
|
33
|
-
accessToken: string,
|
|
34
|
-
refreshToken: string
|
|
35
|
-
) => unknown;
|
|
36
|
-
createSessionAndTokens: (userId: string, userAgent: string, ipAddress: string) => Promise<{ roleIds: string[]; accessToken: string; refreshToken: string }>;
|
|
37
|
-
applyTransformHook: (
|
|
38
|
-
response: AuthResponsePayload,
|
|
39
|
-
method: TransformAuthResponseContext["method"],
|
|
40
|
-
request: Request,
|
|
41
|
-
userId: string
|
|
42
|
-
) => Promise<AuthResponsePayload>;
|
|
43
|
-
}) {
|
|
44
|
-
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = deps;
|
|
45
|
-
const { authRepo, emailService, emailConfig } = config;
|
|
46
|
-
|
|
47
|
-
const magicLinkSchema = z.object({
|
|
48
|
-
email: z.string().email("Invalid email address").max(255)
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const verifyMagicLinkSchema = z.object({
|
|
52
|
-
token: z.string().min(1, "Token is required")
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
function isEmailConfigured(): boolean {
|
|
56
|
-
return !!(emailService && emailService.isConfigured());
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* POST /auth/magic-link
|
|
61
|
-
* Request a magic link email
|
|
62
|
-
*/
|
|
63
|
-
router.post("/magic-link", strictAuthLimiter, async (c) => {
|
|
64
|
-
const { email } = parseBody(magicLinkSchema, await c.req.json());
|
|
65
|
-
|
|
66
|
-
// Require email service
|
|
67
|
-
if (!isEmailConfigured()) {
|
|
68
|
-
throw ApiError.serviceUnavailable("Email service not configured. Magic link login is not available.", "EMAIL_NOT_CONFIGURED");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Always return success (security: don't reveal if email exists)
|
|
72
|
-
const user = await authRepo.getUserByEmail(email);
|
|
73
|
-
|
|
74
|
-
if (user) {
|
|
75
|
-
// Call beforeLogin hook if provided (throw to reject)
|
|
76
|
-
if (ops.beforeLogin) {
|
|
77
|
-
await ops.beforeLogin(email, "magic-link");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Generate magic link token
|
|
81
|
-
const token = generateSecureToken();
|
|
82
|
-
const tokenHash = hashToken(token);
|
|
83
|
-
const expiresAt = getMagicLinkExpiry();
|
|
84
|
-
|
|
85
|
-
await authRepo.createMagicLinkToken(user.id, tokenHash, expiresAt);
|
|
86
|
-
|
|
87
|
-
// Build magic link URL
|
|
88
|
-
const baseUrl = emailConfig?.magicLinkUrl || emailConfig?.resetPasswordUrl || "";
|
|
89
|
-
const magicLinkUrl = `${baseUrl}/auth/magic-link?token=${token}`;
|
|
90
|
-
|
|
91
|
-
// Get email template
|
|
92
|
-
const appName = emailConfig?.appName || "Rebase";
|
|
93
|
-
const templateFn = emailConfig?.templates?.magicLink;
|
|
94
|
-
const emailContent = templateFn
|
|
95
|
-
? templateFn(magicLinkUrl, { email: user.email, displayName: user.displayName })
|
|
96
|
-
: getMagicLinkTemplate(magicLinkUrl, { email: user.email, displayName: user.displayName }, appName);
|
|
97
|
-
|
|
98
|
-
// Send email
|
|
99
|
-
try {
|
|
100
|
-
await emailService!.send({
|
|
101
|
-
to: user.email,
|
|
102
|
-
subject: emailContent.subject,
|
|
103
|
-
html: emailContent.html,
|
|
104
|
-
text: emailContent.text
|
|
105
|
-
});
|
|
106
|
-
} catch (emailError: unknown) {
|
|
107
|
-
logger.error("Failed to send magic link email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
108
|
-
// Don't reveal email sending failure to client
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Always return success
|
|
113
|
-
return c.json({
|
|
114
|
-
success: true,
|
|
115
|
-
message: "If an account with that email exists, a magic link has been sent."
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* POST /auth/magic-link/verify
|
|
121
|
-
* Verify magic link token and create session
|
|
122
|
-
*/
|
|
123
|
-
router.post("/magic-link/verify", strictAuthLimiter, async (c) => {
|
|
124
|
-
const { token } = parseBody(verifyMagicLinkSchema, await c.req.json());
|
|
125
|
-
|
|
126
|
-
// Find valid token
|
|
127
|
-
const tokenHash = hashToken(token);
|
|
128
|
-
const storedToken = await authRepo.findValidMagicLinkToken(tokenHash);
|
|
129
|
-
|
|
130
|
-
if (!storedToken) {
|
|
131
|
-
throw ApiError.badRequest("Invalid or expired magic link", "INVALID_TOKEN");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Mark token as used (one-time use)
|
|
135
|
-
await authRepo.markMagicLinkTokenUsed(tokenHash);
|
|
136
|
-
|
|
137
|
-
// Get user
|
|
138
|
-
const user = await authRepo.getUserById(storedToken.userId);
|
|
139
|
-
if (!user) {
|
|
140
|
-
throw ApiError.badRequest("Invalid or expired magic link", "INVALID_TOKEN");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Clicking a magic link proves email ownership — auto-verify
|
|
144
|
-
if (!user.emailVerified) {
|
|
145
|
-
await authRepo.setEmailVerified(user.id, true);
|
|
146
|
-
user.emailVerified = true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Create session
|
|
150
|
-
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
|
|
151
|
-
user.id,
|
|
152
|
-
c.req.header("user-agent") || "unknown",
|
|
153
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
// Fire onAuthenticated hook (fire-and-forget)
|
|
157
|
-
if (ops.onAuthenticated) {
|
|
158
|
-
ops.onAuthenticated(user, "magic-link").catch(err => {
|
|
159
|
-
logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken) as AuthResponsePayload;
|
|
164
|
-
const finalResponse = await applyTransformHook(authResponse, "magic-link", c.req.raw, user.id);
|
|
165
|
-
return c.json(finalResponse);
|
|
166
|
-
});
|
|
167
|
-
}
|
package/src/auth/mfa-crypto.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Envelope encryption for TOTP secrets using AES-256-GCM.
|
|
3
|
-
*
|
|
4
|
-
* - Derives a 32-byte key via SHA-256 from `MFA_ENCRYPTION_KEY` or `JWT_SECRET`.
|
|
5
|
-
* - Generates a random 12-byte IV per encryption call.
|
|
6
|
-
* - Returns ciphertexts in the format `iv_hex:authTag_hex:ciphertext_hex`.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
|
|
12
|
-
import { logger } from "../utils/logger";
|
|
13
|
-
|
|
14
|
-
const ALGORITHM = "aes-256-gcm";
|
|
15
|
-
const IV_BYTES = 12;
|
|
16
|
-
const AUTH_TAG_LENGTH = 16;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Resolve the raw key string from environment variables.
|
|
20
|
-
*
|
|
21
|
-
* Prefers `MFA_ENCRYPTION_KEY`; falls back to `JWT_SECRET` with a warning.
|
|
22
|
-
* Throws if neither is set.
|
|
23
|
-
*/
|
|
24
|
-
function resolveKeyString(): string {
|
|
25
|
-
const explicit = process.env.MFA_ENCRYPTION_KEY;
|
|
26
|
-
if (explicit) {
|
|
27
|
-
return explicit;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const fallback = process.env.JWT_SECRET;
|
|
31
|
-
if (fallback) {
|
|
32
|
-
logger.warn(
|
|
33
|
-
"[MFA-Crypto] MFA_ENCRYPTION_KEY is not set — falling back to JWT_SECRET. " +
|
|
34
|
-
"Set a dedicated MFA_ENCRYPTION_KEY for production."
|
|
35
|
-
);
|
|
36
|
-
return fallback;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
throw new Error(
|
|
40
|
-
"Cannot encrypt TOTP secret: neither MFA_ENCRYPTION_KEY nor JWT_SECRET is configured."
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Derive a deterministic 32-byte AES-256 key from the raw secret string.
|
|
46
|
-
*/
|
|
47
|
-
function deriveKey(raw: string): Buffer {
|
|
48
|
-
return createHash("sha256").update(raw).digest();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Encrypt a plaintext TOTP secret.
|
|
53
|
-
*
|
|
54
|
-
* @param plaintext - The Base32 TOTP secret to encrypt.
|
|
55
|
-
* @returns A string in the format `iv_hex:authTag_hex:ciphertext_hex`.
|
|
56
|
-
*/
|
|
57
|
-
export function encryptTotpSecret(plaintext: string): string {
|
|
58
|
-
const key = deriveKey(resolveKeyString());
|
|
59
|
-
const iv = randomBytes(IV_BYTES);
|
|
60
|
-
|
|
61
|
-
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
62
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
63
|
-
const authTag = cipher.getAuthTag();
|
|
64
|
-
|
|
65
|
-
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Decrypt a previously encrypted TOTP secret.
|
|
70
|
-
*
|
|
71
|
-
* @param ciphertext - A string in the format `iv_hex:authTag_hex:ciphertext_hex`.
|
|
72
|
-
* @returns The original Base32 TOTP secret.
|
|
73
|
-
*/
|
|
74
|
-
export function decryptTotpSecret(ciphertext: string): string {
|
|
75
|
-
const parts = ciphertext.split(":");
|
|
76
|
-
if (parts.length !== 3) {
|
|
77
|
-
throw new Error("Invalid encrypted TOTP secret format — expected iv:authTag:ciphertext");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
81
|
-
const key = deriveKey(resolveKeyString());
|
|
82
|
-
const iv = Buffer.from(ivHex, "hex");
|
|
83
|
-
const authTag = Buffer.from(authTagHex, "hex");
|
|
84
|
-
const encrypted = Buffer.from(encryptedHex, "hex");
|
|
85
|
-
|
|
86
|
-
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
87
|
-
decipher.setAuthTag(authTag);
|
|
88
|
-
|
|
89
|
-
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
90
|
-
return decrypted.toString("utf8");
|
|
91
|
-
}
|
package/src/auth/mfa-routes.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { ApiError } from "../api/errors";
|
|
4
|
-
import { HonoEnv } from "../api/types";
|
|
5
|
-
import { requireAuth } from "./middleware";
|
|
6
|
-
import { logger } from "../utils/logger";
|
|
7
|
-
import {
|
|
8
|
-
generateTotpSecret,
|
|
9
|
-
verifyTotp,
|
|
10
|
-
base32Decode,
|
|
11
|
-
generateRecoveryCodes,
|
|
12
|
-
hashRecoveryCode
|
|
13
|
-
} from "./mfa";
|
|
14
|
-
import { encryptTotpSecret, decryptTotpSecret } from "./mfa-crypto";
|
|
15
|
-
import {
|
|
16
|
-
generateAccessToken,
|
|
17
|
-
generateRefreshToken,
|
|
18
|
-
hashRefreshToken,
|
|
19
|
-
getRefreshTokenExpiry,
|
|
20
|
-
getAccessTokenExpiry,
|
|
21
|
-
type AccessTokenPayload
|
|
22
|
-
} from "./jwt";
|
|
23
|
-
import type { AuthModuleConfig } from "./routes";
|
|
24
|
-
import { resolveAuthHooks } from "./auth-hooks";
|
|
25
|
-
import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
|
|
26
|
-
|
|
27
|
-
export function mountMfaRoutes(
|
|
28
|
-
router: Hono<HonoEnv>,
|
|
29
|
-
config: AuthModuleConfig,
|
|
30
|
-
ops: ReturnType<typeof resolveAuthHooks>,
|
|
31
|
-
parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T,
|
|
32
|
-
applyTransformHook?: (
|
|
33
|
-
response: AuthResponsePayload,
|
|
34
|
-
method: TransformAuthResponseContext["method"],
|
|
35
|
-
request: Request,
|
|
36
|
-
userId: string
|
|
37
|
-
) => Promise<AuthResponsePayload>
|
|
38
|
-
): void {
|
|
39
|
-
const authRepo = config.authRepo;
|
|
40
|
-
const emailConfig = config.emailConfig;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* POST /auth/mfa/enroll
|
|
44
|
-
* Start MFA enrollment: generate TOTP secret and recovery codes
|
|
45
|
-
*/
|
|
46
|
-
router.post("/mfa/enroll", requireAuth, async (c) => {
|
|
47
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
48
|
-
if (!userCtx) {
|
|
49
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const body = (await c.req.json().catch(() => ({}))) as Record<string, unknown>;
|
|
53
|
-
const friendlyName = typeof body.friendlyName === "string" ? body.friendlyName : undefined;
|
|
54
|
-
const issuer = typeof body.issuer === "string" ? body.issuer : emailConfig?.appName || "Rebase";
|
|
55
|
-
|
|
56
|
-
// Get user for account name
|
|
57
|
-
const user = await authRepo.getUserById(userCtx.userId);
|
|
58
|
-
if (!user) {
|
|
59
|
-
throw ApiError.notFound("User not found");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Generate TOTP secret
|
|
63
|
-
const { secret, uri } = generateTotpSecret(issuer, user.email);
|
|
64
|
-
|
|
65
|
-
// Encrypt the TOTP secret before storage (AES-256-GCM envelope encryption)
|
|
66
|
-
const encryptedSecret = encryptTotpSecret(secret);
|
|
67
|
-
const factor = await authRepo.createMfaFactor(
|
|
68
|
-
user.id,
|
|
69
|
-
"totp",
|
|
70
|
-
encryptedSecret,
|
|
71
|
-
friendlyName
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
// Generate recovery codes
|
|
75
|
-
const codes = generateRecoveryCodes(10);
|
|
76
|
-
const codeHashes = codes.map(hashRecoveryCode);
|
|
77
|
-
await authRepo.createRecoveryCodes(user.id, codeHashes);
|
|
78
|
-
|
|
79
|
-
return c.json(
|
|
80
|
-
{
|
|
81
|
-
factor: {
|
|
82
|
-
id: factor.id,
|
|
83
|
-
factorType: factor.factorType,
|
|
84
|
-
friendlyName: factor.friendlyName
|
|
85
|
-
},
|
|
86
|
-
totp: {
|
|
87
|
-
secret,
|
|
88
|
-
uri,
|
|
89
|
-
qrUri: uri
|
|
90
|
-
},
|
|
91
|
-
recoveryCodes: codes
|
|
92
|
-
},
|
|
93
|
-
201
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* POST /auth/mfa/verify
|
|
99
|
-
* Verify TOTP code to complete MFA enrollment
|
|
100
|
-
*/
|
|
101
|
-
router.post("/mfa/verify", requireAuth, async (c) => {
|
|
102
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
103
|
-
if (!userCtx) {
|
|
104
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const verifySchema = z.object({
|
|
108
|
-
factorId: z.string().min(1, "Factor ID is required"),
|
|
109
|
-
code: z.string().length(6, "Code must be 6 digits")
|
|
110
|
-
});
|
|
111
|
-
const { factorId, code } = parseBody(verifySchema, await c.req.json());
|
|
112
|
-
|
|
113
|
-
// Get the factor
|
|
114
|
-
const factor = await authRepo.getMfaFactorById(factorId);
|
|
115
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
116
|
-
throw ApiError.notFound("MFA factor not found");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (factor.verified) {
|
|
120
|
-
throw ApiError.badRequest("Factor is already verified", "ALREADY_VERIFIED");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Decrypt and verify the TOTP code
|
|
124
|
-
const decryptedSecret = decryptTotpSecret(factor.secretEncrypted);
|
|
125
|
-
const secretBuffer = base32Decode(decryptedSecret);
|
|
126
|
-
const isValid = verifyTotp(secretBuffer, code);
|
|
127
|
-
|
|
128
|
-
if (!isValid) {
|
|
129
|
-
throw ApiError.unauthorized("Invalid TOTP code", "INVALID_CODE");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Mark factor as verified
|
|
133
|
-
await authRepo.verifyMfaFactor(factorId);
|
|
134
|
-
|
|
135
|
-
return c.json({
|
|
136
|
-
success: true,
|
|
137
|
-
message: "MFA factor verified and enrolled"
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* POST /auth/mfa/challenge
|
|
143
|
-
* Create an MFA challenge during login (user has MFA enrolled)
|
|
144
|
-
*/
|
|
145
|
-
router.post("/mfa/challenge", requireAuth, async (c) => {
|
|
146
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
147
|
-
if (!userCtx) {
|
|
148
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const challengeSchema = z.object({
|
|
152
|
-
factorId: z.string().min(1, "Factor ID is required")
|
|
153
|
-
});
|
|
154
|
-
const { factorId } = parseBody(challengeSchema, await c.req.json());
|
|
155
|
-
|
|
156
|
-
// Verify the factor belongs to this user and is verified
|
|
157
|
-
const factor = await authRepo.getMfaFactorById(factorId);
|
|
158
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
159
|
-
throw ApiError.notFound("MFA factor not found");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!factor.verified) {
|
|
163
|
-
throw ApiError.badRequest("MFA factor is not yet verified", "FACTOR_NOT_VERIFIED");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
167
|
-
const challenge = await authRepo.createMfaChallenge(factorId, ipAddress);
|
|
168
|
-
|
|
169
|
-
return c.json({
|
|
170
|
-
challengeId: challenge.id,
|
|
171
|
-
factorId: challenge.factorId,
|
|
172
|
-
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* POST /auth/mfa/challenge/verify
|
|
178
|
-
* Verify a TOTP code for an active challenge, upgrade aal1 → aal2
|
|
179
|
-
*/
|
|
180
|
-
router.post("/mfa/challenge/verify", requireAuth, async (c) => {
|
|
181
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
182
|
-
if (!userCtx) {
|
|
183
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const challengeVerifySchema = z.object({
|
|
187
|
-
challengeId: z.string().min(1, "Challenge ID is required"),
|
|
188
|
-
code: z.string().min(1, "Code is required")
|
|
189
|
-
});
|
|
190
|
-
const { challengeId, code } = parseBody(challengeVerifySchema, await c.req.json());
|
|
191
|
-
|
|
192
|
-
// Find the challenge
|
|
193
|
-
const challenge = await authRepo.getMfaChallengeById(challengeId);
|
|
194
|
-
if (!challenge) {
|
|
195
|
-
throw ApiError.badRequest("Invalid or expired challenge", "INVALID_CHALLENGE");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Get the factor and verify ownership
|
|
199
|
-
const factor = await authRepo.getMfaFactorById(challenge.factorId);
|
|
200
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
201
|
-
throw ApiError.notFound("MFA factor not found");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Try TOTP verification first (standard 6-digit codes)
|
|
205
|
-
const decryptedSecret = decryptTotpSecret(factor.secretEncrypted);
|
|
206
|
-
const secretBuffer = base32Decode(decryptedSecret);
|
|
207
|
-
let isValid = verifyTotp(secretBuffer, code);
|
|
208
|
-
|
|
209
|
-
// Fall back to recovery code verification if TOTP didn't match
|
|
210
|
-
if (!isValid) {
|
|
211
|
-
const codeHash = hashRecoveryCode(code);
|
|
212
|
-
isValid = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (!isValid) {
|
|
216
|
-
throw ApiError.unauthorized("Invalid verification code", "INVALID_CODE");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Mark challenge as verified
|
|
220
|
-
await authRepo.verifyMfaChallenge(challengeId);
|
|
221
|
-
|
|
222
|
-
// Generate new access token with aal2
|
|
223
|
-
const roles = await authRepo.getUserRoles(userCtx.userId);
|
|
224
|
-
const roleIds = roles.map((r) => r.id);
|
|
225
|
-
const accessToken = generateAccessToken(userCtx.userId, roleIds, "aal2");
|
|
226
|
-
const refreshToken = generateRefreshToken();
|
|
227
|
-
|
|
228
|
-
// Create new refresh token
|
|
229
|
-
await authRepo.createRefreshToken(
|
|
230
|
-
userCtx.userId,
|
|
231
|
-
hashRefreshToken(refreshToken),
|
|
232
|
-
getRefreshTokenExpiry(),
|
|
233
|
-
c.req.header("user-agent") || "unknown",
|
|
234
|
-
c.req.header("x-forwarded-for") || "unknown"
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
// Fire onMfaVerified hook
|
|
238
|
-
if (ops.onMfaVerified) {
|
|
239
|
-
ops.onMfaVerified(userCtx.userId, factor.id).catch((err) => {
|
|
240
|
-
logger.error("[AuthHooks] onMfaVerified error", {
|
|
241
|
-
error: err instanceof Error ? err.message : err
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
let mfaResponse: AuthResponsePayload = {
|
|
247
|
-
tokens: {
|
|
248
|
-
accessToken,
|
|
249
|
-
refreshToken,
|
|
250
|
-
accessTokenExpiresAt: getAccessTokenExpiry()
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
if (applyTransformHook) {
|
|
254
|
-
mfaResponse = await applyTransformHook(mfaResponse, "mfa", c.req.raw, userCtx.userId);
|
|
255
|
-
}
|
|
256
|
-
return c.json(mfaResponse);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* GET /auth/mfa/factors
|
|
261
|
-
* List enrolled MFA factors for the current user
|
|
262
|
-
*/
|
|
263
|
-
router.get("/mfa/factors", requireAuth, async (c) => {
|
|
264
|
-
const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
|
|
265
|
-
if (!userCtx) {
|
|
266
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const factors = await authRepo.getMfaFactors(userCtx.userId);
|
|
270
|
-
return c.json({
|
|
271
|
-
factors: factors.map((f) => ({
|
|
272
|
-
id: f.id,
|
|
273
|
-
factorType: f.factorType,
|
|
274
|
-
friendlyName: f.friendlyName,
|
|
275
|
-
verified: f.verified,
|
|
276
|
-
createdAt: f.createdAt
|
|
277
|
-
}))
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* DELETE /auth/mfa/unenroll
|
|
283
|
-
* Remove an MFA factor
|
|
284
|
-
*/
|
|
285
|
-
router.delete("/mfa/unenroll", requireAuth, async (c) => {
|
|
286
|
-
const userCtx = c.get("user") as AccessTokenPayload | undefined;
|
|
287
|
-
if (!userCtx) {
|
|
288
|
-
throw ApiError.unauthorized("Not authenticated");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Require aal2 (MFA-verified session) to unenroll MFA factors
|
|
292
|
-
if (userCtx.aal !== "aal2") {
|
|
293
|
-
throw ApiError.forbidden(
|
|
294
|
-
"MFA verification required to unenroll. Please re-authenticate with your second factor.",
|
|
295
|
-
"AAL2_REQUIRED"
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const unenrollSchema = z.object({
|
|
300
|
-
factorId: z.string().min(1, "Factor ID is required")
|
|
301
|
-
});
|
|
302
|
-
const { factorId } = parseBody(unenrollSchema, await c.req.json());
|
|
303
|
-
|
|
304
|
-
// Verify ownership
|
|
305
|
-
const factor = await authRepo.getMfaFactorById(factorId);
|
|
306
|
-
if (!factor || factor.userId !== userCtx.userId) {
|
|
307
|
-
throw ApiError.notFound("MFA factor not found");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
await authRepo.deleteMfaFactor(factorId, userCtx.userId);
|
|
311
|
-
|
|
312
|
-
// If no more verified factors, clean up recovery codes
|
|
313
|
-
const hasFactors = await authRepo.hasVerifiedMfaFactors(userCtx.userId);
|
|
314
|
-
if (!hasFactors) {
|
|
315
|
-
await authRepo.deleteAllRecoveryCodes(userCtx.userId);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return c.json({
|
|
319
|
-
success: true,
|
|
320
|
-
message: "MFA factor removed"
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
}
|