-
Notifications
You must be signed in to change notification settings - Fork 95
feat: webauthn test flows #883
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
Merged
Merged
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
2ef4cb9
Add third flow for testing webauthn signup on accountlinking disabled
deepjyoti30-st 5a8a6d5
Add init support for testing e2e test flow 2 with webauthn user chang…
deepjyoti30-st 4031702
Add some changes for the test flows
deepjyoti30-st 37eea51
Add fixes for the e2e tests
deepjyoti30-st a23dda2
Add fix for properly checking error for webauthn
deepjyoti30-st 0f6a3f6
Undo index changes
deepjyoti30-st 2414931
test: update test to match new testing method
porcellus f947a91
Remove console log
deepjyoti30-st 0ca95fb
Remove run tests check workflow
deepjyoti30-st d63bf45
Update pre-commit hook workflow to make it pass
deepjyoti30-st aa490f6
Fix examples test workflow to use latest actions
deepjyoti30-st 6630842
Get rid of git commit step in pre-commit hook workflow
deepjyoti30-st 1aa77da
Update pre-commit hook to remove chown step
deepjyoti30-st 557a131
Fix dep version for svelte-navigator crashing the npm i step
deepjyoti30-st 350f74d
Skip install post install scripts for deps
deepjyoti30-st 0c57b8a
Fix some example build failures leading to test failures
deepjyoti30-st c05d096
Fix another example build failure leading to failing test
deepjyoti30-st 08e01fe
Add fix for accountlinking and mfa tests
deepjyoti30-st 8ac94ab
Fix webauthn accountlinking tests
deepjyoti30-st File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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='[email protected]' 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 [email protected] [email protected] 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 | ||
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| /* | ||
| * 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"], | ||
| }; | ||
|
|
||
| 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!"); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.