Skip to content

Commit

Permalink
fixes impl of getBackwardsCompatibleUserInfo (#749)
Browse files Browse the repository at this point in the history
* fixes impl of getBackwardsCompatibleUserInfo

* adds tests
  • Loading branch information
rishabhpoddar authored Dec 4, 2023
1 parent af38995 commit d97a1d7
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [16.6.2] - 2023-12-02

- Fixes implementation of `getBackwardsCompatibleUserInfo` to not throw an error in case of session and user id mismatch.

## [16.6.1] - 2023-11-29

- Removed dependency on the `crypto` library to enable Apple OAuth usage in Cloudflare Workers.
Expand Down
20 changes: 16 additions & 4 deletions lib/build/utils.js

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.

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

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

23 changes: 18 additions & 5 deletions lib/ts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { BaseRequest, BaseResponse } from "./framework";
import { logDebugMessage } from "./logger";
import { HEADER_FDI, HEADER_RID } from "./constants";
import crossFetch from "cross-fetch";
import { User } from "./user";
import { LoginMethod, User } from "./user";
import { SessionContainer } from "./recipe/session";

export const doFetch: typeof fetch = (...args) => {
Expand Down Expand Up @@ -182,17 +182,30 @@ export function getBackwardsCompatibleUserInfo(
}
return resp;
} else {
const loginMethod = result.user.loginMethods.find(
let loginMethod: undefined | LoginMethod = result.user.loginMethods.find(
(lm) => lm.recipeUserId.getAsString() === result.session.getRecipeUserId().getAsString()
);

if (loginMethod === undefined) {
throw new Error("This should never happen: session and user mismatch");
// we pick the oldest login method here for the user.
// this can happen in case the user is implementing something like
// MFA where the session remains the same during the second factor as well.
for (let i = 0; i < result.user.loginMethods.length; i++) {
if (loginMethod === undefined) {
loginMethod = result.user.loginMethods[i];
} else if (loginMethod.timeJoined > result.user.loginMethods[i].timeJoined) {
loginMethod = result.user.loginMethods[i];
}
}
}

if (loginMethod === undefined) {
throw new Error("This should never happen - user has no login methods");
}

const userObj: JSONObject = {
id: result.session.getUserId(),
timeJoined: result.user.timeJoined,
id: result.user.id, // we purposely use this instead of the loginmethod's recipeUserId because if the oldest login method is deleted, then this userID should remain the same.
timeJoined: loginMethod.timeJoined,
};
if (loginMethod.thirdParty) {
userObj.thirdParty = loginMethod.thirdParty;
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,7 +12,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
export const version = "16.6.1";
export const version = "16.6.2";

export const cdiSupported = ["4.0"];

Expand Down
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-node",
"version": "16.6.1",
"version": "16.6.2",
"description": "NodeJS driver for SuperTokens core",
"main": "index.js",
"scripts": {
Expand Down
209 changes: 209 additions & 0 deletions test/accountlinking/userstructure.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const {
resetAll,
assertJSONEquals,
startSTWithMultitenancyAndAccountLinking,
extractInfoFromResponse,
} = require("../utils");
let supertokens = require("../../");
let Session = require("../../recipe/session");
Expand All @@ -30,6 +31,10 @@ let { ProcessState } = require("../../lib/build/processState");
let EmailPassword = require("../../recipe/emailpassword");
let ThirdParty = require("../../recipe/thirdparty");
let Passwordless = require("../../recipe/passwordless");
let AccountLinking = require("../../recipe/accountlinking");
const express = require("express");
let { middleware, errorHandler } = require("../../framework/express");
const request = require("supertest");

describe(`accountlinkingTests: ${printPath("[test/accountlinking/userstructure.test.js]")}`, function () {
beforeEach(async function () {
Expand Down Expand Up @@ -209,4 +214,208 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/userstructure.t
assert(!user.loginMethods[0].hasSamePhoneNumberAs("06701234123"));
assert(!user.loginMethods[0].hasSamePhoneNumberAs("p36701234123"));
});

it("user structure FDI 1.17 is correctly returned even if session does not match logged in user", async function () {
let sessionReused = false;
const connectionURI = await startSTWithMultitenancyAndAccountLinking();
supertokens.init({
supertokens: {
connectionURI,
},
appInfo: {
apiDomain: "api.supertokens.io",
appName: "SuperTokens",
websiteDomain: "supertokens.io",
},
recipeList: [
EmailPassword.init({
override: {
apis: (oI) => {
return {
...oI,
signInPOST: async (input) => {
let session = await Session.getSession(input.options.req, input.options.res, {
sessionRequired: false,
});
if (session !== undefined) {
input.userContext.session = session;
}
let response = await oI.signInPOST(input);
return response;
},
signUpPOST: async (input) => {
let session = await Session.getSession(input.options.req, input.options.res, {
sessionRequired: false,
});
if (session !== undefined) {
input.userContext.session = session;
}
let response = await oI.signUpPOST(input);
return response;
},
};
},
},
}),
Session.init({
override: {
functions: (oI) => {
return {
...oI,
createNewSession: async (input) => {
if (input.userContext.session !== undefined) {
sessionReused = true;
return input.userContext.session;
}
return oI.createNewSession(input);
},
};
},
},
}),
],
});

const app = express();
app.use(middleware());
app.use(errorHandler());

let { user, status } = await EmailPassword.signUp("public", "[email protected]", "password123");
assert(status === "OK");

let res = await new Promise((resolve) =>
request(app)
.post("/auth/signin")
.set("fdi-version", "1.17")
.send({
formFields: [
{
id: "email",
value: "[email protected]",
},
{
id: "password",
value: "password123",
},
],
})
.expect(200)
.end((err, res) => {
if (err) {
resolve(undefined);
} else {
resolve(res);
}
})
);

assert(!sessionReused);
let tokens = extractInfoFromResponse(res);
assert(tokens.accessTokenFromAny !== undefined);

// now we sign up a new user with another email, but with the older session.
let signUp2 = await EmailPassword.signUp("public", "[email protected]", "password123");
assert(signUp2.status === "OK");
let linkingResult = await AccountLinking.createPrimaryUser(signUp2.user.loginMethods[0].recipeUserId);
assert(linkingResult.status === "OK");

let signUp3 = await EmailPassword.signUp("public", "[email protected]", "password123");
assert(signUp3.status === "OK");
linkingResult = await AccountLinking.linkAccounts(signUp3.user.loginMethods[0].recipeUserId, signUp2.user.id);
assert(linkingResult.status === "OK");

res = await new Promise((resolve) =>
request(app)
.post("/auth/signin")
.set("fdi-version", "1.17")
.set("Authorization", `Bearer ${tokens.accessTokenFromAny}`)
.send({
formFields: [
{
id: "email",
value: "[email protected]",
},
{
id: "password",
value: "password123",
},
],
})
.expect(200)
.end((err, res) => {
if (err) {
resolve(undefined);
} else {
resolve(res);
}
})
);

assert(sessionReused);
assert(res.body.status === "OK");
assert(res.body.user.email === "[email protected]");
assert(res.body.user.id === signUp2.user.id);
assert(res.body.user.timeJoined === signUp2.user.timeJoined);
assert(Object.keys(res.body.user).length === 3);
});

it("user structure FDI 1.17 is correctly returned based on session user ID", async function () {
const connectionURI = await startSTWithMultitenancyAndAccountLinking();
supertokens.init({
supertokens: {
connectionURI,
},
appInfo: {
apiDomain: "api.supertokens.io",
appName: "SuperTokens",
websiteDomain: "supertokens.io",
},
recipeList: [EmailPassword.init(), Session.init()],
});

const app = express();
app.use(middleware());
app.use(errorHandler());

let signUp2 = await EmailPassword.signUp("public", "[email protected]", "password123");
assert(signUp2.status === "OK");
let linkingResult = await AccountLinking.createPrimaryUser(signUp2.user.loginMethods[0].recipeUserId);
assert(linkingResult.status === "OK");

let signUp3 = await EmailPassword.signUp("public", "[email protected]", "password123");
assert(signUp3.status === "OK");
linkingResult = await AccountLinking.linkAccounts(signUp3.user.loginMethods[0].recipeUserId, signUp2.user.id);
assert(linkingResult.status === "OK");

res = await new Promise((resolve) =>
request(app)
.post("/auth/signin")
.set("fdi-version", "1.17")
.send({
formFields: [
{
id: "email",
value: "[email protected]",
},
{
id: "password",
value: "password123",
},
],
})
.expect(200)
.end((err, res) => {
if (err) {
resolve(undefined);
} else {
resolve(res);
}
})
);
assert(res.body.status === "OK");
assert(res.body.user.email === "[email protected]");
assert(res.body.user.id === signUp2.user.id);
assert(res.body.user.timeJoined === signUp3.user.timeJoined);
assert(Object.keys(res.body.user).length === 3);
});
});

0 comments on commit d97a1d7

Please sign in to comment.