Skip to content

Commit

Permalink
fix: preserve query params when redirectBack is used (#779)
Browse files Browse the repository at this point in the history
* Preserve query params when redirectBack is used

* PR changes

* Update tests
  • Loading branch information
anku255 authored Dec 21, 2023
1 parent 51976b8 commit 5caaa0d
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 103 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [0.36.1] - 2023-12-20

### Fixes

- Previously, when calling `redirectToAuth` with the `redirectBack` option, query parameters were stripped when redirecting back to the previous page after authentication. This issue has been fixed, and now query parameters are preserved as intended.

## [0.36.0] - 2023-12-07

### Changes
Expand Down
23 changes: 20 additions & 3 deletions lib/build/genericComponentOverrideContext.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/build/utils.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions lib/ts/superTokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { saveCurrentLanguage, TranslationController } from "./translation/transl
import {
appendQueryParamsToURL,
appendTrailingSlashToURL,
getCurrentNormalisedUrlPath,
getCurrentNormalisedUrlPathWithQueryParams,
getDefaultCookieScope,
getOriginOfPage,
isTest,
Expand Down Expand Up @@ -193,7 +193,7 @@ export default class SuperTokens {
queryParams.show = options.show;
}
if (options.redirectBack === true) {
queryParams.redirectToPath = getCurrentNormalisedUrlPath().getAsStringDangerous();
queryParams.redirectToPath = getCurrentNormalisedUrlPathWithQueryParams();
}

let redirectUrl = await this.getRedirectUrl(
Expand Down
18 changes: 17 additions & 1 deletion lib/ts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,18 @@ export function getRedirectToPathFromURL(): string | undefined {
try {
const normalisedURLPath = new NormalisedURLPath(param).getAsStringDangerous();
const pathQueryParams = param.split("?")[1] !== undefined ? `?${param.split("?")[1]}` : "";
return normalisedURLPath + pathQueryParams;
const pathWithQueryParams = normalisedURLPath + pathQueryParams;

// Ensure a leading "/" if `normalisedUrlPath` is empty but `pathWithQueryParams` is not to ensure proper redirection.
// Example: "?test=1" will not redirect the user to `/?test=1` if we don't add a leading "/".
if (
normalisedURLPath.length === 0 &&
pathWithQueryParams.length > 0 &&
!pathWithQueryParams.startsWith("/")
) {
return "/" + pathWithQueryParams;
}
return pathWithQueryParams;
} catch {
return undefined;
}
Expand Down Expand Up @@ -189,6 +200,11 @@ export function getCurrentNormalisedUrlPath(): NormalisedURLPath {
return new NormalisedURLPath(WindowHandlerReference.getReferenceOrThrow().windowHandler.location.getPathName());
}

export function getCurrentNormalisedUrlPathWithQueryParams(): string {
const normalisedUrlPath = getCurrentNormalisedUrlPath().getAsStringDangerous();
return normalisedUrlPath + WindowHandlerReference.getReferenceOrThrow().windowHandler.location.getSearch();
}

export function appendQueryParamsToURL(stringUrl: string, queryParams?: Record<string, string>): string {
if (queryParams === undefined) {
return stringUrl;
Expand Down
2 changes: 1 addition & 1 deletion lib/ts/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
export const package_version = "0.36.0";
export const package_version = "0.36.1";
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supertokens-auth-react",
"version": "0.36.0",
"version": "0.36.1",
"description": "ReactJS SDK that provides login functionality with SuperTokens.",
"main": "./index.js",
"engines": {
Expand Down
132 changes: 54 additions & 78 deletions test/end-to-end/emailverification.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import {
setGeneralErrorToLocalStorage,
isAccountLinkingSupported,
backendBeforeEach,
getDefaultSignUpFieldValues,
getTestEmail,
} from "../helpers";

describe("SuperTokens Email Verification", function () {
Expand Down Expand Up @@ -114,14 +116,11 @@ describe("SuperTokens Email Verification", function () {
page.waitForNavigation({ waitUntil: "networkidle0" }),
]);
await toggleSignInSignUp(page);
const email = `john.doe${Date.now()}@supertokens.io`;
await signUp(page, [
{ name: "email", value: email },
{ name: "password", value: "Str0ngP@ssw0rd" },
{ name: "name", value: "John Doe" },
{ name: "age", value: "20" },
{ name: "country", value: "" },
]);

const email = getTestEmail();
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email });
await signUp(page, fieldValues, postValues, "emailpassword");

await waitForSTElement(page, "[data-supertokens~='sendVerifyEmailIcon']");

await fetch(`${TEST_APPLICATION_SERVER_BASE_URL}/deleteUser`, {
Expand Down Expand Up @@ -169,14 +168,10 @@ describe("SuperTokens Email Verification", function () {
page.waitForNavigation({ waitUntil: "networkidle0" }),
]);
await toggleSignInSignUp(page);
const email = `john.doe${Date.now()}@supertokens.io`;
await signUp(page, [
{ name: "email", value: email },
{ name: "password", value: "Str0ngP@ssw0rd" },
{ name: "name", value: "John Doe" },
{ name: "age", value: "20" },
{ name: "country", value: "" },
]);
const email = getTestEmail();
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email });
await signUp(page, fieldValues, postValues, "emailpassword");

await waitForSTElement(page, "[data-supertokens~='sendVerifyEmailIcon']");
let pathname = await page.evaluate(() => window.location.pathname);
assert.deepStrictEqual(pathname, "/auth/verify-email");
Expand Down Expand Up @@ -264,24 +259,15 @@ describe("SuperTokens Email Verification", function () {
]);
});

it("Should redirect to verify email screen on successful sign up when mode is REQUIRED and email is not verified and then post verification should redirect with original redirectPath and newUser", async function () {
it("Should redirect to verify email screen on successful sign up when mode is REQUIRED and email is not verified and then post verification should redirect with original redirectPath (w/ leading slash) and newUser", async function () {
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth?redirectToPath=%2Fredirect-here`),
page.goto(`${TEST_CLIENT_BASE_URL}/auth?redirectToPath=%2Fredirect-here%3Ffoo%3Dbar`),
page.waitForNavigation({ waitUntil: "networkidle0" }),
]);
await toggleSignInSignUp(page);
const rid = "emailpassword";
await signUp(
page,
[
{ name: "email", value: "[email protected]" },
{ name: "password", value: "Str0ngP@ssw0rd" },
{ name: "name", value: "John Doe" },
{ name: "age", value: "20" },
],
'{"formFields":[{"id":"email","value":"[email protected]"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}',
rid
);
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: "[email protected]" });
await signUp(page, fieldValues, postValues, "emailpassword");

let pathname = await page.evaluate(() => window.location.pathname);
assert.deepStrictEqual(pathname, "/auth/verify-email");

Expand All @@ -295,9 +281,36 @@ describe("SuperTokens Email Verification", function () {
// click on the continue button
await Promise.all([submitForm(page), page.waitForNavigation({ waitUntil: "networkidle0" })]);

// check that we are in /redirect-here
pathname = await page.evaluate(() => window.location.pathname);
assert.deepStrictEqual(pathname, "/redirect-here");
// check that we are in /redirect-here?foo=bar
const urlWithQP = await page.evaluate(() => window.location.pathname + window.location.search);
assert.deepStrictEqual(urlWithQP, "/redirect-here?foo=bar");
});

it("Should redirect to verify email screen on successful sign up when mode is REQUIRED and email is not verified and then post verification should redirect with original redirectPath (w/o leading slash) and newUser", async function () {
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth?redirectToPath=%3Ffoo%3Dbar`),
page.waitForNavigation({ waitUntil: "networkidle0" }),
]);
await toggleSignInSignUp(page);
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: getTestEmail() });
await signUp(page, fieldValues, postValues, "emailpassword");

let pathname = await page.evaluate(() => window.location.pathname);
assert.deepStrictEqual(pathname, "/auth/verify-email");

// we wait for email to be created
await new Promise((r) => setTimeout(r, 1000));

// we fetch the email verification link and go to that
const latestURLWithToken = await getLatestURLWithToken();
await Promise.all([page.waitForNavigation({ waitUntil: "networkidle0" }), page.goto(latestURLWithToken)]);

// click on the continue button
await Promise.all([submitForm(page), page.waitForNavigation({ waitUntil: "networkidle0" })]);

// check that we are in /?foo=bar
const urlWithQP = await page.evaluate(() => window.location.pathname + window.location.search);
assert.deepStrictEqual(urlWithQP, "/?foo=bar");
});

it("Should redirect to verify email screen on successful sign in when mode is REQUIRED and email is not verified", async function () {
Expand Down Expand Up @@ -451,17 +464,8 @@ describe("SuperTokens Email Verification", function () {

it('Should show "Email Verification successful" screen when token is valid with an active session', async function () {
await toggleSignInSignUp(page);
await signUp(
page,
[
{ name: "email", value: "[email protected]" },
{ name: "password", value: "Str0ngP@ssw0rd" },
{ name: "name", value: "John Doe" },
{ name: "age", value: "20" },
],
'{"formFields":[{"id":"email","value":"[email protected]"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}',
"emailpassword"
);
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: "[email protected]" });
await signUp(page, fieldValues, postValues, "emailpassword");

const latestURLWithToken = await getLatestURLWithToken();
await Promise.all([page.waitForNavigation({ waitUntil: "networkidle0" }), page.goto(latestURLWithToken)]);
Expand Down Expand Up @@ -518,17 +522,8 @@ describe("SuperTokens Email Verification", function () {
]);
await toggleSignInSignUp(page);

await signUp(
page,
[
{ name: "email", value: "[email protected]" },
{ name: "password", value: "Str0ngP@ssw0rd" },
{ name: "name", value: "John Doe" },
{ name: "age", value: "20" },
],
'{"formFields":[{"id":"email","value":"[email protected]"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}',
"emailpassword"
);
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: "[email protected]" });
await signUp(page, fieldValues, postValues, "emailpassword");

const latestURLWithToken = await getLatestURLWithToken();
await Promise.all([
Expand Down Expand Up @@ -708,17 +703,8 @@ describe("SuperTokens Email Verification general errors", function () {

it('Should show "General Error" when API returns "GENERAL_ERROR"', async function () {
await toggleSignInSignUp(page);
await signUp(
page,
[
{ name: "email", value: "[email protected]" },
{ name: "password", value: "Str0ngP@ssw0rd" },
{ name: "name", value: "John Doe" },
{ name: "age", value: "20" },
],
'{"formFields":[{"id":"email","value":"[email protected]"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}',
"emailpassword"
);
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: "[email protected]" });
await signUp(page, fieldValues, postValues, "emailpassword");

const latestURLWithToken = await getLatestURLWithToken();
await page.goto(latestURLWithToken);
Expand Down Expand Up @@ -903,18 +889,8 @@ describe("Email verification signOut errors", function () {
await toggleSignInSignUp(page);
await page.evaluate(() => localStorage.setItem("SHOW_GENERAL_ERROR", "SESSION SIGN_OUT"));

const rid = "emailpassword";
await signUp(
page,
[
{ name: "email", value: "[email protected]" },
{ name: "password", value: "Str0ngP@ssw0rd" },
{ name: "name", value: "John Doe" },
{ name: "age", value: "20" },
],
'{"formFields":[{"id":"email","value":"[email protected]"},{"id":"password","value":"Str0ngP@ssw0rd"},{"id":"name","value":"John Doe"},{"id":"age","value":"20"},{"id":"country","value":""}]}',
rid
);
const { fieldValues, postValues } = getDefaultSignUpFieldValues({ email: "[email protected]" });
await signUp(page, fieldValues, postValues, "emailpassword");

let pathname = await page.evaluate(() => window.location.pathname);
assert.deepStrictEqual(pathname, "/auth/verify-email");
Expand Down
26 changes: 25 additions & 1 deletion test/end-to-end/passwordless.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ export function getPasswordlessTestCases({ authRecipe, logId, generalErrorRecipe
]);
});

it("Successful signin w/ redirectToPath and email verification", async function () {
it("Successful signin w/ redirectToPath (w/ leading slash) and email verification", async function () {
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth?redirectToPath=%2Fredirect-here%3Ffoo%3Dbar&mode=REQUIRED`),
page.waitForNavigation({ waitUntil: "networkidle0" }),
Expand Down Expand Up @@ -781,6 +781,30 @@ export function getPasswordlessTestCases({ authRecipe, logId, generalErrorRecipe
]);
});

it("Successful signin w/ redirectPath (w/o leading slash) and email verification", async function () {
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth?redirectToPath=%3Ffoo%3Dbar&mode=REQUIRED`),
page.waitForNavigation({ waitUntil: "networkidle0" }),
]);

await setInputValues(page, [{ name: inputName, value: contactInfo }]);
await submitForm(page);

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

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

await page.waitForNavigation({ waitUntil: "networkidle0" });

const { pathname, search } = await page.evaluate(() => window.location);
assert.deepStrictEqual(pathname + search, "/?foo=bar");
});

it("Submitting empty id", async function () {
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth`),
Expand Down
Loading

0 comments on commit 5caaa0d

Please sign in to comment.