From 8540f9e74044ad545d1bf6fad4aca90acbb6f31a Mon Sep 17 00:00:00 2001 From: Ankit Tiwari Date: Thu, 1 Aug 2024 20:50:18 +0530 Subject: [PATCH 1/4] test: Add e2e test for OAuth2 --- examples/for-tests/package.json | 1 + examples/for-tests/src/App.js | 16 +- .../for-tests/src/AppWithReactDomRouter.js | 4 + examples/for-tests/src/OAuth2Page.js | 49 +++++ examples/for-tests/src/config.js | 13 ++ test/end-to-end/oauth2provider.test.js | 187 ++++++++++++++++++ test/helpers.js | 33 +++- test/server/index.js | 12 +- 8 files changed, 300 insertions(+), 15 deletions(-) create mode 100644 examples/for-tests/src/OAuth2Page.js create mode 100644 examples/for-tests/src/config.js create mode 100644 test/end-to-end/oauth2provider.test.js diff --git a/examples/for-tests/package.json b/examples/for-tests/package.json index 9f67560d0..19770ff4f 100644 --- a/examples/for-tests/package.json +++ b/examples/for-tests/package.json @@ -6,6 +6,7 @@ "axios": "^0.21.0", "react": "^18.0.0", "react-dom": "^18.0.0", + "react-oauth2-code-pkce": "^1.20.1", "react-router-dom": "6.11.2", "react-scripts": "^5.0.1" }, diff --git a/examples/for-tests/src/App.js b/examples/for-tests/src/App.js index 048337ec8..7bdb1092c 100644 --- a/examples/for-tests/src/App.js +++ b/examples/for-tests/src/App.js @@ -14,6 +14,7 @@ import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; import UserRoles from "supertokens-auth-react/recipe/userroles"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import TOTP from "supertokens-auth-react/recipe/totp"; +import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; import axios from "axios"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; @@ -27,6 +28,7 @@ import { logWithPrefix } from "./logWithPrefix"; import { ErrorBoundary } from "./ErrorBoundary"; import { useNavigate } from "react-router-dom"; import { getTestContext, getEnabledRecipes, getQueryParams } from "./testContext"; +import { getApiDomain, getWebsiteDomain } from "./config"; const loadv5RRD = window.localStorage.getItem("react-router-dom-is-v5") === "true"; if (loadv5RRD) { @@ -43,18 +45,6 @@ const withRouter = function (Child) { Session.addAxiosInterceptors(axios); -export function getApiDomain() { - const apiPort = process.env.REACT_APP_API_PORT || 8082; - const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`; - return apiUrl; -} - -export function getWebsiteDomain() { - const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3031; - const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`; - return getQueryParams("websiteDomain") ?? websiteUrl; -} - /* * Use localStorage for tests configurations. */ @@ -419,6 +409,7 @@ let recipeList = [ console.log(`ST_LOGS SESSION ON_HANDLE_EVENT ${ctx.action}`); }, }), + OAuth2Provider.init(), ]; let enabledRecipes = getEnabledRecipes(); @@ -801,7 +792,6 @@ function getSignInFormFields(formType) { id: "test", }, ]; - return; } } diff --git a/examples/for-tests/src/AppWithReactDomRouter.js b/examples/for-tests/src/AppWithReactDomRouter.js index a545bb447..f53ee0f30 100644 --- a/examples/for-tests/src/AppWithReactDomRouter.js +++ b/examples/for-tests/src/AppWithReactDomRouter.js @@ -12,6 +12,7 @@ import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifa import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; import { BaseComponent, Home, Contact, Dashboard, DashboardNoAuthRequired } from "./App"; import { getEnabledRecipes, getTestContext } from "./testContext"; +import OAuth2Page from "./OAuth2Page"; function AppWithReactDomRouter(props) { /** @@ -172,6 +173,9 @@ function AppWithReactDomRouter(props) { } /> )} + + } /> + } /> diff --git a/examples/for-tests/src/OAuth2Page.js b/examples/for-tests/src/OAuth2Page.js new file mode 100644 index 000000000..b8584a0f2 --- /dev/null +++ b/examples/for-tests/src/OAuth2Page.js @@ -0,0 +1,49 @@ +import { useContext } from "react"; +import { AuthContext, AuthProvider } from "react-oauth2-code-pkce"; +import { getApiDomain, getWebsiteDomain } from "./config"; + +const authConfig = { + clientId: window.localStorage.getItem("oauth2-client-id"), + authorizationEndpoint: `${getApiDomain()}/auth/oauth2provider/auth`, + tokenEndpoint: `${getApiDomain()}/auth/oauth2provider/token`, + redirectUri: `${getWebsiteDomain()}/oauth2/callback`, + scope: "profile openid offline_access email", + state: Math.random().toString(36).substring(2), + autoLogin: false, + decodeToken: true, +}; + +function AuthPage() { + const { tokenData, logIn, logOut, error } = useContext(AuthContext); + + return ( +
+

OAuth2 Login Test

+
+ {tokenData ? ( +
+
{JSON.stringify(tokenData, null, 2)}
+ +
+ ) : ( +
+ {error &&

Error: {error}

} + +
+ )} +
+
+ ); +} + +export default function OAuth2Page() { + return ( + + + + ); +} diff --git a/examples/for-tests/src/config.js b/examples/for-tests/src/config.js new file mode 100644 index 000000000..ae591c224 --- /dev/null +++ b/examples/for-tests/src/config.js @@ -0,0 +1,13 @@ +import { getQueryParams } from "./testContext"; + +export function getApiDomain() { + const apiPort = process.env.REACT_APP_API_PORT || 8082; + const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`; + return apiUrl; +} + +export function getWebsiteDomain() { + const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3031; + const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`; + return getQueryParams("websiteDomain") ?? websiteUrl; +} diff --git a/test/end-to-end/oauth2provider.test.js b/test/end-to-end/oauth2provider.test.js new file mode 100644 index 000000000..998981ab7 --- /dev/null +++ b/test/end-to-end/oauth2provider.test.js @@ -0,0 +1,187 @@ +/* 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, + toggleSignInSignUp, + screenshotOnFailure, + backendBeforeEach, + waitForUrl, + createOAuth2Client, + setOAuth2ClientIdInStorage, + removeOAuth2ClientIdFromStorage, + getOAuth2LoginButton, + getOAuth2LogoutButton, + getOAuth2TokenData, + isReact16, + waitFor, + signUp, + getDefaultSignUpFieldValues, + getTestEmail, +} from "../helpers"; +import fetch from "isomorphic-fetch"; + +import { TEST_CLIENT_BASE_URL, TEST_SERVER_BASE_URL, SIGN_OUT_API } from "../constants"; + +/* + * Tests. + */ +describe("SuperTokens OAuth2Provider", function () { + let browser; + let page; + let consoleLogs = []; + + before(async function () { + // Skip these tests if running in React 16 + if (isReact16()) { + this.skip(); + } + + 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: true, + }); + }); + + 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("Generic OAuth2 Client Library", function () { + afterEach(async function () { + await removeOAuth2ClientIdFromStorage(page); + }); + + it("should successfully complete the OAuth2 flow", async function () { + const { client } = await createOAuth2Client({ + scope: "offline_access profile openid email", + redirectUris: [`${TEST_CLIENT_BASE_URL}/oauth2/callback`], + accessTokenStrategy: "jwt", + tokenEndpointAuthMethod: "none", + grantTypes: ["authorization_code", "refresh_token"], + responseTypes: ["code", "id_token"], + skipConsent: true, + }); + + await setOAuth2ClientIdInStorage(page, client.clientId); + + await page.goto(`${TEST_CLIENT_BASE_URL}/oauth2/login`); + + let loginButton = await getOAuth2LoginButton(page); + await loginButton.click(); + + await waitForUrl(page, "/auth"); + + await toggleSignInSignUp(page); + const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: getTestEmail() }); + await signUp(page, fieldValues, postValues, "emailpassword"); + + await waitForUrl(page, "/oauth2/callback"); + + // Validate token data + const tokenData = await getOAuth2TokenData(page); + assert.deepStrictEqual(tokenData.client_id, client.clientId); + + // Logout + const logoutButton = await getOAuth2LogoutButton(page); + await logoutButton.click(); + + // Ensure the Login Button is visible after logout is clicked + loginButton = await getOAuth2LoginButton(page); + assert.ok(loginButton !== null); + }); + + it("should successfully refresh the tokens after expiry", async function () { + const { client } = await createOAuth2Client({ + scope: "offline_access profile openid email", + redirectUris: [`${TEST_CLIENT_BASE_URL}/oauth2/callback`], + accessTokenStrategy: "jwt", + tokenEndpointAuthMethod: "none", + grantTypes: ["authorization_code", "refresh_token"], + responseTypes: ["code", "id_token"], + skipConsent: true, + // Setting access token lifespan to 3 seconds to force refresh + authorizationCodeGrantAccessTokenLifespan: "3s", + }); + + await setOAuth2ClientIdInStorage(page, client.clientId); + + await page.goto(`${TEST_CLIENT_BASE_URL}/oauth2/login`); + + let loginButton = await getOAuth2LoginButton(page); + await loginButton.click(); + + await waitForUrl(page, "/auth"); + + await toggleSignInSignUp(page); + const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: getTestEmail() }); + await signUp(page, fieldValues, postValues, "emailpassword"); + + await waitForUrl(page, "/oauth2/callback"); + + // Validate token data + const tokenDataAfterLogin = await getOAuth2TokenData(page); + assert.deepStrictEqual(tokenDataAfterLogin.client_id, client.clientId); + + // The react-oauth2-code-pkce library refreshes the token in an interval of 10 seconds. + // To force the refresh before that, we wait for 4 seconds and reload the page. + await waitFor(4000); + await page.reload(); + await page.waitForNavigation({ waitUntil: "networkidle0" }); + + const tokenDataAfterRefresh = await getOAuth2TokenData(page); + assert.deepStrictEqual(tokenDataAfterRefresh.client_id, client.clientId); + + // Validate the token was refreshed + assert(tokenDataAfterLogin.jti !== tokenDataAfterRefresh.jti); + assert(tokenDataAfterLogin.exp < tokenDataAfterRefresh.exp); + }); + }); +}); diff --git a/test/helpers.js b/test/helpers.js index bb8b123e3..1c51ccd2f 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -958,7 +958,7 @@ export async function setGeneralErrorToLocalStorage(recipeName, action, page) { }); } -export async function getTestEmail(post) { +export function getTestEmail(post) { return `john.doe+${Date.now()}-${post ?? "0"}@supertokens.io`; } @@ -1105,3 +1105,34 @@ export async function expectErrorThrown(page, cb) { await Promise.all([hitErrorBoundary, cb()]); assert(hitErrorBoundary); } + +export async function createOAuth2Client(input) { + const resp = await fetch(`${TEST_APPLICATION_SERVER_BASE_URL}/test/create-oauth2-client`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(input), + }); + return await resp.json(); +} + +export async function setOAuth2ClientIdInStorage(page, clientId) { + return page.evaluate((clientId) => localStorage.setItem("oauth2-client-id", clientId), clientId); +} + +export async function removeOAuth2ClientIdFromStorage(page) { + return page.evaluate(() => localStorage.removeItem("oauth2-client-id")); +} + +export async function getOAuth2LoginButton(page) { + return page.waitForSelector("#oauth2-login-button"); +} + +export async function getOAuth2LogoutButton(page) { + return page.waitForSelector("#oauth2-logout-button"); +} + +export async function getOAuth2TokenData(page) { + const element = await page.waitForSelector("#oauth2-token-data"); + const tokenData = await element.evaluate((el) => el.textContent); + return JSON.parse(tokenData); +} diff --git a/test/server/index.js b/test/server/index.js index a59381352..a08be9784 100644 --- a/test/server/index.js +++ b/test/server/index.js @@ -52,7 +52,7 @@ const UserRolesRaw = require("supertokens-node/lib/build/recipe/userroles/recipe const UserRoles = require("supertokens-node/recipe/userroles"); const MultitenancyRaw = require("supertokens-node/lib/build/recipe/multitenancy/recipe").default; -const Multitenancy = require("supertokens-node/lib/build/recipe/multitenancy"); +const Multitenancy = require("supertokens-node/recipe/multitenancy"); const AccountLinkingRaw = require("supertokens-node/lib/build/recipe/accountlinking/recipe").default; const AccountLinking = require("supertokens-node/recipe/accountlinking"); @@ -66,6 +66,9 @@ const MultiFactorAuth = require("supertokens-node/recipe/multifactorauth"); const TOTPRaw = require("supertokens-node/lib/build/recipe/totp/recipe").default; const TOTP = require("supertokens-node/recipe/totp"); +const OAuth2ProviderRaw = require("supertokens-node/lib/build/recipe/oauth2provider/recipe").default; +const OAuth2Provider = require("supertokens-node/recipe/oauth2provider"); + const OTPAuth = require("otpauth"); let generalErrorSupported; @@ -489,6 +492,11 @@ app.get("/test/featureFlags", (req, res) => { }); }); +app.post("/test/create-oauth2-client", async (req, res) => { + const { client } = await OAuth2Provider.createOAuth2Client(req.body); + res.send({ client }); +}); + app.use(errorHandler()); app.use(async (err, req, res, next) => { @@ -532,6 +540,7 @@ function initST() { UserMetadataRaw.reset(); MultiFactorAuthRaw.reset(); TOTPRaw.reset(); + OAuth2ProviderRaw.reset(); EmailVerificationRaw.reset(); EmailPasswordRaw.reset(); @@ -730,6 +739,7 @@ function initST() { }, }), ], + ["oauth2provider", OAuth2Provider.init()], ]; passwordlessConfig = { From d866aa52a1f28c4ffae575c34c7ec1d69d0a2344 Mon Sep 17 00:00:00 2001 From: Ankit Tiwari Date: Fri, 2 Aug 2024 20:28:06 +0530 Subject: [PATCH 2/4] fix: PR changes --- examples/for-tests/package.json | 3 ++- examples/for-tests/src/OAuth2Page.js | 37 ++++++++++++++------------ test/end-to-end/oauth2provider.test.js | 22 +++++++++------ test/helpers.js | 2 ++ 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/examples/for-tests/package.json b/examples/for-tests/package.json index 19770ff4f..04323793d 100644 --- a/examples/for-tests/package.json +++ b/examples/for-tests/package.json @@ -4,9 +4,10 @@ "private": true, "dependencies": { "axios": "^0.21.0", + "oidc-client-ts": "^3.0.1", "react": "^18.0.0", "react-dom": "^18.0.0", - "react-oauth2-code-pkce": "^1.20.1", + "react-oidc-context": "^3.1.0", "react-router-dom": "6.11.2", "react-scripts": "^5.0.1" }, diff --git a/examples/for-tests/src/OAuth2Page.js b/examples/for-tests/src/OAuth2Page.js index b8584a0f2..76ab8b130 100644 --- a/examples/for-tests/src/OAuth2Page.js +++ b/examples/for-tests/src/OAuth2Page.js @@ -1,36 +1,39 @@ -import { useContext } from "react"; -import { AuthContext, AuthProvider } from "react-oauth2-code-pkce"; +import { AuthProvider, useAuth } from "react-oidc-context"; import { getApiDomain, getWebsiteDomain } from "./config"; -const authConfig = { - clientId: window.localStorage.getItem("oauth2-client-id"), - authorizationEndpoint: `${getApiDomain()}/auth/oauth2provider/auth`, - tokenEndpoint: `${getApiDomain()}/auth/oauth2provider/token`, - redirectUri: `${getWebsiteDomain()}/oauth2/callback`, +// NOTE: For convenience, the same page/component handles both login initiation and callback. +// Separate pages for login and callback are not required. + +const oidcConfig = { + client_id: window.localStorage.getItem("oauth2-client-id"), + authority: `${getApiDomain()}/auth`, + response_type: "code", + redirect_uri: `${getWebsiteDomain()}/oauth2/callback`, scope: "profile openid offline_access email", - state: Math.random().toString(36).substring(2), - autoLogin: false, - decodeToken: true, + onSigninCallback: async (user) => { + // Clears the response code and other params from the callback url + window.history.replaceState({}, document.title, window.location.pathname); + }, }; function AuthPage() { - const { tokenData, logIn, logOut, error } = useContext(AuthContext); + const { signinRedirect, signoutSilent, user, error } = useAuth(); return (

OAuth2 Login Test

- {tokenData ? ( + {user ? (
-
{JSON.stringify(tokenData, null, 2)}
-
) : (
- {error &&

Error: {error}

} -
@@ -42,7 +45,7 @@ function AuthPage() { export default function OAuth2Page() { return ( - + ); diff --git a/test/end-to-end/oauth2provider.test.js b/test/end-to-end/oauth2provider.test.js index 998981ab7..154a2c1b5 100644 --- a/test/end-to-end/oauth2provider.test.js +++ b/test/end-to-end/oauth2provider.test.js @@ -48,10 +48,12 @@ describe("SuperTokens OAuth2Provider", function () { let browser; let page; let consoleLogs = []; + let skipped = false; before(async function () { // Skip these tests if running in React 16 if (isReact16()) { + skipped = true; this.skip(); } @@ -68,6 +70,9 @@ describe("SuperTokens OAuth2Provider", function () { }); after(async function () { + if (skipped) { + return; + } await browser.close(); await fetch(`${TEST_SERVER_BASE_URL}/after`, { @@ -127,7 +132,7 @@ describe("SuperTokens OAuth2Provider", function () { // Validate token data const tokenData = await getOAuth2TokenData(page); - assert.deepStrictEqual(tokenData.client_id, client.clientId); + assert.deepStrictEqual(tokenData.aud, [client.clientId]); // Logout const logoutButton = await getOAuth2LogoutButton(page); @@ -147,8 +152,9 @@ describe("SuperTokens OAuth2Provider", function () { grantTypes: ["authorization_code", "refresh_token"], responseTypes: ["code", "id_token"], skipConsent: true, - // Setting access token lifespan to 3 seconds to force refresh - authorizationCodeGrantAccessTokenLifespan: "3s", + // The library refreshes the token 60 seconds before it expires. + // We set the token lifespan to 63 seconds to force a refresh in 3 seconds. + authorizationCodeGrantAccessTokenLifespan: "63s", }); await setOAuth2ClientIdInStorage(page, client.clientId); @@ -168,19 +174,19 @@ describe("SuperTokens OAuth2Provider", function () { // Validate token data const tokenDataAfterLogin = await getOAuth2TokenData(page); - assert.deepStrictEqual(tokenDataAfterLogin.client_id, client.clientId); + assert.deepStrictEqual(tokenDataAfterLogin.aud, [client.clientId]); - // The react-oauth2-code-pkce library refreshes the token in an interval of 10 seconds. - // To force the refresh before that, we wait for 4 seconds and reload the page. + // Although the react-oidc-context library automatically refreshes the + // token, we wait for 4 seconds and reload the page to ensure a refresh. await waitFor(4000); await page.reload(); await page.waitForNavigation({ waitUntil: "networkidle0" }); const tokenDataAfterRefresh = await getOAuth2TokenData(page); - assert.deepStrictEqual(tokenDataAfterRefresh.client_id, client.clientId); + assert.deepStrictEqual(tokenDataAfterRefresh.aud, [client.clientId]); // Validate the token was refreshed - assert(tokenDataAfterLogin.jti !== tokenDataAfterRefresh.jti); + assert(tokenDataAfterLogin.iat !== tokenDataAfterRefresh.iat); assert(tokenDataAfterLogin.exp < tokenDataAfterRefresh.exp); }); }); diff --git a/test/helpers.js b/test/helpers.js index 1c51ccd2f..bee2f2a27 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1115,6 +1115,8 @@ export async function createOAuth2Client(input) { return await resp.json(); } +// For the OAuth2 end-to-end test, we need to provide the created clientId to both the OAuth2 login and callback pages. +// We use localStorage to store the clientId instead of query params, as it must be available on the callback page as well. export async function setOAuth2ClientIdInStorage(page, clientId) { return page.evaluate((clientId) => localStorage.setItem("oauth2-client-id", clientId), clientId); } From 1614036447a43c8a94cb6a27ef33dbc22bc3abfc Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Sun, 4 Aug 2024 22:07:31 +0200 Subject: [PATCH 3/4] feat: add tryLinkingWithSessionUser, forceFreshAuth and small test fixes --- lib/build/emailpasswordprebuiltui.js | 92 ++++++++++++++++++- lib/build/index2.js | 6 +- lib/build/oauth2provider-shared.js | 5 +- lib/build/passwordlessprebuiltui.js | 79 +++++++++++----- lib/build/recipe/emailpassword/index.d.ts | 2 + lib/build/recipe/passwordless/index.d.ts | 2 + lib/build/recipe/session/recipe.d.ts | 6 +- lib/build/recipe/thirdparty/index.d.ts | 1 + lib/build/thirdparty-shared.js | 1 + .../components/feature/authPage/authPage.tsx | 6 +- .../components/features/signin/index.tsx | 8 ++ .../components/features/signup/index.tsx | 8 ++ .../components/themes/signIn/index.tsx | 1 + .../components/themes/signUp/index.tsx | 1 + lib/ts/recipe/emailpassword/index.ts | 2 + lib/ts/recipe/oauth2provider/recipe.ts | 2 +- .../components/features/linkSent/index.tsx | 1 + .../components/features/mfa/index.tsx | 3 + .../components/features/signInAndUp/index.tsx | 1 + .../features/signInAndUpEPCombo/index.tsx | 20 +++- .../features/userInputCode/index.tsx | 1 + .../components/themes/signInUp/emailForm.tsx | 1 + .../themes/signInUp/emailOrPhoneForm.tsx | 1 + .../components/themes/signInUp/phoneForm.tsx | 1 + lib/ts/recipe/passwordless/index.ts | 14 ++- lib/ts/recipe/session/recipe.tsx | 6 +- lib/ts/recipe/thirdparty/index.ts | 1 + lib/ts/recipe/thirdparty/utils.ts | 1 + test/end-to-end/emailverification.test.js | 1 + test/end-to-end/generalerror.test.js | 2 +- test/end-to-end/signin-rrdv6.test.js | 6 +- test/end-to-end/signin.test.js | 6 +- .../thirdpartyemailpassword.test.js | 2 +- test/helpers.js | 4 +- test/server/index.js | 18 +++- test/server/utils.js | 4 +- 36 files changed, 253 insertions(+), 63 deletions(-) diff --git a/lib/build/emailpasswordprebuiltui.js b/lib/build/emailpasswordprebuiltui.js index 28263d209..6dd2da964 100644 --- a/lib/build/emailpasswordprebuiltui.js +++ b/lib/build/emailpasswordprebuiltui.js @@ -709,6 +709,7 @@ var SignInForm = uiEntry.withOverride("EmailPasswordSignInForm", function EmailP 4 /*yield*/, props.recipeImplementation.signIn({ formFields: formFields, + tryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -953,7 +954,50 @@ var SignInFeature = function (props) { ); }; var getModifiedRecipeImplementation$1 = function (origImpl) { - return superTokens.__assign({}, origImpl); + return superTokens.__assign(superTokens.__assign({}, origImpl), { + signIn: function (input) { + return superTokens.__awaiter(this, void 0, void 0, function () { + var response; + return superTokens.__generator(this, function (_a) { + switch (_a.label) { + case 0: + return [ + 4 /*yield*/, + origImpl.signIn( + superTokens.__assign(superTokens.__assign({}, input), { + tryLinkingWithSessionUser: false, + }) + ), + ]; + case 1: + response = _a.sent(); + return [2 /*return*/, response]; + } + }); + }); + }, + signUp: function (input) { + return superTokens.__awaiter(this, void 0, void 0, function () { + var response; + return superTokens.__generator(this, function (_a) { + switch (_a.label) { + case 0: + return [ + 4 /*yield*/, + origImpl.signUp( + superTokens.__assign(superTokens.__assign({}, input), { + tryLinkingWithSessionUser: false, + }) + ), + ]; + case 1: + response = _a.sent(); + return [2 /*return*/, response]; + } + }); + }); + }, + }); }; var SignUpForm = uiEntry.withOverride("EmailPasswordSignUpForm", function EmailPasswordSignUpForm(props) { @@ -994,6 +1038,7 @@ var SignUpForm = uiEntry.withOverride("EmailPasswordSignUpForm", function EmailP 4 /*yield*/, props.recipeImplementation.signUp({ formFields: formFields, + tryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -1201,7 +1246,50 @@ var SignUpFeature = function (props) { ); }; var getModifiedRecipeImplementation = function (origImpl) { - return superTokens.__assign({}, origImpl); + return superTokens.__assign(superTokens.__assign({}, origImpl), { + signIn: function (input) { + return superTokens.__awaiter(this, void 0, void 0, function () { + var response; + return superTokens.__generator(this, function (_a) { + switch (_a.label) { + case 0: + return [ + 4 /*yield*/, + origImpl.signIn( + superTokens.__assign(superTokens.__assign({}, input), { + tryLinkingWithSessionUser: false, + }) + ), + ]; + case 1: + response = _a.sent(); + return [2 /*return*/, response]; + } + }); + }); + }, + signUp: function (input) { + return superTokens.__awaiter(this, void 0, void 0, function () { + var response; + return superTokens.__generator(this, function (_a) { + switch (_a.label) { + case 0: + return [ + 4 /*yield*/, + origImpl.signUp( + superTokens.__assign(superTokens.__assign({}, input), { + tryLinkingWithSessionUser: false, + }) + ), + ]; + case 1: + response = _a.sent(); + return [2 /*return*/, response]; + } + }); + }); + }, + }); }; function getThemeSignUpFeatureFormFields(formFields, recipe, userContext) { var _this = this; diff --git a/lib/build/index2.js b/lib/build/index2.js index 658a76f2d..77de0cac3 100644 --- a/lib/build/index2.js +++ b/lib/build/index2.js @@ -907,6 +907,7 @@ var AuthPageInner = function (props) { var showStringFromQSRef = React.useRef(showStringFromQS); var errorFromQSRef = React.useRef(errorFromQS); var loginChallenge = search.get("loginChallenge"); + var forceFreshAuth = search.get("forceFreshAuth") === "true"; var sessionContext = useSessionContext(); var userContext = useUserContext(); var rethrowInRender = superTokens.useRethrowInRender(); @@ -1021,7 +1022,7 @@ var AuthPageInner = function (props) { if (sessionContext.doesSessionExist) { if (props.onSessionAlreadyExists !== undefined) { props.onSessionAlreadyExists(); - } else if (props.redirectOnSessionExists !== false) { + } else if (props.redirectOnSessionExists !== false && !forceFreshAuth) { types.Session.getInstanceOrThrow().config.onHandleEvent({ action: "SESSION_ALREADY_EXISTS", }); @@ -1128,7 +1129,8 @@ var AuthPageInner = function (props) { }), ctx.recipeId, superTokens.getRedirectToPathFromURL(), - userContext + userContext, + props.navigate ); }, [loginChallenge] diff --git a/lib/build/oauth2provider-shared.js b/lib/build/oauth2provider-shared.js index e0c40e5e5..03b573a62 100644 --- a/lib/build/oauth2provider-shared.js +++ b/lib/build/oauth2provider-shared.js @@ -92,10 +92,7 @@ var OAuth2Provider = /** @class */ (function (_super) { basePath = this.config.appInfo.apiBasePath.getAsStringDangerous(); return [ 2 /*return*/, - "" - .concat(domain) - .concat(basePath, "/oauth2provider/login?loginChallenge=") - .concat(ctx.loginChallenge), + "".concat(domain).concat(basePath, "/oauth/login?loginChallenge=").concat(ctx.loginChallenge), ]; } else { throw new Error( diff --git a/lib/build/passwordlessprebuiltui.js b/lib/build/passwordlessprebuiltui.js index e788b3bbd..b58501178 100644 --- a/lib/build/passwordlessprebuiltui.js +++ b/lib/build/passwordlessprebuiltui.js @@ -1306,12 +1306,13 @@ function getModifiedRecipeImplementation$4(originalImpl, setError, rebuildAuthPa resendCode: function (input) { return superTokens.__awaiter(_this, void 0, void 0, function () { var res, loginAttemptInfo, timestamp; - return superTokens.__generator(this, function (_a) { - switch (_a.label) { + var _a; + return superTokens.__generator(this, function (_b) { + switch (_b.label) { case 0: return [4 /*yield*/, originalImpl.resendCode(input)]; case 1: - res = _a.sent(); + res = _b.sent(); if (!(res.status === "OK")) return [3 /*break*/, 5]; return [ 4 /*yield*/, @@ -1320,7 +1321,7 @@ function getModifiedRecipeImplementation$4(originalImpl, setError, rebuildAuthPa }), ]; case 2: - loginAttemptInfo = _a.sent(); + loginAttemptInfo = _b.sent(); if (!(loginAttemptInfo !== undefined)) return [3 /*break*/, 4]; timestamp = Date.now(); return [ @@ -1328,13 +1329,17 @@ function getModifiedRecipeImplementation$4(originalImpl, setError, rebuildAuthPa originalImpl.setLoginAttemptInfo({ userContext: input.userContext, attemptInfo: superTokens.__assign(superTokens.__assign({}, loginAttemptInfo), { + tryLinkingWithSessionUser: + (_a = loginAttemptInfo.tryLinkingWithSessionUser) !== null && _a !== void 0 + ? _a + : false, lastResend: timestamp, }), }), ]; case 3: - _a.sent(); - _a.label = 4; + _b.sent(); + _b.label = 4; case 4: return [3 /*break*/, 7]; case 5: @@ -1346,10 +1351,10 @@ function getModifiedRecipeImplementation$4(originalImpl, setError, rebuildAuthPa }), ]; case 6: - _a.sent(); + _b.sent(); setError("ERROR_SIGN_IN_UP_RESEND_RESTART_FLOW"); rebuildAuthPage(); - _a.label = 7; + _b.label = 7; case 7: return [2 /*return*/, res]; } @@ -1529,6 +1534,7 @@ var EmailForm = uiEntry.withOverride("PasswordlessEmailForm", function Passwordl 4 /*yield*/, props.recipeImplementation.createCode({ email: email, + // tryLinkingWithSessionUser is set by the fn override userContext: userContext, }), ]; @@ -3689,6 +3695,7 @@ var PhoneForm = uiEntry.withOverride("PasswordlessPhoneForm", function Passwordl 4 /*yield*/, props.recipeImplementation.createCode({ phoneNumber: phoneNumber, + // tryLinkingWithSessionUser is set by the fn override userContext: userContext, }), ]; @@ -4768,6 +4775,7 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { 4 /*yield*/, recipeImplementation.createCode( superTokens.__assign(superTokens.__assign({}, createCodeInfo), { + tryLinkingWithSessionUser: true, userContext: userContext, }) ), @@ -4906,6 +4914,7 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { 4 /*yield*/, originalImpl.createCode( superTokens.__assign(superTokens.__assign({}, input), { + tryLinkingWithSessionUser: true, userContext: superTokens.__assign(superTokens.__assign({}, input.userContext), { additionalAttemptInfo: additionalAttemptInfo, }), @@ -4934,12 +4943,13 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { resendCode: function (input) { return superTokens.__awaiter(_this, void 0, void 0, function () { var res, loginAttemptInfo, timestamp; - return superTokens.__generator(this, function (_a) { - switch (_a.label) { + var _a; + return superTokens.__generator(this, function (_b) { + switch (_b.label) { case 0: return [4 /*yield*/, originalImpl.resendCode(input)]; case 1: - res = _a.sent(); + res = _b.sent(); if (!(res.status === "OK")) return [3 /*break*/, 5]; return [ 4 /*yield*/, @@ -4948,7 +4958,7 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { }), ]; case 2: - loginAttemptInfo = _a.sent(); + loginAttemptInfo = _b.sent(); if (!(loginAttemptInfo !== undefined)) return [3 /*break*/, 4]; timestamp = Date.now(); return [ @@ -4956,14 +4966,18 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { originalImpl.setLoginAttemptInfo({ userContext: input.userContext, attemptInfo: superTokens.__assign(superTokens.__assign({}, loginAttemptInfo), { + tryLinkingWithSessionUser: + (_a = loginAttemptInfo.tryLinkingWithSessionUser) !== null && _a !== void 0 + ? _a + : true, lastResend: timestamp, }), }), ]; case 3: - _a.sent(); + _b.sent(); dispatch({ type: "resendCode", timestamp: timestamp }); - _a.label = 4; + _b.label = 4; case 4: return [3 /*break*/, 7]; case 5: @@ -4975,9 +4989,9 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { }), ]; case 6: - _a.sent(); + _b.sent(); dispatch({ type: "restartFlow", error: "ERROR_SIGN_IN_UP_RESEND_RESTART_FLOW" }); - _a.label = 7; + _b.label = 7; case 7: return [2 /*return*/, res]; } @@ -5213,6 +5227,7 @@ var EmailOrPhoneForm = uiEntry.withOverride( 4 /*yield*/, props.recipeImplementation.createCode( superTokens.__assign(superTokens.__assign({}, contactInfo), { + // tryLinkingWithSessionUser is set by the fn override userContext: userContext, }) ), @@ -5521,6 +5536,7 @@ function getModifiedRecipeImplementation$2(originalImpl, config, rebuildAuthPage 4 /*yield*/, originalImpl.createCode( superTokens.__assign(superTokens.__assign({}, input), { + tryLinkingWithSessionUser: false, userContext: superTokens.__assign(superTokens.__assign({}, input.userContext), { additionalAttemptInfo: additionalAttemptInfo, }), @@ -6034,6 +6050,7 @@ function useChildProps$1( 4 /*yield*/, recipeImplementation.createCode({ phoneNumber: contactInfo, + tryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -6078,7 +6095,11 @@ function useChildProps$1( if (!pwlessExists.doesExist) return [3 /*break*/, 6]; return [ 4 /*yield*/, - recipeImplementation.createCode({ email: email, userContext: userContext }), + recipeImplementation.createCode({ + email: email, + tryLinkingWithSessionUser: false, + userContext: userContext, + }), ]; case 5: createRes = _b.sent(); @@ -6127,6 +6148,7 @@ function useChildProps$1( 4 /*yield*/, recipe$1.EmailPassword.getInstanceOrThrow().webJSRecipe.signIn({ formFields: formFields, + tryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -6161,6 +6183,7 @@ function useChildProps$1( 4 /*yield*/, recipeImplementation.createCode( superTokens.__assign(superTokens.__assign({}, createInfo), { + tryLinkingWithSessionUser: false, userContext: userContext, }) ), @@ -6374,6 +6397,7 @@ function getModifiedRecipeImplementation$1(originalImpl, config, rebuildAuthPage 4 /*yield*/, originalImpl.createCode( superTokens.__assign(superTokens.__assign({}, input), { + tryLinkingWithSessionUser: false, userContext: superTokens.__assign(superTokens.__assign({}, input.userContext), { additionalAttemptInfo: additionalAttemptInfo, }), @@ -6569,12 +6593,13 @@ function getModifiedRecipeImplementation(originalImpl, setError, rebuildAuthPage resendCode: function (input) { return superTokens.__awaiter(_this, void 0, void 0, function () { var res, loginAttemptInfo, timestamp; - return superTokens.__generator(this, function (_a) { - switch (_a.label) { + var _a; + return superTokens.__generator(this, function (_b) { + switch (_b.label) { case 0: return [4 /*yield*/, originalImpl.resendCode(input)]; case 1: - res = _a.sent(); + res = _b.sent(); if (!(res.status === "OK")) return [3 /*break*/, 5]; return [ 4 /*yield*/, @@ -6583,7 +6608,7 @@ function getModifiedRecipeImplementation(originalImpl, setError, rebuildAuthPage }), ]; case 2: - loginAttemptInfo = _a.sent(); + loginAttemptInfo = _b.sent(); if (!(loginAttemptInfo !== undefined)) return [3 /*break*/, 4]; timestamp = Date.now(); return [ @@ -6591,13 +6616,17 @@ function getModifiedRecipeImplementation(originalImpl, setError, rebuildAuthPage originalImpl.setLoginAttemptInfo({ userContext: input.userContext, attemptInfo: superTokens.__assign(superTokens.__assign({}, loginAttemptInfo), { + tryLinkingWithSessionUser: + (_a = loginAttemptInfo.tryLinkingWithSessionUser) !== null && _a !== void 0 + ? _a + : false, lastResend: timestamp, }), }), ]; case 3: - _a.sent(); - _a.label = 4; + _b.sent(); + _b.label = 4; case 4: return [3 /*break*/, 7]; case 5: @@ -6609,10 +6638,10 @@ function getModifiedRecipeImplementation(originalImpl, setError, rebuildAuthPage }), ]; case 6: - _a.sent(); + _b.sent(); setError("ERROR_SIGN_IN_UP_RESEND_RESTART_FLOW"); rebuildAuthPage(); - _a.label = 7; + _b.label = 7; case 7: return [2 /*return*/, res]; } diff --git a/lib/build/recipe/emailpassword/index.d.ts b/lib/build/recipe/emailpassword/index.d.ts index f5dbca606..089a44d92 100644 --- a/lib/build/recipe/emailpassword/index.d.ts +++ b/lib/build/recipe/emailpassword/index.d.ts @@ -66,6 +66,7 @@ export default class Wrapper { id: string; value: string; }[]; + tryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< @@ -93,6 +94,7 @@ export default class Wrapper { id: string; value: string; }[]; + tryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< diff --git a/lib/build/recipe/passwordless/index.d.ts b/lib/build/recipe/passwordless/index.d.ts index 0bf17133d..5f851efbe 100644 --- a/lib/build/recipe/passwordless/index.d.ts +++ b/lib/build/recipe/passwordless/index.d.ts @@ -20,11 +20,13 @@ export default class Wrapper { input: | { email: string; + tryLinkingWithSessionUser?: boolean; userContext?: UserContext; options?: RecipeFunctionOptions; } | { phoneNumber: string; + tryLinkingWithSessionUser?: boolean; userContext?: UserContext; options?: RecipeFunctionOptions; } diff --git a/lib/build/recipe/session/recipe.d.ts b/lib/build/recipe/session/recipe.d.ts index 1077959b6..4925a3630 100644 --- a/lib/build/recipe/session/recipe.d.ts +++ b/lib/build/recipe/session/recipe.d.ts @@ -58,9 +58,9 @@ export default class Session extends RecipeModule Promise; /** * This should only get called if validateGlobalClaimsAndHandleSuccessRedirection couldn't get a redirectInfo diff --git a/lib/build/recipe/thirdparty/index.d.ts b/lib/build/recipe/thirdparty/index.d.ts index 6b8fb2194..9978aeb1f 100644 --- a/lib/build/recipe/thirdparty/index.d.ts +++ b/lib/build/recipe/thirdparty/index.d.ts @@ -41,6 +41,7 @@ export default class Wrapper { thirdPartyId: string; frontendRedirectURI: string; redirectURIOnProviderDashboard?: string; + tryLinkingWithSessionUser?: boolean; userContext?: UserContext; options?: RecipeFunctionOptions; }): Promise; diff --git a/lib/build/thirdparty-shared.js b/lib/build/thirdparty-shared.js index 6be7a734c..626dfd0fe 100644 --- a/lib/build/thirdparty-shared.js +++ b/lib/build/thirdparty-shared.js @@ -1299,6 +1299,7 @@ function redirectToThirdPartyLogin(input) { thirdPartyId: input.thirdPartyId, frontendRedirectURI: provider.getRedirectURL(), redirectURIOnProviderDashboard: provider.getRedirectURIOnProviderDashboard(), + tryLinkingWithSessionUser: false, userContext: input.userContext, }), ]; diff --git a/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx b/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx index de23b96b3..66fa995f1 100644 --- a/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx +++ b/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx @@ -105,6 +105,7 @@ const AuthPageInner: React.FC = (props) => { const showStringFromQSRef = useRef(showStringFromQS); const errorFromQSRef = useRef(errorFromQS); const loginChallenge = search.get("loginChallenge"); + const forceFreshAuth = search.get("forceFreshAuth") === "true"; const sessionContext = useSessionContext(); const userContext = useUserContext(); @@ -191,7 +192,7 @@ const AuthPageInner: React.FC = (props) => { if (sessionContext.doesSessionExist) { if (props.onSessionAlreadyExists !== undefined) { props.onSessionAlreadyExists(); - } else if (props.redirectOnSessionExists !== false) { + } else if (props.redirectOnSessionExists !== false && !forceFreshAuth) { Session.getInstanceOrThrow().config.onHandleEvent({ action: "SESSION_ALREADY_EXISTS", }); @@ -298,7 +299,8 @@ const AuthPageInner: React.FC = (props) => { }, ctx.recipeId, getRedirectToPathFromURL(), - userContext + userContext, + props.navigate ); }, [loginChallenge] diff --git a/lib/ts/recipe/emailpassword/components/features/signin/index.tsx b/lib/ts/recipe/emailpassword/components/features/signin/index.tsx index 0ffe5ab2d..dd580735f 100644 --- a/lib/ts/recipe/emailpassword/components/features/signin/index.tsx +++ b/lib/ts/recipe/emailpassword/components/features/signin/index.tsx @@ -182,5 +182,13 @@ export default SignInFeature; const getModifiedRecipeImplementation = (origImpl: RecipeInterface): RecipeInterface => { return { ...origImpl, + signIn: async function (input) { + const response = await origImpl.signIn({ ...input, tryLinkingWithSessionUser: false }); + return response; + }, + signUp: async function (input) { + const response = await origImpl.signUp({ ...input, tryLinkingWithSessionUser: false }); + return response; + }, }; }; diff --git a/lib/ts/recipe/emailpassword/components/features/signup/index.tsx b/lib/ts/recipe/emailpassword/components/features/signup/index.tsx index 24b2e5152..46728ee42 100644 --- a/lib/ts/recipe/emailpassword/components/features/signup/index.tsx +++ b/lib/ts/recipe/emailpassword/components/features/signup/index.tsx @@ -166,6 +166,14 @@ export default SignUpFeature; const getModifiedRecipeImplementation = (origImpl: RecipeInterface): RecipeInterface => { return { ...origImpl, + signIn: async function (input) { + const response = await origImpl.signIn({ ...input, tryLinkingWithSessionUser: false }); + return response; + }, + signUp: async function (input) { + const response = await origImpl.signUp({ ...input, tryLinkingWithSessionUser: false }); + return response; + }, }; }; diff --git a/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx b/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx index 0c7817f71..7e57d8aef 100644 --- a/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx +++ b/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx @@ -58,6 +58,7 @@ export const SignInForm = withOverride( const response = await props.recipeImplementation.signIn({ formFields, + tryLinkingWithSessionUser: false, userContext, }); if (response.status === "WRONG_CREDENTIALS_ERROR") { diff --git a/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx b/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx index f0388629b..b0c76e350 100644 --- a/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx +++ b/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx @@ -58,6 +58,7 @@ export const SignUpForm = withOverride( const res = await props.recipeImplementation.signUp({ formFields, + tryLinkingWithSessionUser: false, userContext, }); diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts index e601cf2ba..98d170cf5 100644 --- a/lib/ts/recipe/emailpassword/index.ts +++ b/lib/ts/recipe/emailpassword/index.ts @@ -100,6 +100,7 @@ export default class Wrapper { id: string; value: string; }[]; + tryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< @@ -133,6 +134,7 @@ export default class Wrapper { id: string; value: string; }[]; + tryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts index f59a09d1e..d88503db1 100644 --- a/lib/ts/recipe/oauth2provider/recipe.ts +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -111,7 +111,7 @@ export default class OAuth2Provider extends RecipeModule< const domain = this.config.appInfo.apiDomain.getAsStringDangerous(); const basePath = this.config.appInfo.apiBasePath.getAsStringDangerous(); - return `${domain}${basePath}/oauth2provider/login?loginChallenge=${ctx.loginChallenge}`; + return `${domain}${basePath}/oauth/login?loginChallenge=${ctx.loginChallenge}`; } else { throw new Error("Should never come here: unknown action in OAuth2Provider.getDefaultRedirectionURL"); } diff --git a/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx b/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx index a6ec99e49..d31beed4a 100644 --- a/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx @@ -204,6 +204,7 @@ function getModifiedRecipeImplementation( userContext: input.userContext, attemptInfo: { ...loginAttemptInfo, + tryLinkingWithSessionUser: loginAttemptInfo.tryLinkingWithSessionUser ?? false, lastResend: timestamp, }, }); diff --git a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx index 7fd0332cf..c80e9d4c5 100644 --- a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx @@ -357,6 +357,7 @@ function useOnLoad( // createCode also dispatches the event that marks this page fully loaded createResp = await recipeImplementation!.createCode({ ...createCodeInfo, + tryLinkingWithSessionUser: true, userContext, }); } catch (err: any) { @@ -463,6 +464,7 @@ function getModifiedRecipeImplementation( const res = await originalImpl.createCode({ ...input, + tryLinkingWithSessionUser: true, userContext: { ...input.userContext, additionalAttemptInfo }, }); @@ -493,6 +495,7 @@ function getModifiedRecipeImplementation( userContext: input.userContext, attemptInfo: { ...loginAttemptInfo, + tryLinkingWithSessionUser: loginAttemptInfo.tryLinkingWithSessionUser ?? true, lastResend: timestamp, }, }); diff --git a/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx b/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx index a825d2d32..34c440ff6 100644 --- a/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx @@ -208,6 +208,7 @@ function getModifiedRecipeImplementation( const res = await originalImpl.createCode({ ...input, + tryLinkingWithSessionUser: false, userContext: { ...input.userContext, additionalAttemptInfo }, }); if (res.status === "OK") { diff --git a/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx b/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx index 96a6a3852..adc9e243c 100644 --- a/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx @@ -76,7 +76,11 @@ export function useChildProps( showContinueWithPasswordlessLink, onContactInfoSubmit: async (contactInfo: string) => { if (isPhoneNumber) { - const createRes = await recipeImplementation.createCode({ phoneNumber: contactInfo, userContext }); + const createRes = await recipeImplementation.createCode({ + phoneNumber: contactInfo, + tryLinkingWithSessionUser: false, + userContext, + }); if (createRes.status === "SIGN_IN_UP_NOT_ALLOWED") { throw new STGeneralError(createRes.reason); @@ -105,7 +109,11 @@ export function useChildProps( return { status: "OK" }; } else if (pwlessExists.doesExist) { // only pwless exists - const createRes = await recipeImplementation.createCode({ email, userContext }); + const createRes = await recipeImplementation.createCode({ + email, + tryLinkingWithSessionUser: false, + userContext, + }); if (createRes.status === "SIGN_IN_UP_NOT_ALLOWED") { throw new STGeneralError(createRes.reason); @@ -134,6 +142,7 @@ export function useChildProps( const response = await EmailPassword.getInstanceOrThrow().webJSRecipe.signIn({ formFields, + tryLinkingWithSessionUser: false, userContext, }); if (response.status === "WRONG_CREDENTIALS_ERROR") { @@ -147,7 +156,11 @@ export function useChildProps( onContinueWithPasswordlessClick: async (contactInfo) => { // When this function is called, the contactInfo has already been validated const createInfo = isPhoneNumber ? { phoneNumber: contactInfo } : { email: contactInfo }; - const createRes = await recipeImplementation.createCode({ ...createInfo, userContext }); + const createRes = await recipeImplementation.createCode({ + ...createInfo, + tryLinkingWithSessionUser: false, + userContext, + }); if (createRes.status !== "OK") { onError(createRes.reason); } else { @@ -313,6 +326,7 @@ function getModifiedRecipeImplementation( const res = await originalImpl.createCode({ ...input, + tryLinkingWithSessionUser: false, userContext: { ...input.userContext, additionalAttemptInfo }, }); if (res.status === "OK") { diff --git a/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx b/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx index 7141d7d13..bcdeff3b2 100644 --- a/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx @@ -199,6 +199,7 @@ function getModifiedRecipeImplementation( userContext: input.userContext, attemptInfo: { ...loginAttemptInfo, + tryLinkingWithSessionUser: loginAttemptInfo.tryLinkingWithSessionUser ?? false, lastResend: timestamp, }, }); diff --git a/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx b/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx index 902924b0c..8eb3b1911 100644 --- a/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx +++ b/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx @@ -62,6 +62,7 @@ export const EmailForm = withOverride( const response = await props.recipeImplementation.createCode({ email, + // tryLinkingWithSessionUser is set by the fn override userContext, }); diff --git a/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx b/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx index 795bf7faa..2b4b609fb 100644 --- a/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx +++ b/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx @@ -137,6 +137,7 @@ export const EmailOrPhoneForm = withOverride( const response = await props.recipeImplementation.createCode({ ...contactInfo, + // tryLinkingWithSessionUser is set by the fn override userContext, }); diff --git a/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx b/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx index 0fe9efe43..58d84f969 100644 --- a/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx +++ b/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx @@ -79,6 +79,7 @@ export const PhoneForm = withOverride( const response = await props.recipeImplementation.createCode({ phoneNumber, + // tryLinkingWithSessionUser is set by the fn override userContext, }); diff --git a/lib/ts/recipe/passwordless/index.ts b/lib/ts/recipe/passwordless/index.ts index b3276b8e0..0ce48c588 100644 --- a/lib/ts/recipe/passwordless/index.ts +++ b/lib/ts/recipe/passwordless/index.ts @@ -40,8 +40,18 @@ export default class Wrapper { static async createCode( input: - | { email: string; userContext?: UserContext; options?: RecipeFunctionOptions } - | { phoneNumber: string; userContext?: UserContext; options?: RecipeFunctionOptions } + | { + email: string; + tryLinkingWithSessionUser?: boolean; + userContext?: UserContext; + options?: RecipeFunctionOptions; + } + | { + phoneNumber: string; + tryLinkingWithSessionUser?: boolean; + userContext?: UserContext; + options?: RecipeFunctionOptions; + } ): Promise< | { status: "OK"; diff --git a/lib/ts/recipe/session/recipe.tsx b/lib/ts/recipe/session/recipe.tsx index e6dca7ac9..98ac0fb94 100644 --- a/lib/ts/recipe/session/recipe.tsx +++ b/lib/ts/recipe/session/recipe.tsx @@ -134,9 +134,9 @@ export default class Session extends RecipeModule => { userContext = getNormalisedUserContext(userContext); // First we check if there is an active session diff --git a/lib/ts/recipe/thirdparty/index.ts b/lib/ts/recipe/thirdparty/index.ts index 126364905..7f35750e9 100644 --- a/lib/ts/recipe/thirdparty/index.ts +++ b/lib/ts/recipe/thirdparty/index.ts @@ -86,6 +86,7 @@ export default class Wrapper { thirdPartyId: string; frontendRedirectURI: string; redirectURIOnProviderDashboard?: string; + tryLinkingWithSessionUser?: boolean; userContext?: UserContext; options?: RecipeFunctionOptions; }): Promise { diff --git a/lib/ts/recipe/thirdparty/utils.ts b/lib/ts/recipe/thirdparty/utils.ts index eceb9dd45..3b48b8883 100644 --- a/lib/ts/recipe/thirdparty/utils.ts +++ b/lib/ts/recipe/thirdparty/utils.ts @@ -152,6 +152,7 @@ export async function redirectToThirdPartyLogin(input: { thirdPartyId: input.thirdPartyId, frontendRedirectURI: provider.getRedirectURL(), redirectURIOnProviderDashboard: provider.getRedirectURIOnProviderDashboard(), + tryLinkingWithSessionUser: false, userContext: input.userContext, }); diff --git a/test/end-to-end/emailverification.test.js b/test/end-to-end/emailverification.test.js index 585d9d08e..435f29f27 100644 --- a/test/end-to-end/emailverification.test.js +++ b/test/end-to-end/emailverification.test.js @@ -104,6 +104,7 @@ describe("SuperTokens Email Verification", function () { consoleLogs = []; page.on("console", (consoleObj) => { const log = consoleObj.text(); + // console.log(log); if (log.startsWith("ST_LOGS")) { consoleLogs.push(log); } diff --git a/test/end-to-end/generalerror.test.js b/test/end-to-end/generalerror.test.js index 10160aebd..b29d76fa3 100644 --- a/test/end-to-end/generalerror.test.js +++ b/test/end-to-end/generalerror.test.js @@ -175,7 +175,7 @@ describe("General error rendering", function () { { name: "name", value: "John Doe" }, { name: "age", value: "20" }, ], - '{"formFields":[{"id":"email","value":"john.doe2@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}', + '{"formFields":[{"id":"email","value":"john.doe2@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}],"tryLinkingWithSessionUser":false}', rid ); diff --git a/test/end-to-end/signin-rrdv6.test.js b/test/end-to-end/signin-rrdv6.test.js index ab595eba7..53dbcdc84 100644 --- a/test/end-to-end/signin-rrdv6.test.js +++ b/test/end-to-end/signin-rrdv6.test.js @@ -186,7 +186,7 @@ describe("SuperTokens SignIn with react router dom v6", function () { assert.strictEqual(request.headers().rid, "emailpassword"); assert.strictEqual( request.postData(), - '{"formFields":[{"id":"email","value":"john@gmail.com"},{"id":"password","value":"********"}]}' + '{"formFields":[{"id":"email","value":"john@gmail.com"},{"id":"password","value":"********"}],"tryLinkingWithSessionUser":false}' ); assert.strictEqual(response.status, "WRONG_CREDENTIALS_ERROR"); @@ -258,7 +258,7 @@ describe("SuperTokens SignIn with react router dom v6", function () { assert.strictEqual(request.headers().rid, "emailpassword"); assert.strictEqual( request.postData(), - '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}]}' + '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}],"tryLinkingWithSessionUser":false}' ); assert.strictEqual(response.status, "OK"); @@ -403,7 +403,7 @@ describe("SuperTokens SignIn with react router dom v6", function () { assert.strictEqual(request.headers().rid, "emailpassword"); assert.strictEqual( request.postData(), - '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}]}' + '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}],"tryLinkingWithSessionUser":false}' ); assert.strictEqual(response.status, "OK"); diff --git a/test/end-to-end/signin.test.js b/test/end-to-end/signin.test.js index 877cfe70b..876fd83bd 100644 --- a/test/end-to-end/signin.test.js +++ b/test/end-to-end/signin.test.js @@ -183,7 +183,7 @@ describe("SuperTokens SignIn", function () { assert.strictEqual(request.headers().rid, "emailpassword"); assert.strictEqual( request.postData(), - '{"formFields":[{"id":"email","value":"john@gmail.com"},{"id":"password","value":"********"}]}' + '{"formFields":[{"id":"email","value":"john@gmail.com"},{"id":"password","value":"********"}],"tryLinkingWithSessionUser":false}' ); assert.strictEqual(response.status, "WRONG_CREDENTIALS_ERROR"); @@ -270,7 +270,7 @@ describe("SuperTokens SignIn", function () { assert.strictEqual(request.headers().rid, "emailpassword"); assert.strictEqual( request.postData(), - '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}]}' + '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}],"tryLinkingWithSessionUser":false}' ); assert.strictEqual(response.status, "OK"); @@ -415,7 +415,7 @@ describe("SuperTokens SignIn", function () { assert.strictEqual(request.headers().rid, "emailpassword"); assert.strictEqual( request.postData(), - '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}]}' + '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"}],"tryLinkingWithSessionUser":false}' ); assert.strictEqual(response.status, "OK"); diff --git a/test/end-to-end/thirdpartyemailpassword.test.js b/test/end-to-end/thirdpartyemailpassword.test.js index ae102d88d..d1b7f56fa 100644 --- a/test/end-to-end/thirdpartyemailpassword.test.js +++ b/test/end-to-end/thirdpartyemailpassword.test.js @@ -332,7 +332,7 @@ describe("SuperTokens Third Party Email Password", function () { { name: "name", value: "John Doe" }, { name: "age", value: "20" }, ], - '{"formFields":[{"id":"email","value":"bradparishdoh@gmail.com"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}', + '{"formFields":[{"id":"email","value":"bradparishdoh@gmail.com"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}],"tryLinkingWithSessionUser":false}', "thirdpartyemailpassword" ); await waitForUrl(page, "/dashboard"); diff --git a/test/helpers.js b/test/helpers.js index bee2f2a27..efe97ed4d 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -649,7 +649,7 @@ export async function defaultSignUp(page, rid = "emailpassword") { { name: "name", value: "John Doe" }, { name: "age", value: "20" }, ], - '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}', + '{"formFields":[{"id":"email","value":"john.doe@supertokens.io"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}],"tryLinkingWithSessionUser":false}', rid ); } @@ -666,7 +666,7 @@ export function getDefaultSignUpFieldValues({ { name: "name", value: name }, { name: "age", value: age }, ]; - const postValues = `{"formFields":[{"id":"email","value":"${email}"},{"id":"password","value":"${password}"},{"id":"name","value":"${name}"},{"id":"age","value":"${age}"},{"id":"country","value":""}]}`; + const postValues = `{"formFields":[{"id":"email","value":"${email}"},{"id":"password","value":"${password}"},{"id":"name","value":"${name}"},{"id":"age","value":"${age}"},{"id":"country","value":""}],"tryLinkingWithSessionUser":false}`; return { fieldValues, postValues }; } diff --git a/test/server/index.js b/test/server/index.js index a08be9784..f76e6c3a1 100644 --- a/test/server/index.js +++ b/test/server/index.js @@ -389,10 +389,22 @@ app.get("/token", async (_, res) => { app.post("/setupTenant", async (req, res) => { const { tenantId, loginMethods, coreConfig } = req.body; + const firstFactors = []; + + if (loginMethods.emailPassword?.enabled === true) { + firstFactors.push("emailpassword"); + } + if (loginMethods.thirdParty?.enabled === true) { + firstFactors.push("thirdparty"); + } + if (loginMethods.passwordless?.enabled === true) { + firstFactors.push("otp-phone"); + firstFactors.push("otp-email"); + firstFactors.push("link-phone"); + firstFactors.push("link-email"); + } let coreResp = await Multitenancy.createOrUpdateTenant(tenantId, { - emailPasswordEnabled: loginMethods.emailPassword?.enabled === true, - thirdPartyEnabled: loginMethods.thirdParty?.enabled === true, - passwordlessEnabled: loginMethods.passwordless?.enabled === true, + firstFactors, coreConfig, }); diff --git a/test/server/utils.js b/test/server/utils.js index 5e7bb8c8a..b568ff9ca 100644 --- a/test/server/utils.js +++ b/test/server/utils.js @@ -180,9 +180,7 @@ module.exports.startST = async function (config = {}) { }, body: JSON.stringify({ appId, - emailPasswordEnabled: true, - thirdPartyEnabled: true, - passwordlessEnabled: true, + firstFactors: ["emailpassword", "thirdparty", "passwordless"], coreConfig: config.coreConfig, }), }); From 7abc76494e3c2b2e987ff488e1079f25f92964ba Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Sun, 4 Aug 2024 22:12:01 +0200 Subject: [PATCH 4/4] test: add explanation comment to oauth2 tests --- test/end-to-end/oauth2provider.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/end-to-end/oauth2provider.test.js b/test/end-to-end/oauth2provider.test.js index 154a2c1b5..0165ce80b 100644 --- a/test/end-to-end/oauth2provider.test.js +++ b/test/end-to-end/oauth2provider.test.js @@ -41,6 +41,10 @@ import fetch from "isomorphic-fetch"; import { TEST_CLIENT_BASE_URL, TEST_SERVER_BASE_URL, SIGN_OUT_API } from "../constants"; +// We do no thave to use a separate domain for the oauth2 client, since the way we are testing +// the lib doesn't interact with the supertokens session handling. +// Using a redirection uri that has the same domain as the auth portal shouldn't affect the test. + /* * Tests. */