diff --git a/examples/for-tests/package.json b/examples/for-tests/package.json
index 9f67560d0..04323793d 100644
--- a/examples/for-tests/package.json
+++ b/examples/for-tests/package.json
@@ -4,8 +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-oidc-context": "^3.1.0",
"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..76ab8b130
--- /dev/null
+++ b/examples/for-tests/src/OAuth2Page.js
@@ -0,0 +1,52 @@
+import { AuthProvider, useAuth } from "react-oidc-context";
+import { getApiDomain, getWebsiteDomain } from "./config";
+
+// 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",
+ 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 { signinRedirect, signoutSilent, user, error } = useAuth();
+
+ return (
+
+
OAuth2 Login Test
+
+ {user ? (
+
+
{JSON.stringify(user.profile, null, 2)}
+
+
+ ) : (
+
+ {error &&
Error: {error.message}
}
+
+
+ )}
+
+
+ );
+}
+
+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/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/oauth2provider.test.js b/test/end-to-end/oauth2provider.test.js
new file mode 100644
index 000000000..0165ce80b
--- /dev/null
+++ b/test/end-to-end/oauth2provider.test.js
@@ -0,0 +1,197 @@
+/* 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";
+
+// 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.
+ */
+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();
+ }
+
+ 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 () {
+ if (skipped) {
+ return;
+ }
+ 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.aud, [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,
+ // 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);
+
+ 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.aud, [client.clientId]);
+
+ // 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.aud, [client.clientId]);
+
+ // Validate the token was refreshed
+ assert(tokenDataAfterLogin.iat !== tokenDataAfterRefresh.iat);
+ assert(tokenDataAfterLogin.exp < tokenDataAfterRefresh.exp);
+ });
+ });
+});
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 bb8b123e3..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 };
}
@@ -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,36 @@ 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();
+}
+
+// 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);
+}
+
+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..f76e6c3a1 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;
@@ -386,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,
});
@@ -489,6 +504,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 +552,7 @@ function initST() {
UserMetadataRaw.reset();
MultiFactorAuthRaw.reset();
TOTPRaw.reset();
+ OAuth2ProviderRaw.reset();
EmailVerificationRaw.reset();
EmailPasswordRaw.reset();
@@ -730,6 +751,7 @@ function initST() {
},
}),
],
+ ["oauth2provider", OAuth2Provider.init()],
];
passwordlessConfig = {
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,
}),
});