@spinajs/rbac-http-user 2.0.485 → 2.0.487
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/lib/cjs/config/rbac-http.d.ts +0 -15
- package/lib/cjs/config/rbac-http.d.ts.map +1 -1
- package/lib/cjs/config/rbac-http.js +6 -16
- package/lib/cjs/config/rbac-http.js.map +1 -1
- package/lib/cjs/controllers/ActiveRoleController.d.ts +2 -1
- package/lib/cjs/controllers/ActiveRoleController.d.ts.map +1 -1
- package/lib/cjs/controllers/ActiveRoleController.js +7 -17
- package/lib/cjs/controllers/ActiveRoleController.js.map +1 -1
- package/lib/cjs/controllers/ImpersonationController.d.ts +7 -9
- package/lib/cjs/controllers/ImpersonationController.d.ts.map +1 -1
- package/lib/cjs/controllers/ImpersonationController.js +31 -56
- package/lib/cjs/controllers/ImpersonationController.js.map +1 -1
- package/lib/cjs/controllers/LoginController.d.ts +12 -5
- package/lib/cjs/controllers/LoginController.d.ts.map +1 -1
- package/lib/cjs/controllers/LoginController.js +39 -40
- package/lib/cjs/controllers/LoginController.js.map +1 -1
- package/lib/cjs/controllers/PasswordResetController.d.ts +54 -0
- package/lib/cjs/controllers/PasswordResetController.d.ts.map +1 -0
- package/lib/cjs/controllers/PasswordResetController.js +129 -0
- package/lib/cjs/controllers/PasswordResetController.js.map +1 -0
- package/lib/cjs/controllers/SessionsController.d.ts +65 -0
- package/lib/cjs/controllers/SessionsController.d.ts.map +1 -0
- package/lib/cjs/controllers/SessionsController.js +143 -0
- package/lib/cjs/controllers/SessionsController.js.map +1 -0
- package/lib/cjs/controllers/TwoFactorAuthController.d.ts +24 -23
- package/lib/cjs/controllers/TwoFactorAuthController.d.ts.map +1 -1
- package/lib/cjs/controllers/TwoFactorAuthController.js +39 -72
- package/lib/cjs/controllers/TwoFactorAuthController.js.map +1 -1
- package/lib/cjs/controllers/TwoFactorAuthUserController.d.ts +98 -0
- package/lib/cjs/controllers/TwoFactorAuthUserController.d.ts.map +1 -0
- package/lib/cjs/controllers/TwoFactorAuthUserController.js +195 -0
- package/lib/cjs/controllers/TwoFactorAuthUserController.js.map +1 -0
- package/lib/cjs/controllers/UserController.d.ts +10 -7
- package/lib/cjs/controllers/UserController.d.ts.map +1 -1
- package/lib/cjs/controllers/UserController.js +56 -58
- package/lib/cjs/controllers/UserController.js.map +1 -1
- package/lib/cjs/controllers/UserMetadataController.d.ts +6 -12
- package/lib/cjs/controllers/UserMetadataController.d.ts.map +1 -1
- package/lib/cjs/controllers/UserMetadataController.js +23 -72
- package/lib/cjs/controllers/UserMetadataController.js.map +1 -1
- package/lib/cjs/dto/confirm-password-dto.d.ts +23 -0
- package/lib/cjs/dto/confirm-password-dto.d.ts.map +1 -0
- package/lib/cjs/dto/{restore-password-dto.js → confirm-password-dto.js} +16 -12
- package/lib/cjs/dto/confirm-password-dto.js.map +1 -0
- package/lib/cjs/dto/password-reset-dto.d.ts +58 -0
- package/lib/cjs/dto/password-reset-dto.d.ts.map +1 -0
- package/lib/cjs/dto/password-reset-dto.js +57 -0
- package/lib/cjs/dto/password-reset-dto.js.map +1 -0
- package/lib/cjs/handlers/DefaultLogoutHandler.d.ts +4 -1
- package/lib/cjs/handlers/DefaultLogoutHandler.d.ts.map +1 -1
- package/lib/cjs/handlers/DefaultLogoutHandler.js +20 -13
- package/lib/cjs/handlers/DefaultLogoutHandler.js.map +1 -1
- package/lib/cjs/handlers/ImpersonationLogoutHandler.d.ts +4 -7
- package/lib/cjs/handlers/ImpersonationLogoutHandler.d.ts.map +1 -1
- package/lib/cjs/handlers/ImpersonationLogoutHandler.js +30 -31
- package/lib/cjs/handlers/ImpersonationLogoutHandler.js.map +1 -1
- package/lib/cjs/index.d.ts +20 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +22 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/logout.d.ts +7 -0
- package/lib/cjs/logout.d.ts.map +1 -1
- package/lib/cjs/logout.js.map +1 -1
- package/lib/cjs/policies/2FaPolicy.d.ts +21 -1
- package/lib/cjs/policies/2FaPolicy.d.ts.map +1 -1
- package/lib/cjs/policies/2FaPolicy.js +31 -7
- package/lib/cjs/policies/2FaPolicy.js.map +1 -1
- package/lib/cjs/services/ImpersonationService.d.ts +90 -0
- package/lib/cjs/services/ImpersonationService.d.ts.map +1 -0
- package/lib/cjs/services/ImpersonationService.js +147 -0
- package/lib/cjs/services/ImpersonationService.js.map +1 -0
- package/lib/cjs/services/SessionCookies.d.ts +41 -0
- package/lib/cjs/services/SessionCookies.d.ts.map +1 -0
- package/lib/cjs/services/SessionCookies.js +62 -0
- package/lib/cjs/services/SessionCookies.js.map +1 -0
- package/lib/cjs/services/UserMetadataService.d.ts +53 -0
- package/lib/cjs/services/UserMetadataService.d.ts.map +1 -0
- package/lib/cjs/services/UserMetadataService.js +94 -0
- package/lib/cjs/services/UserMetadataService.js.map +1 -0
- package/lib/cjs/services/grants.d.ts +29 -0
- package/lib/cjs/services/grants.d.ts.map +1 -0
- package/lib/cjs/services/grants.js +43 -0
- package/lib/cjs/services/grants.js.map +1 -0
- package/lib/mjs/config/rbac-http.d.ts +0 -15
- package/lib/mjs/config/rbac-http.d.ts.map +1 -1
- package/lib/mjs/config/rbac-http.js +6 -16
- package/lib/mjs/config/rbac-http.js.map +1 -1
- package/lib/mjs/controllers/ActiveRoleController.d.ts +2 -1
- package/lib/mjs/controllers/ActiveRoleController.d.ts.map +1 -1
- package/lib/mjs/controllers/ActiveRoleController.js +8 -18
- package/lib/mjs/controllers/ActiveRoleController.js.map +1 -1
- package/lib/mjs/controllers/ImpersonationController.d.ts +7 -9
- package/lib/mjs/controllers/ImpersonationController.d.ts.map +1 -1
- package/lib/mjs/controllers/ImpersonationController.js +32 -57
- package/lib/mjs/controllers/ImpersonationController.js.map +1 -1
- package/lib/mjs/controllers/LoginController.d.ts +10 -3
- package/lib/mjs/controllers/LoginController.d.ts.map +1 -1
- package/lib/mjs/controllers/LoginController.js +42 -43
- package/lib/mjs/controllers/LoginController.js.map +1 -1
- package/lib/mjs/controllers/PasswordResetController.d.ts +54 -0
- package/lib/mjs/controllers/PasswordResetController.d.ts.map +1 -0
- package/lib/mjs/controllers/PasswordResetController.js +126 -0
- package/lib/mjs/controllers/PasswordResetController.js.map +1 -0
- package/lib/mjs/controllers/SessionsController.d.ts +65 -0
- package/lib/mjs/controllers/SessionsController.d.ts.map +1 -0
- package/lib/mjs/controllers/SessionsController.js +140 -0
- package/lib/mjs/controllers/SessionsController.js.map +1 -0
- package/lib/mjs/controllers/TwoFactorAuthController.d.ts +24 -23
- package/lib/mjs/controllers/TwoFactorAuthController.d.ts.map +1 -1
- package/lib/mjs/controllers/TwoFactorAuthController.js +43 -76
- package/lib/mjs/controllers/TwoFactorAuthController.js.map +1 -1
- package/lib/mjs/controllers/TwoFactorAuthUserController.d.ts +98 -0
- package/lib/mjs/controllers/TwoFactorAuthUserController.d.ts.map +1 -0
- package/lib/mjs/controllers/TwoFactorAuthUserController.js +192 -0
- package/lib/mjs/controllers/TwoFactorAuthUserController.js.map +1 -0
- package/lib/mjs/controllers/UserController.d.ts +10 -7
- package/lib/mjs/controllers/UserController.d.ts.map +1 -1
- package/lib/mjs/controllers/UserController.js +59 -28
- package/lib/mjs/controllers/UserController.js.map +1 -1
- package/lib/mjs/controllers/UserMetadataController.d.ts +6 -12
- package/lib/mjs/controllers/UserMetadataController.d.ts.map +1 -1
- package/lib/mjs/controllers/UserMetadataController.js +23 -72
- package/lib/mjs/controllers/UserMetadataController.js.map +1 -1
- package/lib/mjs/dto/confirm-password-dto.d.ts +23 -0
- package/lib/mjs/dto/confirm-password-dto.d.ts.map +1 -0
- package/lib/mjs/dto/{user-dto.js → confirm-password-dto.js} +15 -10
- package/lib/mjs/dto/confirm-password-dto.js.map +1 -0
- package/lib/mjs/dto/password-reset-dto.d.ts +58 -0
- package/lib/mjs/dto/password-reset-dto.d.ts.map +1 -0
- package/lib/mjs/dto/password-reset-dto.js +54 -0
- package/lib/mjs/dto/password-reset-dto.js.map +1 -0
- package/lib/mjs/handlers/DefaultLogoutHandler.d.ts +4 -1
- package/lib/mjs/handlers/DefaultLogoutHandler.d.ts.map +1 -1
- package/lib/mjs/handlers/DefaultLogoutHandler.js +23 -16
- package/lib/mjs/handlers/DefaultLogoutHandler.js.map +1 -1
- package/lib/mjs/handlers/ImpersonationLogoutHandler.d.ts +4 -7
- package/lib/mjs/handlers/ImpersonationLogoutHandler.d.ts.map +1 -1
- package/lib/mjs/handlers/ImpersonationLogoutHandler.js +31 -32
- package/lib/mjs/handlers/ImpersonationLogoutHandler.js.map +1 -1
- package/lib/mjs/index.d.ts +20 -0
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +22 -0
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/logout.d.ts +7 -0
- package/lib/mjs/logout.d.ts.map +1 -1
- package/lib/mjs/logout.js.map +1 -1
- package/lib/mjs/policies/2FaPolicy.d.ts +21 -1
- package/lib/mjs/policies/2FaPolicy.d.ts.map +1 -1
- package/lib/mjs/policies/2FaPolicy.js +29 -6
- package/lib/mjs/policies/2FaPolicy.js.map +1 -1
- package/lib/mjs/services/ImpersonationService.d.ts +90 -0
- package/lib/mjs/services/ImpersonationService.d.ts.map +1 -0
- package/lib/mjs/services/ImpersonationService.js +144 -0
- package/lib/mjs/services/ImpersonationService.js.map +1 -0
- package/lib/mjs/services/SessionCookies.d.ts +41 -0
- package/lib/mjs/services/SessionCookies.d.ts.map +1 -0
- package/lib/mjs/services/SessionCookies.js +59 -0
- package/lib/mjs/services/SessionCookies.js.map +1 -0
- package/lib/mjs/services/UserMetadataService.d.ts +53 -0
- package/lib/mjs/services/UserMetadataService.d.ts.map +1 -0
- package/lib/mjs/services/UserMetadataService.js +91 -0
- package/lib/mjs/services/UserMetadataService.js.map +1 -0
- package/lib/mjs/services/grants.d.ts +29 -0
- package/lib/mjs/services/grants.d.ts.map +1 -0
- package/lib/mjs/services/grants.js +38 -0
- package/lib/mjs/services/grants.js.map +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +17 -11
- package/lib/cjs/controllers/AuthController.d.ts +0 -1
- package/lib/cjs/controllers/AuthController.d.ts.map +0 -1
- package/lib/cjs/controllers/AuthController.js +0 -211
- package/lib/cjs/controllers/AuthController.js.map +0 -1
- package/lib/cjs/controllers/FederatedLoginController.d.ts +0 -1
- package/lib/cjs/controllers/FederatedLoginController.d.ts.map +0 -1
- package/lib/cjs/controllers/FederatedLoginController.js +0 -38
- package/lib/cjs/controllers/FederatedLoginController.js.map +0 -1
- package/lib/cjs/dto/login-dto.d.ts +0 -17
- package/lib/cjs/dto/login-dto.d.ts.map +0 -1
- package/lib/cjs/dto/login-dto.js +0 -33
- package/lib/cjs/dto/login-dto.js.map +0 -1
- package/lib/cjs/dto/restore-password-dto.d.ts +0 -24
- package/lib/cjs/dto/restore-password-dto.d.ts.map +0 -1
- package/lib/cjs/dto/restore-password-dto.js.map +0 -1
- package/lib/cjs/dto/user-dto.d.ts +0 -22
- package/lib/cjs/dto/user-dto.d.ts.map +0 -1
- package/lib/cjs/dto/user-dto.js +0 -33
- package/lib/cjs/dto/user-dto.js.map +0 -1
- package/lib/cjs/fingerprint/FingerprintJs.d.ts +0 -1
- package/lib/cjs/fingerprint/FingerprintJs.d.ts.map +0 -1
- package/lib/cjs/fingerprint/FingerprintJs.js +0 -5
- package/lib/cjs/fingerprint/FingerprintJs.js.map +0 -1
- package/lib/cjs/middlewares/AttributeFilter.d.ts +0 -11
- package/lib/cjs/middlewares/AttributeFilter.d.ts.map +0 -1
- package/lib/cjs/middlewares/AttributeFilter.js +0 -19
- package/lib/cjs/middlewares/AttributeFilter.js.map +0 -1
- package/lib/cjs/policies/AllowFederatedLoginPolicy.d.ts +0 -7
- package/lib/cjs/policies/AllowFederatedLoginPolicy.d.ts.map +0 -1
- package/lib/cjs/policies/AllowFederatedLoginPolicy.js +0 -32
- package/lib/cjs/policies/AllowFederatedLoginPolicy.js.map +0 -1
- package/lib/cjs/policies/CaptchaPolicy.d.ts +0 -1
- package/lib/cjs/policies/CaptchaPolicy.d.ts.map +0 -1
- package/lib/cjs/policies/CaptchaPolicy.js +0 -1
- package/lib/cjs/policies/CaptchaPolicy.js.map +0 -1
- package/lib/mjs/controllers/AuthController.d.ts +0 -2
- package/lib/mjs/controllers/AuthController.d.ts.map +0 -1
- package/lib/mjs/controllers/AuthController.js +0 -212
- package/lib/mjs/controllers/AuthController.js.map +0 -1
- package/lib/mjs/controllers/FederatedLoginController.d.ts +0 -2
- package/lib/mjs/controllers/FederatedLoginController.d.ts.map +0 -1
- package/lib/mjs/controllers/FederatedLoginController.js +0 -39
- package/lib/mjs/controllers/FederatedLoginController.js.map +0 -1
- package/lib/mjs/dto/login-dto.d.ts +0 -17
- package/lib/mjs/dto/login-dto.d.ts.map +0 -1
- package/lib/mjs/dto/login-dto.js +0 -30
- package/lib/mjs/dto/login-dto.js.map +0 -1
- package/lib/mjs/dto/restore-password-dto.d.ts +0 -24
- package/lib/mjs/dto/restore-password-dto.d.ts.map +0 -1
- package/lib/mjs/dto/restore-password-dto.js +0 -31
- package/lib/mjs/dto/restore-password-dto.js.map +0 -1
- package/lib/mjs/dto/user-dto.d.ts +0 -22
- package/lib/mjs/dto/user-dto.d.ts.map +0 -1
- package/lib/mjs/dto/user-dto.js.map +0 -1
- package/lib/mjs/fingerprint/FingerprintJs.d.ts +0 -2
- package/lib/mjs/fingerprint/FingerprintJs.d.ts.map +0 -1
- package/lib/mjs/fingerprint/FingerprintJs.js +0 -6
- package/lib/mjs/fingerprint/FingerprintJs.js.map +0 -1
- package/lib/mjs/middlewares/AttributeFilter.d.ts +0 -11
- package/lib/mjs/middlewares/AttributeFilter.d.ts.map +0 -1
- package/lib/mjs/middlewares/AttributeFilter.js +0 -15
- package/lib/mjs/middlewares/AttributeFilter.js.map +0 -1
- package/lib/mjs/policies/AllowFederatedLoginPolicy.d.ts +0 -7
- package/lib/mjs/policies/AllowFederatedLoginPolicy.d.ts.map +0 -1
- package/lib/mjs/policies/AllowFederatedLoginPolicy.js +0 -28
- package/lib/mjs/policies/AllowFederatedLoginPolicy.js.map +0 -1
- package/lib/mjs/policies/CaptchaPolicy.d.ts +0 -2
- package/lib/mjs/policies/CaptchaPolicy.d.ts.map +0 -1
- package/lib/mjs/policies/CaptchaPolicy.js +0 -2
- package/lib/mjs/policies/CaptchaPolicy.js.map +0 -1
|
@@ -11,61 +11,58 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
13
|
import { TokenDto } from './../dto/token-dto.js';
|
|
14
|
-
import { BaseController, BasePath, Ok, Post,
|
|
15
|
-
import { SessionProvider, User as UserModel,
|
|
16
|
-
import { Session } from
|
|
14
|
+
import { BaseController, BasePath, Ok, Post, ForbiddenResponse } from '@spinajs/http';
|
|
15
|
+
import { SessionProvider, User as UserModel, AccessControl, regenerateSession } from '@spinajs/rbac';
|
|
16
|
+
import { Session } from '@spinajs/rbac-http';
|
|
17
17
|
import { Body, Policy } from '@spinajs/http';
|
|
18
18
|
import { TwoFacRouteEnabled } from '../policies/2FaPolicy.js';
|
|
19
|
-
import { AutoinjectService
|
|
19
|
+
import { AutoinjectService } from '@spinajs/configuration';
|
|
20
20
|
import { Autoinject } from '@spinajs/di';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { auth2Fa, disableUser2Fa } from "./../actions/2fa.js";
|
|
24
|
-
import { enableUser2Fa } from "../actions/2fa.js";
|
|
21
|
+
import { User, NotAuthorizedPolicy } from '@spinajs/rbac-http';
|
|
22
|
+
import { auth2Fa, enableUser2Fa } from '../actions/2fa.js';
|
|
25
23
|
import { InvalidOperation } from '@spinajs/exceptions';
|
|
24
|
+
import { TWO_FA_METATADATA_KEYS } from '../2fa/Default2FaToken.js';
|
|
25
|
+
import { SessionCookieFactory } from '../services/SessionCookies.js';
|
|
26
|
+
import { activeRoleOf, buildUserWithGrants } from '../services/grants.js';
|
|
26
27
|
/**
|
|
27
|
-
* Two-factor authentication
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Two-factor authentication during login.
|
|
29
|
+
*
|
|
30
|
+
* These routes serve the narrow window between a successful password check and
|
|
31
|
+
* full authorization: the session is `Logged` but not `Authorized` and carries
|
|
32
|
+
* the `TwoFactorAuth` marker. Both class policies must hold, so the window
|
|
33
|
+
* closes the moment verification succeeds.
|
|
34
|
+
*
|
|
35
|
+
* Managing a TOTP device outside that window — enrolling for the first time, or
|
|
36
|
+
* turning 2FA off — lives on {@link TwoFactorAuthUserController} under
|
|
37
|
+
* `/user/2fa`, where the caller is fully authorized. Notably there is no
|
|
38
|
+
* "disable" here on purpose: a session that has not yet proven possession of
|
|
39
|
+
* the second factor must not be able to remove it, which would reduce the whole
|
|
40
|
+
* scheme to the password alone.
|
|
41
|
+
*
|
|
32
42
|
* @tags Two-Factor Authentication
|
|
33
43
|
*/
|
|
34
44
|
let TwoFactorAuthController = class TwoFactorAuthController extends BaseController {
|
|
35
45
|
/**
|
|
36
|
-
*
|
|
37
|
-
* Generates a TOTP secret for
|
|
38
|
-
*
|
|
46
|
+
* Set up two-factor authentication during login
|
|
47
|
+
* Generates a TOTP secret for a user who must enrol before continuing — the state
|
|
48
|
+
* signalled by `TwoFactorInitRequired` at login when `rbac.twoFactorAuth.forceUser`
|
|
49
|
+
* is on. Returns the OTP provisioning URI to scan with an authenticator app; the
|
|
50
|
+
* caller then confirms with `POST /auth/2fa/verify`.
|
|
39
51
|
* @security cookieAuth
|
|
40
52
|
* @returns {IEnable2faResponse} OTP provisioning URI to scan with an authenticator app
|
|
41
53
|
* @response 400 Two-factor authentication is already enabled for this user
|
|
42
54
|
* @response 401 Unauthorized — valid session required
|
|
55
|
+
* @response 403 Session is not awaiting two-factor enrolment
|
|
43
56
|
*/
|
|
44
|
-
async
|
|
45
|
-
if (user.Metadata[
|
|
57
|
+
async setup2fa(user) {
|
|
58
|
+
if (user.Metadata[TWO_FA_METATADATA_KEYS.ENABLED]) {
|
|
46
59
|
throw new InvalidOperation(`User ${user.Uuid} already has 2fa enabled`);
|
|
47
60
|
}
|
|
48
61
|
const result = await enableUser2Fa(user);
|
|
49
62
|
return new Ok({
|
|
50
|
-
otp: result
|
|
63
|
+
otp: result,
|
|
51
64
|
});
|
|
52
65
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Disable two-factor authentication
|
|
55
|
-
* Removes the TOTP secret and disables 2FA for the authenticated user.
|
|
56
|
-
* Throws if 2FA is not currently enabled for the user.
|
|
57
|
-
* @security cookieAuth
|
|
58
|
-
* @response 200 Two-factor authentication disabled successfully
|
|
59
|
-
* @response 400 Two-factor authentication is not enabled for this user
|
|
60
|
-
* @response 401 Unauthorized — valid session required
|
|
61
|
-
*/
|
|
62
|
-
async disable2Fa(user) {
|
|
63
|
-
if (!user.Metadata['2fa:enabled']) {
|
|
64
|
-
throw new InvalidOperation(`User ${user.Uuid} already has 2fa disabled`);
|
|
65
|
-
}
|
|
66
|
-
await disableUser2Fa(user);
|
|
67
|
-
return new Ok();
|
|
68
|
-
}
|
|
69
66
|
/**
|
|
70
67
|
* Verify TOTP token
|
|
71
68
|
* Validates the provided TOTP token against the user's 2FA secret. On success, marks the session
|
|
@@ -86,41 +83,22 @@ let TwoFactorAuthController = class TwoFactorAuthController extends BaseControll
|
|
|
86
83
|
// id to defend against session fixation and reset the ssid cookie.
|
|
87
84
|
const regenerated = await regenerateSession(this.SessionProvider, session);
|
|
88
85
|
this._log.trace('User logged in, 2fa authorized', {
|
|
89
|
-
Uuid: logged.Uuid
|
|
86
|
+
Uuid: logged.Uuid,
|
|
90
87
|
});
|
|
91
88
|
// Mirror the login response shape: resolve grants for the session's
|
|
92
89
|
// active role (falling back to the first role) and include ActiveRole,
|
|
93
90
|
// instead of flattening every role with no ActiveRole reported.
|
|
94
|
-
const activeRole = session
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...logged.dehydrateWithRelations({
|
|
98
|
-
dateTimeFormat: "iso"
|
|
99
|
-
}),
|
|
100
|
-
Role: logged.Role,
|
|
101
|
-
ActiveRole: activeRole,
|
|
102
|
-
Grants: combinedGrants,
|
|
103
|
-
}, {
|
|
104
|
-
Coockies: [
|
|
105
|
-
{
|
|
106
|
-
Name: 'ssid',
|
|
107
|
-
Value: regenerated.SessionId,
|
|
108
|
-
Options: {
|
|
109
|
-
signed: true,
|
|
110
|
-
httpOnly: true,
|
|
111
|
-
maxAge: sessionCookieMaxAge(regenerated),
|
|
112
|
-
...this.SessionCookieConfig,
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
],
|
|
91
|
+
const activeRole = activeRoleOf(logged, session);
|
|
92
|
+
return new Ok(buildUserWithGrants(logged, activeRole, this.AC), {
|
|
93
|
+
Coockies: [this.SessionCookies.issue(regenerated)],
|
|
116
94
|
});
|
|
117
95
|
}
|
|
118
96
|
catch (err) {
|
|
119
97
|
if (err instanceof Error) {
|
|
120
|
-
this._log.error(err,
|
|
98
|
+
this._log.error(err, '2fa verification failed');
|
|
121
99
|
}
|
|
122
100
|
else {
|
|
123
|
-
this._log.error(
|
|
101
|
+
this._log.error('2fa verification failed', { error: err });
|
|
124
102
|
}
|
|
125
103
|
return new ForbiddenResponse({
|
|
126
104
|
error: {
|
|
@@ -139,36 +117,25 @@ let TwoFactorAuthController = class TwoFactorAuthController extends BaseControll
|
|
|
139
117
|
return auth2Fa(user, token);
|
|
140
118
|
}
|
|
141
119
|
};
|
|
142
|
-
__decorate([
|
|
143
|
-
Autoinject(QueueService),
|
|
144
|
-
__metadata("design:type", QueueService)
|
|
145
|
-
], TwoFactorAuthController.prototype, "Queue", void 0);
|
|
146
120
|
__decorate([
|
|
147
121
|
AutoinjectService('rbac.session'),
|
|
148
122
|
__metadata("design:type", SessionProvider)
|
|
149
123
|
], TwoFactorAuthController.prototype, "SessionProvider", void 0);
|
|
150
124
|
__decorate([
|
|
151
|
-
|
|
152
|
-
__metadata("design:type",
|
|
153
|
-
], TwoFactorAuthController.prototype, "
|
|
125
|
+
Autoinject(SessionCookieFactory),
|
|
126
|
+
__metadata("design:type", SessionCookieFactory)
|
|
127
|
+
], TwoFactorAuthController.prototype, "SessionCookies", void 0);
|
|
154
128
|
__decorate([
|
|
155
129
|
Autoinject(AccessControl),
|
|
156
130
|
__metadata("design:type", AccessControl)
|
|
157
131
|
], TwoFactorAuthController.prototype, "AC", void 0);
|
|
158
132
|
__decorate([
|
|
159
|
-
|
|
160
|
-
__param(0, User()),
|
|
161
|
-
__metadata("design:type", Function),
|
|
162
|
-
__metadata("design:paramtypes", [UserModel]),
|
|
163
|
-
__metadata("design:returntype", Promise)
|
|
164
|
-
], TwoFactorAuthController.prototype, "enable2fa", null);
|
|
165
|
-
__decorate([
|
|
166
|
-
Get('2fa/disable'),
|
|
133
|
+
Post('2fa/setup'),
|
|
167
134
|
__param(0, User()),
|
|
168
135
|
__metadata("design:type", Function),
|
|
169
136
|
__metadata("design:paramtypes", [UserModel]),
|
|
170
137
|
__metadata("design:returntype", Promise)
|
|
171
|
-
], TwoFactorAuthController.prototype, "
|
|
138
|
+
], TwoFactorAuthController.prototype, "setup2fa", null);
|
|
172
139
|
__decorate([
|
|
173
140
|
Post('2fa/verify'),
|
|
174
141
|
__param(0, User()),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TwoFactorAuthController.js","sourceRoot":"","sources":["../../../src/controllers/TwoFactorAuthController.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"TwoFactorAuthController.js","sourceRoot":"","sources":["../../../src/controllers/TwoFactorAuthController.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { BaseController, Ok, Unauthorized } from '@spinajs/http';
|
|
2
|
+
import { PasswordProvider, SessionProvider, User as UserModel } from '@spinajs/rbac';
|
|
3
|
+
import type { ISession } from '@spinajs/rbac';
|
|
4
|
+
import { IEnable2faResponse } from '@spinajs/rbac-http';
|
|
5
|
+
import { ConfirmPasswordDto } from '../dto/confirm-password-dto.js';
|
|
6
|
+
import { SessionCookieFactory } from '../services/SessionCookies.js';
|
|
7
|
+
/** Whether the authenticated user currently has a TOTP device enrolled. */
|
|
8
|
+
export interface ITwoFactorStatus {
|
|
9
|
+
Enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Self-service two-factor authentication.
|
|
13
|
+
*
|
|
14
|
+
* Enrolling and unenrolling a TOTP device for one's own account, from a fully
|
|
15
|
+
* authorized session. These operations were previously only reachable from the
|
|
16
|
+
* login-time 2FA window, whose policies (`TwoFacRouteEnabled` +
|
|
17
|
+
* `NotAuthorizedPolicy`) exclude an authorized session by construction — so a
|
|
18
|
+
* normal logged-in user could neither enrol nor unenrol, and the only ways in
|
|
19
|
+
* were the CLI command or an administrator's 2FA reset.
|
|
20
|
+
*
|
|
21
|
+
* Both mutations re-verify the account password. A stolen session cookie must
|
|
22
|
+
* not be enough to attach an attacker-controlled authenticator or to strip the
|
|
23
|
+
* second factor off the account.
|
|
24
|
+
*
|
|
25
|
+
* @tags Two-Factor Authentication
|
|
26
|
+
*/
|
|
27
|
+
export declare class TwoFactorAuthUserController extends BaseController {
|
|
28
|
+
protected PasswordProvider: PasswordProvider;
|
|
29
|
+
protected SessionProvider: SessionProvider;
|
|
30
|
+
protected SessionCookies: SessionCookieFactory;
|
|
31
|
+
/**
|
|
32
|
+
* Get own two-factor status
|
|
33
|
+
* Reports whether the authenticated user currently has a TOTP device enrolled.
|
|
34
|
+
* @security cookieAuth
|
|
35
|
+
* @returns {ITwoFactorStatus} Current enrolment state
|
|
36
|
+
* @response 401 Unauthorized — valid session required
|
|
37
|
+
* @response 403 Forbidden — insufficient permissions
|
|
38
|
+
*/
|
|
39
|
+
status(user: UserModel): Promise<Ok<ITwoFactorStatus>>;
|
|
40
|
+
/**
|
|
41
|
+
* Enable own two-factor authentication
|
|
42
|
+
* Generates a TOTP secret for the authenticated user and returns the OTP provisioning
|
|
43
|
+
* URI to scan with an authenticator app. Requires the account password to be re-entered.
|
|
44
|
+
* @security cookieAuth
|
|
45
|
+
* @returns {IEnable2faResponse} OTP provisioning URI to scan with an authenticator app
|
|
46
|
+
* @response 400 Two-factor authentication is already enabled for this user
|
|
47
|
+
* @response 401 Unauthorized — valid session required, or password invalid
|
|
48
|
+
* @response 403 Forbidden — insufficient permissions
|
|
49
|
+
*/
|
|
50
|
+
enable(user: UserModel, confirmation: ConfirmPasswordDto, session: ISession): Promise<Ok<IEnable2faResponse> | Unauthorized>;
|
|
51
|
+
/**
|
|
52
|
+
* Disable own two-factor authentication
|
|
53
|
+
* Removes the TOTP secret and disables 2FA for the authenticated user.
|
|
54
|
+
* Requires the account password to be re-entered.
|
|
55
|
+
* @security cookieAuth
|
|
56
|
+
* @response 200 Two-factor authentication disabled successfully
|
|
57
|
+
* @response 400 Two-factor authentication is not enabled for this user
|
|
58
|
+
* @response 401 Unauthorized — valid session required, or password invalid
|
|
59
|
+
* @response 403 Forbidden — insufficient permissions
|
|
60
|
+
*/
|
|
61
|
+
disable(user: UserModel, confirmation: ConfirmPasswordDto, session: ISession): Promise<Ok | Unauthorized>;
|
|
62
|
+
/**
|
|
63
|
+
* Rotates the session id after the account's authentication requirements
|
|
64
|
+
* changed, and returns the response options carrying the new cookie.
|
|
65
|
+
*
|
|
66
|
+
* Attaching or removing a second factor changes what the session is worth. A
|
|
67
|
+
* session id that was observed before the change must not keep working after
|
|
68
|
+
* it.
|
|
69
|
+
*
|
|
70
|
+
* @param session - the caller's current session
|
|
71
|
+
*/
|
|
72
|
+
protected rotate(session: ISession): Promise<{
|
|
73
|
+
Headers: {
|
|
74
|
+
Name: string;
|
|
75
|
+
Value: string;
|
|
76
|
+
}[];
|
|
77
|
+
Coockies?: undefined;
|
|
78
|
+
} | {
|
|
79
|
+
Coockies: import("../services/SessionCookies.js").ISessionCookie[];
|
|
80
|
+
Headers: {
|
|
81
|
+
Name: string;
|
|
82
|
+
Value: string;
|
|
83
|
+
}[];
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Generate and store the TOTP secret, returning the provisioning URI. Wraps
|
|
87
|
+
* the module-level action so tests can stub it without a TOTP/DB setup.
|
|
88
|
+
*/
|
|
89
|
+
protected enrol(user: UserModel): Promise<unknown>;
|
|
90
|
+
/** Clear the TOTP secret. Wrapped for the same reason as {@link enrol}. */
|
|
91
|
+
protected unenrol(user: UserModel): Promise<unknown>;
|
|
92
|
+
/**
|
|
93
|
+
* Re-verify the account password. Returns `true` on success, or the
|
|
94
|
+
* Unauthorized response to hand back to the caller.
|
|
95
|
+
*/
|
|
96
|
+
protected confirmPassword(user: UserModel, password: string): Promise<true | Unauthorized>;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=TwoFactorAuthUserController.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwoFactorAuthUserController.d.ts","sourceRoot":"","sources":["../../../src/controllers/TwoFactorAuthUserController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAuB,EAAE,EAAgB,YAAY,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,IAAI,IAAI,SAAS,EAAqB,MAAM,eAAe,CAAC;AACxG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAoB,kBAAkB,EAA0D,MAAM,oBAAoB,CAAC;AAGlI,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAIrE,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAIa,2BAA4B,SAAQ,cAAc;IAE7D,SAAS,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAG7C,SAAS,CAAC,eAAe,EAAE,eAAe,CAAC;IAG3C,SAAS,CAAC,cAAc,EAAE,oBAAoB,CAAC;IAE/C;;;;;;;OAOG;IAGU,MAAM,CAAS,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC;IAI3E;;;;;;;;;OASG;IAGU,MAAM,CAAS,IAAI,EAAE,SAAS,EAAU,YAAY,EAAE,kBAAkB,EAAqB,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,EAAE,CAAC,kBAAkB,CAAC,GAAG,YAAY,CAAC;IAoB5K;;;;;;;;;OASG;IAGU,OAAO,CAAS,IAAI,EAAE,SAAS,EAAU,YAAY,EAAE,kBAAkB,EAAqB,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,EAAE,GAAG,YAAY,CAAC;IAczJ;;;;;;;;;OASG;cACa,MAAM,CAAC,OAAO,EAAE,QAAQ;;;;;;;;;;;;;IAaxC;;;OAGG;IACH,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAIlD,2EAA2E;IAC3E,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAIpD;;;OAGG;cACa,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC;CAajG"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { BaseController, BasePath, Body, Get, Ok, Policy, Post, Unauthorized } from '@spinajs/http';
|
|
14
|
+
import { PasswordProvider, SessionProvider, User as UserModel, regenerateSession } from '@spinajs/rbac';
|
|
15
|
+
import { Autoinject } from '@spinajs/di';
|
|
16
|
+
import { AutoinjectService } from '@spinajs/configuration';
|
|
17
|
+
import { AuthorizedPolicy, Permission, Resource, Session as SessionRouteArg, User } from '@spinajs/rbac-http';
|
|
18
|
+
import { InvalidOperation } from '@spinajs/exceptions';
|
|
19
|
+
import { TwoFactorAuthEnabled } from '../policies/2FaPolicy.js';
|
|
20
|
+
import { ConfirmPasswordDto } from '../dto/confirm-password-dto.js';
|
|
21
|
+
import { SessionCookieFactory } from '../services/SessionCookies.js';
|
|
22
|
+
import { disableUser2Fa, enableUser2Fa } from '../actions/2fa.js';
|
|
23
|
+
import { TWO_FA_METATADATA_KEYS } from '../2fa/Default2FaToken.js';
|
|
24
|
+
/**
|
|
25
|
+
* Self-service two-factor authentication.
|
|
26
|
+
*
|
|
27
|
+
* Enrolling and unenrolling a TOTP device for one's own account, from a fully
|
|
28
|
+
* authorized session. These operations were previously only reachable from the
|
|
29
|
+
* login-time 2FA window, whose policies (`TwoFacRouteEnabled` +
|
|
30
|
+
* `NotAuthorizedPolicy`) exclude an authorized session by construction — so a
|
|
31
|
+
* normal logged-in user could neither enrol nor unenrol, and the only ways in
|
|
32
|
+
* were the CLI command or an administrator's 2FA reset.
|
|
33
|
+
*
|
|
34
|
+
* Both mutations re-verify the account password. A stolen session cookie must
|
|
35
|
+
* not be enough to attach an attacker-controlled authenticator or to strip the
|
|
36
|
+
* second factor off the account.
|
|
37
|
+
*
|
|
38
|
+
* @tags Two-Factor Authentication
|
|
39
|
+
*/
|
|
40
|
+
let TwoFactorAuthUserController = class TwoFactorAuthUserController extends BaseController {
|
|
41
|
+
/**
|
|
42
|
+
* Get own two-factor status
|
|
43
|
+
* Reports whether the authenticated user currently has a TOTP device enrolled.
|
|
44
|
+
* @security cookieAuth
|
|
45
|
+
* @returns {ITwoFactorStatus} Current enrolment state
|
|
46
|
+
* @response 401 Unauthorized — valid session required
|
|
47
|
+
* @response 403 Forbidden — insufficient permissions
|
|
48
|
+
*/
|
|
49
|
+
async status(user) {
|
|
50
|
+
return new Ok({ Enabled: Boolean(user.Metadata[TWO_FA_METATADATA_KEYS.ENABLED]) });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Enable own two-factor authentication
|
|
54
|
+
* Generates a TOTP secret for the authenticated user and returns the OTP provisioning
|
|
55
|
+
* URI to scan with an authenticator app. Requires the account password to be re-entered.
|
|
56
|
+
* @security cookieAuth
|
|
57
|
+
* @returns {IEnable2faResponse} OTP provisioning URI to scan with an authenticator app
|
|
58
|
+
* @response 400 Two-factor authentication is already enabled for this user
|
|
59
|
+
* @response 401 Unauthorized — valid session required, or password invalid
|
|
60
|
+
* @response 403 Forbidden — insufficient permissions
|
|
61
|
+
*/
|
|
62
|
+
async enable(user, confirmation, session) {
|
|
63
|
+
if (user.Metadata[TWO_FA_METATADATA_KEYS.ENABLED]) {
|
|
64
|
+
throw new InvalidOperation(`User ${user.Uuid} already has 2fa enabled`);
|
|
65
|
+
}
|
|
66
|
+
const confirmed = await this.confirmPassword(user, confirmation.Password);
|
|
67
|
+
if (confirmed !== true) {
|
|
68
|
+
return confirmed;
|
|
69
|
+
}
|
|
70
|
+
// NOTE: enrolment takes effect immediately — the secret is stored and
|
|
71
|
+
// `2fa:enabled` is set before the user has proven they scanned it. That is
|
|
72
|
+
// the same contract the login-time setup route follows, and it is what the
|
|
73
|
+
// login check reads. A user who never scans the returned URI locks
|
|
74
|
+
// themselves out and needs an administrator 2FA reset to recover.
|
|
75
|
+
const result = await this.enrol(user);
|
|
76
|
+
return new Ok({ otp: result }, await this.rotate(session));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Disable own two-factor authentication
|
|
80
|
+
* Removes the TOTP secret and disables 2FA for the authenticated user.
|
|
81
|
+
* Requires the account password to be re-entered.
|
|
82
|
+
* @security cookieAuth
|
|
83
|
+
* @response 200 Two-factor authentication disabled successfully
|
|
84
|
+
* @response 400 Two-factor authentication is not enabled for this user
|
|
85
|
+
* @response 401 Unauthorized — valid session required, or password invalid
|
|
86
|
+
* @response 403 Forbidden — insufficient permissions
|
|
87
|
+
*/
|
|
88
|
+
async disable(user, confirmation, session) {
|
|
89
|
+
if (!user.Metadata[TWO_FA_METATADATA_KEYS.ENABLED]) {
|
|
90
|
+
throw new InvalidOperation(`User ${user.Uuid} already has 2fa disabled`);
|
|
91
|
+
}
|
|
92
|
+
const confirmed = await this.confirmPassword(user, confirmation.Password);
|
|
93
|
+
if (confirmed !== true) {
|
|
94
|
+
return confirmed;
|
|
95
|
+
}
|
|
96
|
+
await this.unenrol(user);
|
|
97
|
+
return new Ok(null, await this.rotate(session));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Rotates the session id after the account's authentication requirements
|
|
101
|
+
* changed, and returns the response options carrying the new cookie.
|
|
102
|
+
*
|
|
103
|
+
* Attaching or removing a second factor changes what the session is worth. A
|
|
104
|
+
* session id that was observed before the change must not keep working after
|
|
105
|
+
* it.
|
|
106
|
+
*
|
|
107
|
+
* @param session - the caller's current session
|
|
108
|
+
*/
|
|
109
|
+
async rotate(session) {
|
|
110
|
+
if (!session) {
|
|
111
|
+
return { Headers: [{ Name: 'Cache-Control', Value: 'no-store' }] };
|
|
112
|
+
}
|
|
113
|
+
const regenerated = await regenerateSession(this.SessionProvider, session);
|
|
114
|
+
return {
|
|
115
|
+
Coockies: [this.SessionCookies.issue(regenerated)],
|
|
116
|
+
Headers: [{ Name: 'Cache-Control', Value: 'no-store' }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Generate and store the TOTP secret, returning the provisioning URI. Wraps
|
|
121
|
+
* the module-level action so tests can stub it without a TOTP/DB setup.
|
|
122
|
+
*/
|
|
123
|
+
enrol(user) {
|
|
124
|
+
return enableUser2Fa(user);
|
|
125
|
+
}
|
|
126
|
+
/** Clear the TOTP secret. Wrapped for the same reason as {@link enrol}. */
|
|
127
|
+
unenrol(user) {
|
|
128
|
+
return disableUser2Fa(user);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Re-verify the account password. Returns `true` on success, or the
|
|
132
|
+
* Unauthorized response to hand back to the caller.
|
|
133
|
+
*/
|
|
134
|
+
async confirmPassword(user, password) {
|
|
135
|
+
const valid = await this.PasswordProvider.verify(user.Password, password);
|
|
136
|
+
if (!valid) {
|
|
137
|
+
this._log.warn(`2fa change rejected for ${user.Uuid}: password confirmation failed`);
|
|
138
|
+
return new Unauthorized({
|
|
139
|
+
error: { code: 'E_PASSWORD_INVALID', message: 'Invalid password' },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
__decorate([
|
|
146
|
+
AutoinjectService('rbac.password'),
|
|
147
|
+
__metadata("design:type", PasswordProvider)
|
|
148
|
+
], TwoFactorAuthUserController.prototype, "PasswordProvider", void 0);
|
|
149
|
+
__decorate([
|
|
150
|
+
AutoinjectService('rbac.session'),
|
|
151
|
+
__metadata("design:type", SessionProvider)
|
|
152
|
+
], TwoFactorAuthUserController.prototype, "SessionProvider", void 0);
|
|
153
|
+
__decorate([
|
|
154
|
+
Autoinject(SessionCookieFactory),
|
|
155
|
+
__metadata("design:type", SessionCookieFactory)
|
|
156
|
+
], TwoFactorAuthUserController.prototype, "SessionCookies", void 0);
|
|
157
|
+
__decorate([
|
|
158
|
+
Get('2fa'),
|
|
159
|
+
Permission(['readOwn']),
|
|
160
|
+
__param(0, User()),
|
|
161
|
+
__metadata("design:type", Function),
|
|
162
|
+
__metadata("design:paramtypes", [UserModel]),
|
|
163
|
+
__metadata("design:returntype", Promise)
|
|
164
|
+
], TwoFactorAuthUserController.prototype, "status", null);
|
|
165
|
+
__decorate([
|
|
166
|
+
Post('2fa/enable'),
|
|
167
|
+
Permission(['updateOwn']),
|
|
168
|
+
__param(0, User()),
|
|
169
|
+
__param(1, Body()),
|
|
170
|
+
__param(2, SessionRouteArg()),
|
|
171
|
+
__metadata("design:type", Function),
|
|
172
|
+
__metadata("design:paramtypes", [UserModel, ConfirmPasswordDto, Object]),
|
|
173
|
+
__metadata("design:returntype", Promise)
|
|
174
|
+
], TwoFactorAuthUserController.prototype, "enable", null);
|
|
175
|
+
__decorate([
|
|
176
|
+
Post('2fa/disable'),
|
|
177
|
+
Permission(['updateOwn']),
|
|
178
|
+
__param(0, User()),
|
|
179
|
+
__param(1, Body()),
|
|
180
|
+
__param(2, SessionRouteArg()),
|
|
181
|
+
__metadata("design:type", Function),
|
|
182
|
+
__metadata("design:paramtypes", [UserModel, ConfirmPasswordDto, Object]),
|
|
183
|
+
__metadata("design:returntype", Promise)
|
|
184
|
+
], TwoFactorAuthUserController.prototype, "disable", null);
|
|
185
|
+
TwoFactorAuthUserController = __decorate([
|
|
186
|
+
BasePath('user'),
|
|
187
|
+
Resource('user'),
|
|
188
|
+
Policy(AuthorizedPolicy),
|
|
189
|
+
Policy(TwoFactorAuthEnabled)
|
|
190
|
+
], TwoFactorAuthUserController);
|
|
191
|
+
export { TwoFactorAuthUserController };
|
|
192
|
+
//# sourceMappingURL=TwoFactorAuthUserController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwoFactorAuthUserController.js","sourceRoot":"","sources":["../../../src/controllers/TwoFactorAuthUserController.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,IAAI,IAAI,SAAS,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAExG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAsB,UAAU,EAAE,QAAQ,EAAE,OAAO,IAAI,eAAe,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAClI,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAOnE;;;;;;;;;;;;;;;GAeG;AAKI,IAAM,2BAA2B,GAAjC,MAAM,2BAA4B,SAAQ,cAAc;IAU7D;;;;;;;OAOG;IAGU,AAAN,KAAK,CAAC,MAAM,CAAS,IAAe;QACzC,OAAO,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACrF,CAAC;IAED;;;;;;;;;OASG;IAGU,AAAN,KAAK,CAAC,MAAM,CAAS,IAAe,EAAU,YAAgC,EAAqB,OAAiB;QACzH,IAAI,IAAI,CAAC,QAAQ,CAAC,sBAAsB,CAAC,OAAO,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,gBAAgB,CAAC,QAAQ,IAAI,CAAC,IAAI,0BAA0B,CAAC,CAAC;QAC1E,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC1E,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,sEAAsE;QACtE,2EAA2E;QAC3E,2EAA2E;QAC3E,mEAAmE;QACnE,kEAAkE;QAClE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAEtC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,MAAgB,EAAE,EAAE,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;;;OASG;IAGU,AAAN,KAAK,CAAC,OAAO,CAAS,IAAe,EAAU,YAAgC,EAAqB,OAAiB;QAC1H,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,sBAAsB,CAAC,OAAO,CAAC,EAAE,CAAC;YACnD,MAAM,IAAI,gBAAgB,CAAC,QAAQ,IAAI,CAAC,IAAI,2BAA2B,CAAC,CAAC;QAC3E,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC1E,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAClD,CAAC;IAED;;;;;;;;;OASG;IACO,KAAK,CAAC,MAAM,CAAC,OAAiB;QACtC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;QACrE,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAE3E,OAAO;YACL,QAAQ,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAClD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;SACxD,CAAC;IACJ,CAAC;IAED;;;OAGG;IACO,KAAK,CAAC,IAAe;QAC7B,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,2EAA2E;IACjE,OAAO,CAAC,IAAe;QAC/B,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED;;;OAGG;IACO,KAAK,CAAC,eAAe,CAAC,IAAe,EAAE,QAAgB;QAC/D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAE1E,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B,IAAI,CAAC,IAAI,gCAAgC,CAAC,CAAC;YAErF,OAAO,IAAI,YAAY,CAAC;gBACtB,KAAK,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,kBAAkB,EAAE;aACnE,CAAC,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AArIW;IADT,iBAAiB,CAAC,eAAe,CAAC;8BACP,gBAAgB;qEAAC;AAGnC;IADT,iBAAiB,CAAC,cAAc,CAAC;8BACP,eAAe;oEAAC;AAGjC;IADT,UAAU,CAAC,oBAAoB,CAAC;8BACP,oBAAoB;mEAAC;AAYlC;IAFZ,GAAG,CAAC,KAAK,CAAC;IACV,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC;IACH,WAAA,IAAI,EAAE,CAAA;;qCAAO,SAAS;;yDAE1C;AAcY;IAFZ,IAAI,CAAC,YAAY,CAAC;IAClB,UAAU,CAAC,CAAC,WAAW,CAAC,CAAC;IACL,WAAA,IAAI,EAAE,CAAA;IAAmB,WAAA,IAAI,EAAE,CAAA;IAAoC,WAAA,eAAe,EAAE,CAAA;;qCAAvE,SAAS,EAAwB,kBAAkB;;yDAkBpF;AAcY;IAFZ,IAAI,CAAC,aAAa,CAAC;IACnB,UAAU,CAAC,CAAC,WAAW,CAAC,CAAC;IACJ,WAAA,IAAI,EAAE,CAAA;IAAmB,WAAA,IAAI,EAAE,CAAA;IAAoC,WAAA,eAAe,EAAE,CAAA;;qCAAvE,SAAS,EAAwB,kBAAkB;;0DAYrF;AAhFU,2BAA2B;IAJvC,QAAQ,CAAC,MAAM,CAAC;IAChB,QAAQ,CAAC,MAAM,CAAC;IAChB,MAAM,CAAC,gBAAgB,CAAC;IACxB,MAAM,CAAC,oBAAoB,CAAC;GAChB,2BAA2B,CAuIvC"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PasswordDto } from '../dto/password-dto.js';
|
|
2
2
|
import { User as UserModel, PasswordProvider, SessionProvider, AccessControl } from '@spinajs/rbac';
|
|
3
|
+
import type { ISession } from '@spinajs/rbac';
|
|
3
4
|
import { BaseController, Ok } from '@spinajs/http';
|
|
4
5
|
import { IGrantsMap } from '@spinajs/rbac-http';
|
|
6
|
+
import { SessionCookieFactory } from '../services/SessionCookies.js';
|
|
5
7
|
/**
|
|
6
8
|
* Current user profile management.
|
|
7
9
|
* Allows an authenticated user to read and modify their own account — refresh profile data,
|
|
@@ -10,9 +12,9 @@ import { IGrantsMap } from '@spinajs/rbac-http';
|
|
|
10
12
|
*/
|
|
11
13
|
export declare class UserController extends BaseController {
|
|
12
14
|
protected PasswordProvider: PasswordProvider;
|
|
13
|
-
protected CoockieSecret: string;
|
|
14
15
|
protected SessionProvider: SessionProvider;
|
|
15
16
|
protected AC: AccessControl;
|
|
17
|
+
protected SessionCookies: SessionCookieFactory;
|
|
16
18
|
/**
|
|
17
19
|
* Refresh current user profile
|
|
18
20
|
* Reloads the authenticated user's record from the database (including metadata) and
|
|
@@ -22,17 +24,18 @@ export declare class UserController extends BaseController {
|
|
|
22
24
|
* @response 401 Unauthorized — valid session required
|
|
23
25
|
* @response 403 Forbidden — insufficient permissions
|
|
24
26
|
*/
|
|
25
|
-
refresh(user: UserModel, ssid: string): Promise<Ok<import("@spinajs/orm").
|
|
27
|
+
refresh(user: UserModel, ssid: string): Promise<Ok<import("@spinajs/orm").ModelDataWithRelationData<UserModel>>>;
|
|
26
28
|
/**
|
|
27
29
|
* Get current user grants
|
|
28
|
-
* Returns the flattened RBAC grants for the authenticated user
|
|
29
|
-
* the
|
|
30
|
+
* Returns the flattened RBAC grants in effect for the authenticated user — those of
|
|
31
|
+
* the session's active role, resolved through its inheritance chain. Switching the
|
|
32
|
+
* active role (`POST /auth/active-role`) changes what this returns.
|
|
30
33
|
* @security cookieAuth
|
|
31
|
-
* @returns {IGrantsMap}
|
|
34
|
+
* @returns {IGrantsMap} RBAC grants map for the active role: resource → action → permission descriptor
|
|
32
35
|
* @response 401 Unauthorized — valid session required
|
|
33
36
|
* @response 403 Forbidden — insufficient permissions
|
|
34
37
|
*/
|
|
35
|
-
getGrants(user: UserModel): Promise<Ok<IGrantsMap>>;
|
|
38
|
+
getGrants(user: UserModel, session: ISession): Promise<Ok<IGrantsMap>>;
|
|
36
39
|
/**
|
|
37
40
|
* Change own password
|
|
38
41
|
* Changes the authenticated user's password. Requires the current (old) password for verification.
|
|
@@ -42,6 +45,6 @@ export declare class UserController extends BaseController {
|
|
|
42
45
|
* @response 401 Unauthorized — valid session required
|
|
43
46
|
* @response 403 Forbidden — insufficient permissions
|
|
44
47
|
*/
|
|
45
|
-
newPassword(user: UserModel, pwd: PasswordDto): Promise<Ok<
|
|
48
|
+
newPassword(user: UserModel, pwd: PasswordDto, session: ISession): Promise<Ok<null>>;
|
|
46
49
|
}
|
|
47
50
|
//# sourceMappingURL=UserController.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserController.d.ts","sourceRoot":"","sources":["../../../src/controllers/UserController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,gBAAgB,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"UserController.d.ts","sourceRoot":"","sources":["../../../src/controllers/UserController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAA8C,aAAa,EAAE,MAAM,eAAe,CAAC;AAChJ,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAiB,EAAE,EAAuB,MAAM,eAAe,CAAC;AAIvF,OAAO,EAAgD,UAAU,EAAyD,MAAM,oBAAoB,CAAC;AAGrJ,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAIrE;;;;;GAKG;AACH,qBAGa,cAAe,SAAQ,cAAc;IAEhD,SAAS,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAG7C,SAAS,CAAC,eAAe,EAAE,eAAe,CAAC;IAG3C,SAAS,CAAC,EAAE,EAAE,aAAa,CAAC;IAG5B,SAAS,CAAC,cAAc,EAAE,oBAAoB,CAAC;IAE/C;;;;;;;;OAQG;IAGU,OAAO,CAAS,IAAI,EAAE,SAAS,EAAkB,IAAI,EAAE,MAAM;IA0B1E;;;;;;;;;OASG;IAGU,SAAS,CAAS,IAAI,EAAE,SAAS,EAAqB,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;IAS9G;;;;;;;;OAQG;IAGU,WAAW,CAAS,IAAI,EAAE,SAAS,EAAU,GAAG,EAAE,WAAW,EAAqB,OAAO,EAAE,QAAQ;CAwCjH"}
|