From 69c29e369c16c4448276c5fe0709647c188994dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Lengyel?= Date: Tue, 26 Nov 2024 16:07:22 +0100 Subject: [PATCH] fix: validate MFA claim before allowing TOTP device removal (#963) * fix: validate MFA claim before allowing TOTP device removal * fix: fix how we check the MFA claim in removeDevice --- .gitignore | 5 ++- CHANGELOG.md | 4 ++ lib/build/recipe/totp/api/implementation.js | 47 ++++++++++++++++++++- lib/build/recipe/totp/api/removeDevice.js | 5 ++- lib/build/version.d.ts | 2 +- lib/build/version.js | 2 +- lib/ts/recipe/totp/api/implementation.ts | 11 ++++- lib/ts/recipe/totp/api/removeDevice.ts | 5 ++- lib/ts/version.ts | 2 +- package-lock.json | 4 +- package.json | 2 +- test/test-server/src/testFunctionMapper.ts | 10 +++++ 12 files changed, 88 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index a5e771d2c..5f6ac05e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /node_modules /examples/**/node_modules +/examples/**/dist .DS_Store /.history *.js.map @@ -12,4 +13,6 @@ releasePassword /test_report /temp_test_exports /temp_* -/.nyc_output \ No newline at end of file +/.nyc_output +.circleci/.pat +examples/express/with-m2m/clients.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7005e44b8..e10cc11df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [20.1.6] - 2024-11-26 + +- Fixes an issue where `removeDevice` API allowed removing verifiedTOTP devices without the user completing MFA. + ## [20.1.5] - 2024-10-09 - Fixes an issue where users were not able to reset their password if a user with the same email address was created before account linking was enabled. diff --git a/lib/build/recipe/totp/api/implementation.js b/lib/build/recipe/totp/api/implementation.js index 909546b34..d282a5731 100644 --- a/lib/build/recipe/totp/api/implementation.js +++ b/lib/build/recipe/totp/api/implementation.js @@ -13,13 +13,49 @@ * License for the specific language governing permissions and limitations * under the License. */ +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); + } + : function (o, v) { + o["default"] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const multifactorauth_1 = __importDefault(require("../../multifactorauth")); +const multifactorauth_1 = __importStar(require("../../multifactorauth")); const recipe_1 = __importDefault(require("../../multifactorauth/recipe")); const error_1 = __importDefault(require("../../session/error")); function getAPIInterface() { @@ -58,6 +94,15 @@ function getAPIInterface() { }, removeDevicePOST: async function ({ deviceName, options, session, userContext }) { const userId = session.getUserId(); + const deviceList = await options.recipeImplementation.listDevices({ + userId, + userContext, + }); + if (deviceList.devices.some((device) => device.name === deviceName && device.verified)) { + await session.assertClaims([ + multifactorauth_1.MultiFactorAuthClaim.validators.hasCompletedMFARequirementsForAuth(), + ]); + } return await options.recipeImplementation.removeDevice({ userId, deviceName, diff --git a/lib/build/recipe/totp/api/removeDevice.js b/lib/build/recipe/totp/api/removeDevice.js index 3893442a1..ef3e0335c 100644 --- a/lib/build/recipe/totp/api/removeDevice.js +++ b/lib/build/recipe/totp/api/removeDevice.js @@ -28,7 +28,10 @@ async function removeDeviceAPI(apiImplementation, options, userContext) { const session = await session_1.default.getSession( options.req, options.res, - { overrideGlobalClaimValidators: () => [], sessionRequired: true }, + { + overrideGlobalClaimValidators: () => [], + sessionRequired: true, + }, userContext ); const bodyParams = await options.req.getJSONBody(); diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts index d00dc1c93..edd0a9a91 100644 --- a/lib/build/version.d.ts +++ b/lib/build/version.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck -export declare const version = "20.1.5"; +export declare const version = "20.1.6"; export declare const cdiSupported: string[]; export declare const dashboardVersion = "0.13"; diff --git a/lib/build/version.js b/lib/build/version.js index 02ed3001a..b1a6ec9eb 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -15,7 +15,7 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * License for the specific language governing permissions and limitations * under the License. */ -exports.version = "20.1.5"; +exports.version = "20.1.6"; exports.cdiSupported = ["5.1"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} exports.dashboardVersion = "0.13"; diff --git a/lib/ts/recipe/totp/api/implementation.ts b/lib/ts/recipe/totp/api/implementation.ts index 444860542..3e1359a3a 100644 --- a/lib/ts/recipe/totp/api/implementation.ts +++ b/lib/ts/recipe/totp/api/implementation.ts @@ -14,7 +14,7 @@ */ import { APIInterface } from "../"; -import MultiFactorAuth from "../../multifactorauth"; +import MultiFactorAuth, { MultiFactorAuthClaim } from "../../multifactorauth"; import MultiFactorAuthRecipe from "../../multifactorauth/recipe"; import SessionError from "../../session/error"; @@ -59,6 +59,15 @@ export default function getAPIInterface(): APIInterface { removeDevicePOST: async function ({ deviceName, options, session, userContext }) { const userId = session.getUserId(); + const deviceList = await options.recipeImplementation.listDevices({ + userId, + userContext, + }); + + if (deviceList.devices.some((device) => device.name === deviceName && device.verified)) { + await session.assertClaims([MultiFactorAuthClaim.validators.hasCompletedMFARequirementsForAuth()]); + } + return await options.recipeImplementation.removeDevice({ userId, deviceName, diff --git a/lib/ts/recipe/totp/api/removeDevice.ts b/lib/ts/recipe/totp/api/removeDevice.ts index 65c57fec1..9e87abe96 100644 --- a/lib/ts/recipe/totp/api/removeDevice.ts +++ b/lib/ts/recipe/totp/api/removeDevice.ts @@ -30,7 +30,10 @@ export default async function removeDeviceAPI( const session = await Session.getSession( options.req, options.res, - { overrideGlobalClaimValidators: () => [], sessionRequired: true }, + { + overrideGlobalClaimValidators: () => [], + sessionRequired: true, + }, userContext ); diff --git a/lib/ts/version.ts b/lib/ts/version.ts index eeb048f94..cb69344fa 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -12,7 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -export const version = "20.1.5"; +export const version = "20.1.6"; export const cdiSupported = ["5.1"]; diff --git a/package-lock.json b/package-lock.json index 1793d8619..f984cb679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supertokens-node", - "version": "20.1.5", + "version": "20.1.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "supertokens-node", - "version": "20.1.5", + "version": "20.1.6", "license": "Apache-2.0", "dependencies": { "buffer": "^6.0.3", diff --git a/package.json b/package.json index 723c30c41..bda753216 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supertokens-node", - "version": "20.1.5", + "version": "20.1.6", "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { diff --git a/test/test-server/src/testFunctionMapper.ts b/test/test-server/src/testFunctionMapper.ts index c342ac37f..5c3da195d 100644 --- a/test/test-server/src/testFunctionMapper.ts +++ b/test/test-server/src/testFunctionMapper.ts @@ -385,6 +385,16 @@ export function getFunc(evalStr: string): (...args: any[]) => any { } if (evalStr.startsWith("multifactorauth.init.override.functions")) { + if (evalStr.includes(`getMFARequirementsForAuth:async()=>["totp"]`)) { + return (e) => { + return { + ...e, + getMFARequirementsForAuth: (e) => { + return ["totp"]; + }, + }; + }; + } return (e) => { return { ...e,