Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve debugging/DX when enabling MFA #822

Merged
merged 3 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [17.0.4] - 2024-04-09

### Changes

- Improved error message to help debugging if MFA was enabled without account linking.
- Now the `resyncSessionAndFetchMFAInfoPUT` will throw if the user is in a stuck state, because they are required to complete factors, but they are not allowed to because of some configuration issue.

## [17.0.3] - 2024-04-08

### Fixes
Expand Down
3 changes: 2 additions & 1 deletion lib/build/authUtils.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 17 additions & 9 deletions lib/build/recipe/multifactorauth/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,26 @@ function getAPIInterface() {
message: "User no longer associated with the session",
});
}
// next array is filtered to only include factors that are allowed to be setup or are already setup
// we do this because the factor chooser in the frontend will be based on the next array only
// we do not simply filter out the factors that are not allowed to be setup because in the case
// where user has already setup a factor and not completed it, none of the factors will be allowed to
// be setup, and that that will result in an empty next array. However, we want to show the factor
// that the user has already setup in that case.
const next = nextSetOfUnsatisfiedFactors.factorIds.filter(
(factorId) => factorsAllowedToSetup.includes(factorId) || factorsSetUpForUser.includes(factorId)
);
if (next.length === 0 && nextSetOfUnsatisfiedFactors.factorIds.length !== 0) {
throw new Error(
`The user is required to complete secondary factors they are not allowed to (${nextSetOfUnsatisfiedFactors.factorIds.join(
", "
)}), likely because of configuration issues.`
);
}
return {
status: "OK",
factors: {
// next array is filtered to only include factors that are allowed to be setup or are already setup
// we do this because the factor chooser in the frontend will be based on the next array only
// we do not simply filter out the factors that are not allowed to be setup because in the case
// where user has already setup a factor and not completed it, none of the factors will be allowed to
// be setup, and that that will result in an empty next array. However, we want to show the factor
// that the user has already setup in that case.
next: nextSetOfUnsatisfiedFactors.factorIds.filter(
(factorId) => factorsAllowedToSetup.includes(factorId) || factorsSetUpForUser.includes(factorId)
),
next,
alreadySetup: factorsSetUpForUser,
allowedToSetup: factorsAllowedToSetup,
},
Expand Down
2 changes: 1 addition & 1 deletion lib/build/version.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion lib/ts/authUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,8 @@ export const AuthUtils = {
} else {
throw new SuperTokensError({
type: SuperTokensError.BAD_INPUT_ERROR,
message: "First factor sign in/up called for a non-first factor with an active session.",
message:
"First factor sign in/up called for a non-first factor with an active session. This might indicate that you are trying to use this as a secondary factor, but disabled account linking.",
});
}
}
Expand Down
28 changes: 19 additions & 9 deletions lib/ts/recipe/multifactorauth/api/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,28 @@ export default function getAPIInterface(): APIInterface {
});
}

// next array is filtered to only include factors that are allowed to be setup or are already setup
// we do this because the factor chooser in the frontend will be based on the next array only
// we do not simply filter out the factors that are not allowed to be setup because in the case
// where user has already setup a factor and not completed it, none of the factors will be allowed to
// be setup, and that that will result in an empty next array. However, we want to show the factor
// that the user has already setup in that case.
const next = nextSetOfUnsatisfiedFactors.factorIds.filter(
(factorId) => factorsAllowedToSetup.includes(factorId) || factorsSetUpForUser.includes(factorId)
);

if (next.length === 0 && nextSetOfUnsatisfiedFactors.factorIds.length !== 0) {
throw new Error(
`The user is required to complete secondary factors they are not allowed to (${nextSetOfUnsatisfiedFactors.factorIds.join(
", "
)}), likely because of configuration issues.`
);
}
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

return {
status: "OK",
factors: {
// next array is filtered to only include factors that are allowed to be setup or are already setup
// we do this because the factor chooser in the frontend will be based on the next array only
// we do not simply filter out the factors that are not allowed to be setup because in the case
// where user has already setup a factor and not completed it, none of the factors will be allowed to
// be setup, and that that will result in an empty next array. However, we want to show the factor
// that the user has already setup in that case.
next: nextSetOfUnsatisfiedFactors.factorIds.filter(
(factorId) => factorsAllowedToSetup.includes(factorId) || factorsSetUpForUser.includes(factorId)
),
next,
alreadySetup: factorsSetUpForUser,
allowedToSetup: factorsAllowedToSetup,
},
Expand Down
2 changes: 1 addition & 1 deletion lib/ts/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
export const version = "17.0.3";
export const version = "17.0.4";

export const cdiSupported = ["5.0"];

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supertokens-node",
"version": "17.0.3",
"version": "17.0.4",
"description": "NodeJS driver for SuperTokens core",
"main": "index.js",
"scripts": {
Expand Down
58 changes: 58 additions & 0 deletions test/mfa/mfa.api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,64 @@ describe(`mfa-api: ${printPath("[test/mfa/mfa.api.test.js]")}`, function () {
assert.deepEqual(["emailpassword", "otp-email", "totp"], res.body.factors.allowedToSetup);
});

it("mfa info errors if the user is stuck", async function () {
const connectionURI = await startSTWithMultitenancy();
let requireFactor = false;

SuperTokens.init({
supertokens: {
connectionURI,
},
appInfo: {
apiDomain: "api.supertokens.io",
appName: "SuperTokens",
websiteDomain: "supertokens.io",
},
recipeList: [
EmailPassword.init(),
ThirdParty.init(),
AccountLinking.init({
shouldDoAutomaticAccountLinking: async () => ({
shouldAutomaticallyLink: true,
shouldRequireVerification: true,
}),
}),
MultiFactorAuth.init({
override: {
functions: (oI) => ({
...oI,
getMFARequirementsForAuth: () => (requireFactor ? ["otp-phone"] : []),
}),
},
}),
Session.init(),
],
});

const app = getTestExpressApp();

await EmailPassword.signUp("public", "[email protected]", "password");

let res = await epSignIn(app, "[email protected]", "password");
assert.equal("OK", res.body.status);

let cookies = extractInfoFromResponse(res);
const accessToken = cookies.accessTokenFromAny;

res = await getMfaInfo(app, accessToken);
assert.equal("OK", res.body.status);
assert.deepEqual([], res.body.factors.next);

requireFactor = true;

res = await getMfaInfo(app, accessToken, 500);
assert(
res.text.includes(
"The user is required to complete secondary factors they are not allowed to (otp-phone), likely because of configuration issues."
)
);
});

it("test that only a valid first factor is allowed to login", async function () {
const connectionURI = await startSTWithMultitenancy();
SuperTokens.init({
Expand Down
4 changes: 2 additions & 2 deletions test/mfa/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,6 @@ module.exports.tpSignInUp = async function (app, thirdPartyId, email, accessToke
}
};

module.exports.getMfaInfo = async function (app, accessToken) {
return request(app).put("/auth/mfa/info").set("Authorization", `Bearer ${accessToken}`).expect(200);
module.exports.getMfaInfo = async function (app, accessToken, statusCode = 200) {
return request(app).put("/auth/mfa/info").set("Authorization", `Bearer ${accessToken}`).expect(statusCode);
};
Loading