Skip to content

Commit

Permalink
test: initial test for MFA
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Oct 30, 2023
1 parent f17de3a commit b8b3e3c
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 15 deletions.
12 changes: 9 additions & 3 deletions examples/for-tests/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ const testContext = getTestContext();

let totpDevices = [];
let tryCount = 0;
setInterval(() => (tryCount = tryCount > 0 ? tryCount - 1 : 0), 30);

setInterval(() => (tryCount = tryCount > 0 ? tryCount - 1 : 0), 30000);
window.resetTOTP = () => {
totpDevices = [];
tryCount = 0;
Expand Down Expand Up @@ -207,6 +208,11 @@ let recipeList = [
verifyCode: async ({ totp }) => {
const dev = totpDevices.find((d) => d.deviceName.endsWith(totp) && d.verified);
if (dev) {
await fetch("http://localhost:8082/completeFactor", {
method: "POST",
body: JSON.stringify({ id: "totp" }),
headers: new Headers([["Content-Type", "application/json"]]),
});
return { status: "OK" };
}

Expand All @@ -223,9 +229,9 @@ let recipeList = [
if (deviceName.endsWith(totp)) {
const wasAlreadyVerified = dev.verified;
dev.verified = true;
await fetch("http://localhost:8082/mergeIntoAccessTokenPayload", {
await fetch("http://localhost:8082/completeFactor", {
method: "POST",
body: JSON.stringify({ hasTOTP: true }),
body: JSON.stringify({ id: "totp" }),
headers: new Headers([["Content-Type", "application/json"]]),
});
return { status: "OK", wasAlreadyVerified };
Expand Down
11 changes: 10 additions & 1 deletion examples/for-tests/src/testContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function getTestContext() {
thirdPartyRedirectURL: localStorage.getItem("thirdPartyRedirectURL"),
authRecipe: window.localStorage.getItem("authRecipe") || "emailpassword",
usesDynamicLoginMethods: localStorage.getItem("usesDynamicLoginMethods") === "true",
enableAllRecipes: localStorage.getItem("enableAllRecipes") === "true",
clientRecipeListForDynamicLogin: localStorage.getItem("clientRecipeListForDynamicLogin"),
mockLoginMethodsForDynamicLogin: localStorage.getItem("mockLoginMethodsForDynamicLogin"),
staticProviderList: localStorage.getItem("staticProviderList"),
Expand All @@ -23,7 +24,15 @@ export function getEnabledRecipes() {

let enabledRecipes = [];

if (true) {
if (testContext.enableAllRecipes) {
enabledRecipes = [
"emailpassword",
"thirdparty",
"thirdpartyemailpassword",
"passwordless",
"thirdpartypasswordless",
];
} else if (testContext.usesDynamicLoginMethods) {
if (testContext.clientRecipeListForDynamicLogin) {
enabledRecipes = JSON.parse(testContext.clientRecipeListForDynamicLogin);
} else {
Expand Down
156 changes: 156 additions & 0 deletions test/end-to-end/mfa.mock.signin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

/*
* Imports
*/

import assert from "assert";
import puppeteer from "puppeteer";
import {
clearBrowserCookiesWithoutAffectingConsole,
clickForgotPasswordLink,
getFieldErrors,
getGeneralError,
getInputNames,
getInputTypes,
getLogoutButton,
getLabelsText,
getPlaceholders,
getUserIdWithAxios,
getSessionHandleWithAxios,
getUserIdWithFetch,
getSessionHandleWithFetch,
getShowPasswordIcon,
getSubmitFormButtonLabel,
getInputAdornmentsSuccess,
hasMethodBeenCalled,
setInputValues,
submitForm,
submitFormReturnRequestAndResponse,
toggleShowPasswordIcon,
toggleSignInSignUp,
getInputAdornmentsError,
defaultSignUp,
getUserIdFromSessionContext,
getTextInDashboardNoAuth,
waitForSTElement,
screenshotOnFailure,
isGeneralErrorSupported,
setGeneralErrorToLocalStorage,
getInvalidClaimsJSON as getInvalidClaims,
waitForText,
backendBeforeEach,
getTestEmail,
getPasswordlessDevice,
} from "../helpers";
import fetch from "isomorphic-fetch";
import { SOMETHING_WENT_WRONG_ERROR, TEST_APPLICATION_SERVER_BASE_URL } from "../constants";

import { EMAIL_EXISTS_API, SIGN_IN_API, TEST_CLIENT_BASE_URL, TEST_SERVER_BASE_URL, SIGN_OUT_API } from "../constants";

/*
* Tests.
*/
describe("SuperTokens SignIn", function () {
let browser;
let page;
let consoleLogs = [];

before(async function () {
await backendBeforeEach();

await fetch(`${TEST_SERVER_BASE_URL}/startst`, {
method: "POST",
}).catch(console.error);

browser = await puppeteer.launch({
args: ["--no-sandbox", "--disable-setuid-sandbox"],
headless: false,
});
});

after(async function () {
await browser.close();

await fetch(`${TEST_SERVER_BASE_URL}/after`, {
method: "POST",
}).catch(console.error);

await fetch(`${TEST_SERVER_BASE_URL}/stopst`, {
method: "POST",
}).catch(console.error);
});

afterEach(function () {
return screenshotOnFailure(this, browser);
});

beforeEach(async function () {
page = await browser.newPage();
page.on("console", (consoleObj) => {
const log = consoleObj.text();
if (log.startsWith("ST_LOGS")) {
consoleLogs.push(log);
}
});
consoleLogs = await clearBrowserCookiesWithoutAffectingConsole(page, []);
});

describe("SignIn test ", function () {
it("Successful Sign In", async function () {
consoleLogs = await clearBrowserCookiesWithoutAffectingConsole(page, consoleLogs);
const email = await getTestEmail();
await page.evaluate(() => window.localStorage.setItem("enableAllRecipes", "true"));

let resp = await fetch(`${TEST_APPLICATION_SERVER_BASE_URL}/setMFAInfo`, {
method: "POST",
headers: new Headers([["content-type", "application/json"]]),
body: JSON.stringify({
requirements: ["otp-email"],
}),
});
assert.strictEqual(resp.status, 200);
console.log(await resp.text());
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth/?rid=emailpassword`),
page.waitForNavigation({ waitUntil: "networkidle0" }),
]);

await toggleSignInSignUp(page);

await setInputValues(page, [
{ name: "email", value: email },
{ name: "password", value: "Asdf12.." },
{ name: "name", value: "asdf" },
{ name: "age", value: "20" },
]);

await submitForm(page);
await new Promise((res) => setTimeout(res, 250));

await waitForSTElement(page, "[data-supertokens~=input][name=userInputCode]");

const loginAttemptInfo = JSON.parse(
await page.evaluate(() => localStorage.getItem("supertokens-passwordless-loginAttemptInfo"))
);
const device = await getPasswordlessDevice(loginAttemptInfo);
await setInputValues(page, [{ name: "userInputCode", value: device.codes[0].userInputCode }]);
await submitForm(page);

await page.waitForSelector(".sessionInfo-user-id");
});
});
});
98 changes: 87 additions & 11 deletions test/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ let passwordlessConfig = {};
let accountLinkingConfig = {};
let enabledProviders = undefined;
let enabledRecipes = undefined;
let mfaInfo = {};

initST();

Expand Down Expand Up @@ -382,16 +383,32 @@ app.post(
}
);

let mfaInfo = {};
app.post("/setMFAInfo", verifySession(), async (req, res) => {
let session = req.session;

mfaInfo = req.body.mfaInfo;
await session.mergeIntoAccessTokenPayload(req.body.payload);
app.post("/setMFAInfo", async (req, res) => {
mfaInfo = { mfaInfo, ...req.body };

res.send({ status: "OK" });
});

app.post("/completeFactor", verifySession(), async (req, res) => {
let session = req.session;
const mfaClaim = payload["st-mfa"];
const c = {
...mfaClaim.c,
[req.payload.id]: Date.now(),
};
if (req.payload.id === "totp") {
mfaInfo.hasTOTP = true;
}

await session.mergeIntoAccessTokenPayload({
"st-mfa": {
...mfaClaim,
c,
n: getNextArray(c),
},
});
});

app.post("/mergeIntoAccessTokenPayload", verifySession(), async (req, res) => {
let session = req.session;

Expand All @@ -413,15 +430,47 @@ app.get("/auth/mfa/info", verifySession(), async (req, res) => {
if (user.emails.length > 0) {
isAlreadySetup.push("otp-email");
}
if (payload.hasTOTP) {
isAlreadySetup.push("totp");

if (mfaInfo.hasTOTP) {
isAllowedToSetup.push("totp");
}

const mfaClaim = payload["st-mfa"];
if (mfaClaim === undefined) {
console.log(mfaInfo, mfaClaim);
if (mfaInfo?.claimValue) {
await session.mergeIntoAccessTokenPayload({
"st-mfa": mfaInfo.claimValue,
});
} else if (mfaInfo?.requirements) {
let c;
if (mfaClaim) {
c = mfaClaim.c;
} else {
const recipeUser = user.loginMethods.find(
(u) => u.recipeUserId.toString() === session.getRecipeUserId().toString()
);
if (recipeUser.recipeId !== "passwordless") {
c = { [recipeUser.recipeUserId]: Date.now() };
} else if (recipeUser.email) {
// This isn't correct, but will do for testing
c = { "otp-email": Date.now() };
} else {
c = { "otp-phone": Date.now() };
}
}

let n = getNextArray(c);
await session.mergeIntoAccessTokenPayload({
"st-mfa": {
c: c,
n: n,
},
});
} else if (mfaClaim === undefined) {
await session.mergeIntoAccessTokenPayload({
"st-mfa": {
c: {}, // Technically the first factor should be in there... but it isn't necessary
n: ["otp-phone", "otp-email"],
n: [],
},
});
}
Expand All @@ -440,7 +489,7 @@ app.get("/auth/mfa/info", verifySession(), async (req, res) => {
isAllowedToSetup,
isAlreadySetup,
},
...mfaInfo,
...mfaInfo.resp,
});
});

Expand Down Expand Up @@ -600,8 +649,35 @@ server.listen(process.env.NODE_PORT === undefined ? 8080 : process.env.NODE_PORT
}
})(process.env.START === "true");

function getNextArray(c) {
let n = [];
for (const step of mfaInfo.requirements) {
if (typeof step === "string") {
if (c[step] === undefined) {
n = [step];
break;
}
} else if (step.oneOf !== undefined) {
if (step.oneOf.every((id) => c[id] === undefined)) {
n = step.oneOf;
break;
}
} else if (step.allOf !== undefined) {
const missing = step.allOf.filter((id) => c[id] === undefined);
if (missing.length > 0) {
n = missing;
break;
}
} else {
throw new Error("Bad requirement" + JSON.stringify(step));
}
}
return n;
}

function initST() {
if (process.env.TEST_MODE) {
mfaInfo = {};
if (userRolesSupported) {
UserRolesRaw.reset();
}
Expand Down

0 comments on commit b8b3e3c

Please sign in to comment.