diff --git a/.github/workflows/pre-commit-hook-run.yml b/.github/workflows/pre-commit-hook-run.yml
index 968977f92..71669f1b7 100644
--- a/.github/workflows/pre-commit-hook-run.yml
+++ b/.github/workflows/pre-commit-hook-run.yml
@@ -12,15 +12,8 @@ jobs:
pr-title:
name: Pre commit hook check
runs-on: ubuntu-latest
- container: rishabhpoddar/supertokens_website_sdk_testing_node_16
steps:
- - uses: actions/checkout@v2
- - run: git init && git add --all && git -c user.name='test' -c user.email='test@example.com' commit -m 'init for pr action'
- - run: npm i --force || true
- # the below command is there cause otherwise running npm run check-circular-dependencies gives an error like:
- # Your cache folder contains root-owned files, due to a bug in
- # npm ERR! previous versions of npm which has since been addressed.
- - run: chown -R 1001:121 "/github/home/.npm"
- - run: npm i --force
- - run: cd test/with-typescript && npm i --force
+ - uses: actions/checkout@v4
+ - run: npm ci
+ - run: cd test/with-typescript && npm ci
- run: ./hooks/pre-commit.sh
diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml
index a799a33da..92d5b9229 100644
--- a/.github/workflows/test-examples.yml
+++ b/.github/workflows/test-examples.yml
@@ -6,7 +6,7 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- run: bash test/findExamplesWithTests.sh
- id: set-matrix
run: echo "::set-output name=matrix::{\"include\":$(bash test/findExamplesWithTests.sh)}"
@@ -24,7 +24,7 @@ jobs:
run:
working-directory: ${{ matrix.examplePath }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- run: bash ../../test/updateExampleAppDeps.sh .
- run: npm install mocha@6.1.4 jsdom-global@3.0.2 puppeteer@^11.0.0 isomorphic-fetch@^3.0.0
- run: npm run build || true
@@ -36,7 +36,7 @@ jobs:
)
- name: The job has failed
if: ${{ failure() }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./**/*screenshot.jpeg
diff --git a/.github/workflows/tests-pass-check-pr.yml b/.github/workflows/tests-pass-check-pr.yml
deleted file mode 100644
index cfa2cbaec..000000000
--- a/.github/workflows/tests-pass-check-pr.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: 'Check if "Run tests" action succeeded'
-
-on:
- pull_request:
- types:
- - opened
- - reopened
- - edited
- - synchronize
-
-jobs:
- pr-run-test-action:
- name: Check if "Run tests" action succeeded
- timeout-minutes: 60
- concurrency:
- group: ${{ github.head_ref }}
- cancel-in-progress: true
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: node install
- run: cd ./.github/helpers && npm i
- - name: Calling github API
- run: cd ./.github/helpers && GITHUB_TOKEN=${{ github.token }} REPO=${{ github.repository }} RUN_ID=${{ github.run_id }} BRANCH=${{ github.head_ref }} JOB_ID=${{ github.job }} SOURCE_OWNER=${{ github.event.pull_request.head.repo.owner.login }} CURRENT_SHA=${{ github.event.pull_request.head.sha }} node node_modules/github-workflow-helpers/test-pass-check-pr.js
diff --git a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx
index 1e116dd3f..8fd9051ec 100644
--- a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx
+++ b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx
@@ -54,6 +54,7 @@ export const LinkingPage: React.FC = () => {
try {
let response = await Passwordless.createCode({
phoneNumber,
+ shouldTryLinkingWithSessionUser: true,
});
if (cancel) {
diff --git a/examples/with-multifactorauth-phone-chooser/frontend/package.json b/examples/with-multifactorauth-phone-chooser/frontend/package.json
index 9bd476ef3..8d12ddcb7 100644
--- a/examples/with-multifactorauth-phone-chooser/frontend/package.json
+++ b/examples/with-multifactorauth-phone-chooser/frontend/package.json
@@ -15,7 +15,7 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.1",
- "supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/mfa_redirect",
+ "supertokens-auth-react": "github:supertokens/supertokens-auth-react",
"supertokens-web-js": "latest",
"typescript": "^4.8.2",
"web-vitals": "^2.1.4"
diff --git a/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx b/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx
index 7feaec211..34181bbbf 100644
--- a/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx
+++ b/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx
@@ -23,7 +23,10 @@ export default function SelectPhone() {
navigate: nav,
});
} else if (loadedInfo.user.phoneNumbers.length === 1) {
- await Passwordless.createCode({ phoneNumber: loadedInfo.user.phoneNumbers[0] });
+ await Passwordless.createCode({
+ phoneNumber: loadedInfo.user.phoneNumbers[0],
+ shouldTryLinkingWithSessionUser: true,
+ });
await MultiFactorAuth.redirectToFactor({
factorId: MultiFactorAuth.FactorIds.OTP_PHONE,
redirectBack: false,
@@ -60,7 +63,7 @@ export default function SelectPhone() {
{
- Passwordless.createCode({ phoneNumber: number })
+ Passwordless.createCode({ phoneNumber: number, shouldTryLinkingWithSessionUser: true })
.then(async (info) => {
if (info.status !== "OK") {
setError(info.reason);
@@ -72,6 +75,7 @@ export default function SelectPhone() {
contactMethod: "PHONE",
contactInfo: number,
hasOtherPhoneNumbers: true,
+ shouldTryLinkingWithSessionUser: true,
},
});
return MultiFactorAuth.redirectToFactor({
diff --git a/examples/with-multiple-email-sign-in/api-server/epOverride.ts b/examples/with-multiple-email-sign-in/api-server/epOverride.ts
index b37bd0db3..13c151fa1 100644
--- a/examples/with-multiple-email-sign-in/api-server/epOverride.ts
+++ b/examples/with-multiple-email-sign-in/api-server/epOverride.ts
@@ -7,7 +7,7 @@ export function epOverride(oI: APIInterface): APIInterface {
signInPOST: async function (input) {
const emailField = input.formFields.find((f) => f.id === "email")!;
- let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value);
+ let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value as string);
if (primaryEmail !== undefined) {
emailField.value = primaryEmail;
}
@@ -16,7 +16,7 @@ export function epOverride(oI: APIInterface): APIInterface {
signUpPOST: async function (input) {
const emailField = input.formFields.find((f) => f.id === "email")!;
- let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value);
+ let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value as string);
if (primaryEmail !== undefined) {
emailField.value = primaryEmail;
}
diff --git a/examples/with-phone-password-mfa/api-server/index.ts b/examples/with-phone-password-mfa/api-server/index.ts
index 213df7df6..61d3aa417 100644
--- a/examples/with-phone-password-mfa/api-server/index.ts
+++ b/examples/with-phone-password-mfa/api-server/index.ts
@@ -77,7 +77,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
@@ -93,7 +93,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
diff --git a/examples/with-phone-password/api-server/index.ts b/examples/with-phone-password/api-server/index.ts
index 6a697598a..e7b6c7e0b 100644
--- a/examples/with-phone-password/api-server/index.ts
+++ b/examples/with-phone-password/api-server/index.ts
@@ -75,7 +75,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
@@ -91,7 +91,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
diff --git a/examples/with-svelte-react-thirdpartyemailpassword/package.json b/examples/with-svelte-react-thirdpartyemailpassword/package.json
index a744965ad..c89e4172a 100644
--- a/examples/with-svelte-react-thirdpartyemailpassword/package.json
+++ b/examples/with-svelte-react-thirdpartyemailpassword/package.json
@@ -43,6 +43,6 @@
"sirv-cli": "^2.0.0",
"supertokens-auth-react": "latest",
"supertokens-node": "latest",
- "svelte-navigator": "^3.1.5"
+ "svelte-navigator": "latest"
}
}
diff --git a/test/end-to-end/accountlinking.test.js b/test/end-to-end/accountlinking.test.js
index 0c4e03adc..71be719c1 100644
--- a/test/end-to-end/accountlinking.test.js
+++ b/test/end-to-end/accountlinking.test.js
@@ -831,7 +831,7 @@ async function tryEmailPasswordSignUp(page, email) {
await new Promise((res) => setTimeout(res, 250));
}
-async function tryPasswordlessSignInUp(page, email) {
+export async function tryPasswordlessSignInUp(page, email) {
await page.evaluate(() => localStorage.removeItem("supertokens-passwordless-loginAttemptInfo"));
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth/?authRecipe=passwordless`),
diff --git a/test/end-to-end/webauthn.accountlinking.test.js b/test/end-to-end/webauthn.accountlinking.test.js
new file mode 100644
index 000000000..8199a2b03
--- /dev/null
+++ b/test/end-to-end/webauthn.accountlinking.test.js
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2022, SuperTokens.com
+ * All rights reserved.
+ */
+
+import { TEST_CLIENT_BASE_URL } from "../constants";
+import {
+ setupBrowser,
+ screenshotOnFailure,
+ clearBrowserCookiesWithoutAffectingConsole,
+ toggleSignInSignUp,
+ getTestEmail,
+ waitForSTElement,
+ submitForm,
+ setInputValues,
+ getPasswordlessDevice,
+ waitForUrl,
+ changeEmail,
+ getLatestURLWithToken,
+ getUserIdWithFetch,
+ submitFormUnsafe,
+ backendHook,
+ setupCoreApp,
+ setupST,
+ isWebauthnSupported,
+} from "../helpers";
+import {
+ openRecoveryAccountPage,
+ tryWebauthnSignUp,
+ getTokenFromEmail,
+ openRecoveryWithToken,
+ tryWebauthnSignIn,
+} from "./webauthn.helpers";
+import { tryPasswordlessSignInUp } from "./accountlinking.test";
+import assert from "assert";
+
+/*
+ * Test case:
+ * 1. The app has account linking disabled
+ * 2. A user signs up using a non-webauthn factor (e.g.: passwordless)
+ * 3. The user now tries signing up with webauthn using the same email
+ * -> this should work and create an entirely separate user with an unverified email address
+ */
+describe("SuperTokens WebAuthn Account Linking", function () {
+ let browser;
+ let page;
+ let consoleLogs = [];
+ let skipped = false;
+ const appConfig = {
+ enabledRecipes: [
+ "webauthn",
+ "emailpassword",
+ "session",
+ "dashboard",
+ "userroles",
+ "multifactorauth",
+ "passwordless",
+ "emailverification",
+ "accountlinking",
+ ],
+ };
+
+ before(async function () {
+ if (!(await isWebauthnSupported())) {
+ skipped = true;
+ this.skip();
+ }
+
+ await backendHook("before");
+ const coreUrl = await setupCoreApp();
+ appConfig.coreUrl = coreUrl;
+ await setupST(appConfig);
+
+ browser = await setupBrowser();
+ page = await browser.newPage();
+ page.on("console", (consoleObj) => {
+ const log = consoleObj.text();
+ if (log.startsWith("ST_LOGS")) {
+ consoleLogs.push(log);
+ }
+ });
+ });
+
+ after(async function () {
+ if (skipped) {
+ return;
+ }
+
+ await page?.close();
+ await browser?.close();
+ await backendHook("after");
+ });
+
+ afterEach(async function () {
+ await screenshotOnFailure(this, browser);
+ await backendHook("afterEach");
+ });
+
+ beforeEach(async function () {
+ await backendHook("beforeEach");
+ consoleLogs = [];
+ consoleLogs = await clearBrowserCookiesWithoutAffectingConsole(page, consoleLogs);
+ await toggleSignInSignUp(page);
+ });
+
+ it("Should create separate users when signing up with same email using different auth methods (account linking disabled)", async function () {
+ // Disable account linking
+ await setupST({
+ ...appConfig,
+ accountLinkingConfig: {
+ enabled: true,
+ shouldAutoLink: {
+ shouldAutomaticallyLink: false,
+ shouldRequireVerification: false,
+ },
+ },
+ });
+ const email = await getTestEmail();
+
+ await Promise.all([
+ page.goto(`${TEST_CLIENT_BASE_URL}/auth?authRecipe=passwordless`),
+ page.waitForNavigation({ waitUntil: "networkidle0" }),
+ ]);
+
+ // Signup using the email
+ await setInputValues(page, [{ name: "email", value: email }]);
+ await submitForm(page);
+
+ 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.waitForTimeout(2000);
+
+ // We want to parse the text inside the session-context-userId div
+ const userId1 = await page.evaluate(() => document.querySelector(".session-context-userId").textContent);
+
+ // Find the div with classname logoutButton and click it using normal
+ // puppeteer selector
+ const logoutButton = await page.waitForSelector("div.logoutButton");
+ await logoutButton.click();
+ await new Promise((res) => setTimeout(res, 1000));
+
+ await tryWebauthnSignUp(page, email);
+
+ // We should be in the confirmation page now.
+ await submitForm(page);
+ await page.waitForTimeout(4000);
+
+ // Extract second userId from console logs
+ const userId2 = await page.evaluate(() => document.querySelector(".session-context-userId").textContent);
+
+ // Verify that two different users were created
+ assert.notStrictEqual(
+ userId1,
+ userId2,
+ "Different auth methods with same email should create separate users when account linking is disabled"
+ );
+ });
+
+ it("should handle email updates correctly for user that signed up with webauthn", async () => {
+ await page.evaluate(() => window.localStorage.setItem("mode", "REQUIRED"));
+ await setupST({
+ ...appConfig,
+ accountLinkingConfig: {
+ enabled: true,
+ shouldAutoLink: {
+ shouldAutomaticallyLink: false,
+ shouldRequireVerification: false,
+ },
+ },
+ });
+ const email = await getTestEmail();
+
+ await tryWebauthnSignUp(page, email);
+
+ // We should be in the confirmation page now.
+ await submitForm(page);
+
+ await waitForUrl(page, "/auth/verify-email");
+
+ // we wait for email to be created
+ await new Promise((r) => setTimeout(r, 1000));
+
+ // we fetch the email verification link and go to that
+ const latestURLWithToken = await getLatestURLWithToken();
+ await Promise.all([page.waitForNavigation({ waitUntil: "networkidle0" }), page.goto(latestURLWithToken)]);
+
+ // click on the continue button
+ await Promise.all([submitForm(page), page.waitForNavigation({ waitUntil: "networkidle0" })]);
+ await waitForUrl(page, "/dashboard");
+
+ await page.waitForTimeout(4000);
+
+ // Change the email for the webauthn user
+ await Promise.all([page.waitForSelector(".sessionInfo-user-id"), page.waitForNetworkIdle()]);
+ const recipeUserId = await getUserIdWithFetch(page);
+ assert.ok(recipeUserId);
+
+ // Find the div with classname logoutButton and click it using normal
+ // puppeteer selector
+ const logoutButton = await page.waitForSelector("div.logoutButton");
+ await logoutButton.click();
+ await new Promise((res) => setTimeout(res, 1000));
+
+ const newEmail = getTestEmail("new");
+ const res = await changeEmail("webauthn", recipeUserId, newEmail, null);
+
+ // Sign in with the new email
+ await tryWebauthnSignIn(page);
+
+ // Since mode is required, user should be redirected to verify email
+ // screen as the email was changed and the new email is not verified.
+ await waitForUrl(page, "/auth/verify-email");
+
+ await page.waitForTimeout(4000);
+ });
+
+ it("should allow same emails to be linked but requiring verification", async () => {
+ await page.evaluate(() => window.localStorage.setItem("mode", "REQUIRED"));
+ await setupST({
+ ...appConfig,
+ accountLinkingConfig: {
+ enabled: true,
+ shouldAutoLink: {
+ shouldAutomaticallyLink: true,
+ shouldRequireVerification: true,
+ },
+ },
+ });
+ const email = await getTestEmail();
+
+ await tryPasswordlessSignInUp(page, email);
+ await page.waitForTimeout(1000);
+
+ await Promise.all([page.waitForSelector(".sessionInfo-user-id"), page.waitForNetworkIdle()]);
+ const userId1 = await getUserIdWithFetch(page);
+ assert.ok(userId1);
+
+ await page.waitForTimeout(1000);
+
+ // Find the div with classname logoutButton and click it using normal
+ // puppeteer selector
+ const logoutButton = await page.waitForSelector("div.logoutButton");
+ await logoutButton.click();
+ await new Promise((res) => setTimeout(res, 1000));
+
+ // Try to signup with the same email through webauthn now
+ await tryWebauthnSignUp(page, email);
+
+ // We should be in the confirmation page now.
+ await submitForm(page);
+
+ await page.waitForTimeout(1000);
+ await waitForSTElement(page, "[data-supertokens~='generalError']");
+
+ // Try to recover the webauthn account using the same email
+ await openRecoveryAccountPage(page, email, true);
+ await page.waitForTimeout(1000);
+
+ // Get the token from the email
+ const token = await getTokenFromEmail(email);
+ assert.ok(token);
+
+ // Use the token to recover the account
+ await openRecoveryWithToken(page, token);
+
+ // We should be in the recovery page now, click the continue button
+ await submitFormUnsafe(page);
+
+ await new Promise((res) => setTimeout(res, 2000));
+
+ const successContainer = await waitForSTElement(page, "[data-supertokens~='headerText']");
+ const headerText = await successContainer.evaluate((el) => el.textContent);
+
+ // Assert the text contains "Account recovered successfully!"
+ assert.deepStrictEqual(headerText, "Account recovered successfully!");
+ });
+});
diff --git a/test/updateExampleAppDeps.sh b/test/updateExampleAppDeps.sh
index be8602111..4ea3f129d 100755
--- a/test/updateExampleAppDeps.sh
+++ b/test/updateExampleAppDeps.sh
@@ -1,7 +1,7 @@
#!/bin/bash
cd $1;
-npm i;
+npm i --ignore-scripts;
npm install git+https://github.com:supertokens/supertokens-auth-react.git#$GITHUB_SHA;
if [ -d "frontend" ]; then