Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: call isEmailChangeAllowed in pwless updateUser #875

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- the user has another email address or phone number associated with it
- Account linking based on emails now require the email to be verified in both users if `shouldRequireVerification` is set to `true` instead of only requiring it for the recipe user.
- The access token cookie expiry has been changed from 100 years to 1 year due to some browsers capping the maximum expiry at 400 days. No action is needed on your part.
- Recipe functions that update the email address of users now call `isEmailChangeAllowed` to check if the email update should be allowed or not.
- This only has an effect if account linking is turned on.
- This is aimed to help you avoid security issues.
- `isEmailChangeAllowed` is now called in functions:
- `updateUser` (Passwordless recipe)
- `updateEmailOrPassword` (EmailPassword recipe)
- `manuallyCreateOrUpdateUser` (ThirdParty recipe)

### Changes

Expand Down
33 changes: 33 additions & 0 deletions lib/build/recipe/passwordless/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var __importDefault =
};
Object.defineProperty(exports, "__esModule", { value: true });
const recipe_1 = __importDefault(require("../accountlinking/recipe"));
const recipe_2 = __importDefault(require("../emailverification/recipe"));
const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath"));
const logger_1 = require("../../logger");
const user_1 = require("../../user");
Expand Down Expand Up @@ -147,6 +148,38 @@ function getRecipeInterface(querier) {
return { status: "OK" };
},
updateUser: async function (input) {
const accountLinking = recipe_1.default.getInstance();
if (input.email) {
const user = await __1.getUser(input.recipeUserId.getAsString(), input.userContext);
if (user === undefined) {
return { status: "UNKNOWN_USER_ID_ERROR" };
}
const evInstance = recipe_2.default.getInstance();
let isEmailVerified = false;
if (evInstance) {
isEmailVerified = await evInstance.recipeInterfaceImpl.isEmailVerified({
recipeUserId: input.recipeUserId,
email: input.email,
userContext: input.userContext,
});
}
const isEmailChangeAllowed = await accountLinking.isEmailChangeAllowed({
user,
isVerified: isEmailVerified,
newEmail: input.email,
session: undefined,
userContext: input.userContext,
});
if (!isEmailChangeAllowed.allowed) {
return {
status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR",
reason:
isEmailChangeAllowed.reason === "ACCOUNT_TAKEOVER_RISK"
? "New email cannot be applied to existing account because of account takeover risks."
: "New email cannot be applied to existing account because of there is another primary user with the same email address.",
};
}
}
let response = await querier.sendPutRequest(
new normalisedURLPath_1.default(`/recipe/user`),
copyAndRemoveUserContextAndTenantId(input),
Expand Down
36 changes: 36 additions & 0 deletions lib/ts/recipe/passwordless/recipeImplementation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RecipeInterface } from "./types";
import { Querier } from "../../querier";
import AccountLinking from "../accountlinking/recipe";
import EmailVerification from "../emailverification/recipe";
import NormalisedURLPath from "../../normalisedURLPath";
import { logDebugMessage } from "../../logger";
import { User } from "../../user";
Expand Down Expand Up @@ -161,6 +162,41 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface {
return { status: "OK" };
},
updateUser: async function (input) {
const accountLinking = AccountLinking.getInstance();
if (input.email) {
const user = await getUser(input.recipeUserId.getAsString(), input.userContext);

if (user === undefined) {
return { status: "UNKNOWN_USER_ID_ERROR" };
}

const evInstance = EmailVerification.getInstance();

let isEmailVerified = false;
if (evInstance) {
isEmailVerified = await evInstance.recipeInterfaceImpl.isEmailVerified({
recipeUserId: input.recipeUserId,
email: input.email,
userContext: input.userContext,
});
}
const isEmailChangeAllowed = await accountLinking.isEmailChangeAllowed({
user,
isVerified: isEmailVerified,
newEmail: input.email,
session: undefined,
userContext: input.userContext,
});
if (!isEmailChangeAllowed.allowed) {
return {
status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR",
reason:
isEmailChangeAllowed.reason === "ACCOUNT_TAKEOVER_RISK"
? "New email cannot be applied to existing account because of account takeover risks."
: "New email cannot be applied to existing account because of there is another primary user with the same email address.",
};
}
}
let response = await querier.sendPutRequest(
new NormalisedURLPath(`/recipe/user`),
copyAndRemoveUserContextAndTenantId(input),
Expand Down
19 changes: 19 additions & 0 deletions test/test-server/src/passwordless.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Router } from "express";
import SuperTokens from "../../..";
import Passwordless from "../../../recipe/passwordless";
import { convertRequestSessionToSessionObject, serializeRecipeUserId, serializeUser } from "./utils";
import { logger } from "./logger";
Expand Down Expand Up @@ -67,6 +68,24 @@ const router = Router()
} catch (e) {
next(e);
}
})
.post("/updateuser", async (req, res, next) => {
try {
logDebugMessage("Passwordless:updateUser %j", req.body);
const response = await Passwordless.updateUser({
recipeUserId: SuperTokens.convertToRecipeUserId(req.body.recipeUserId),
email: req.body.email,
phoneNumber: req.body.phoneNumber,
userContext: req.body.userContext,
});
res.json({
...response,
...serializeUser(response),
...serializeRecipeUserId(response),
});
} catch (e) {
next(e);
}
});

export default router;
Loading