From e7ed8483b5445c0f3e58a52b29547ef3a698cac0 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 13:42:01 +0530 Subject: [PATCH 1/9] Add definition for password update API interface definition --- .../emailpassword/api/implementation.js | 72 ++++++++++++++- lib/build/recipe/emailpassword/types.d.ts | 7 ++ .../emailpassword/api/implementation.ts | 92 ++++++++++++++++++- lib/ts/recipe/emailpassword/types.ts | 4 + 4 files changed, 173 insertions(+), 2 deletions(-) diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 6eea377fe..3fefcd9cb 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -13,6 +13,7 @@ const recipeUserId_1 = __importDefault(require("../../../recipeUserId")); const utils_1 = require("../utils"); const authUtils_1 = require("../../../authUtils"); const utils_2 = require("../../thirdparty/utils"); +const error_1 = __importDefault(require("../../session/error")); function getAPIImplementation() { return { emailExistsGET: async function ({ email, tenantId, userContext }) { @@ -775,8 +776,77 @@ function getAPIImplementation() { user: postAuthChecks.user, }; }, - updatePasswordPOST: undefined, changeEmailPOST: undefined, + updatePasswordPOST: async function ({ newPassword, oldPassword, userContext, options, session, tenantId }) { + /** + * This function will update the user's password by verifying that + * they have provided the correct old password. + */ + // We need to find the recipe user ID for the emailpassword recipe so we will + // use the userId to get user's login methods and then extract the recipe user + // ID accordingly. + const existingUser = await __1.getUser(session.getUserId(), userContext); + if (existingUser === undefined) { + // Should never come here as we verified above that the user exists. + throw new Error("Should never come here as the user was checked to exist"); + } + const recipeUser = existingUser.loginMethods.find((lm) => lm.recipeId === "emailpassword"); + if (!recipeUser || recipeUser.email === undefined) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "A valid session is required to authenticate user for updating password", + }); + } + const email = recipeUser.email; + // User has provided us the required values so we can go ahead with the rest + // of the logic which is verifying the user's credentials (ie: older password) + const areUserCredentialsValid = async (tenantId) => { + return ( + ( + await options.recipeImplementation.verifyCredentials({ + email, + password: oldPassword, + tenantId, + userContext, + }) + ).status === "OK" + ); + }; + if (!areUserCredentialsValid) { + // Seems like user has provided an invalid password, cannot continue. + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + let updateResponse = await options.recipeImplementation.updateEmailOrPassword({ + tenantIdForPasswordPolicy: tenantId, + recipeUserId: recipeUser.recipeUserId, + password: newPassword, + userContext, + }); + if ( + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + throw new Error("This should never come here because we are not updating the email"); + } else if (updateResponse.status === "UNKNOWN_USER_ID_ERROR") { + // This should happen only cause of a race condition where the user + // might be deleted after user was checked to exist and before their update email + // call was made. + return { + status: "USER_DELETED_WHILE_IN_PROGRESS", + reason: "The user was deleted while the password update was in progress", + }; + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + return { + status: "PASSWORD_POLICY_VIOLATED_ERROR", + failureReason: updateResponse.failureReason, + }; + } + return { + status: "OK", + }; + }, }; } exports.default = getAPIImplementation; diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index 9757645b2..388e5cab9 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -9,6 +9,7 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import SessionError from "../session/error"; export declare type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; signInFeature: TypeNormalisedInputSignIn; @@ -330,6 +331,7 @@ export declare type APIInterface = { session: SessionContainerInterface; options: APIOptions; userContext: UserContext; + tenantId: string; }) => Promise< | { status: "OK"; @@ -341,6 +343,11 @@ export declare type APIInterface = { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string; } + | { + status: "USER_DELETED_WHILE_IN_PROGRESS"; + reason: string; + } + | SessionError | GeneralErrorResponse >); changeEmailPOST: diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index 1eb46e992..e1e5afd55 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -10,6 +10,7 @@ import { getPasswordResetLink } from "../utils"; import { AuthUtils } from "../../../authUtils"; import { isFakeEmail } from "../../thirdparty/utils"; import { SessionContainerInterface } from "../../session/types"; +import SessionError from "../../session/error"; export default function getAPIImplementation(): APIInterface { return { @@ -927,7 +928,96 @@ export default function getAPIImplementation(): APIInterface { }; }, - updatePasswordPOST: undefined, changeEmailPOST: undefined, + + updatePasswordPOST: async function ({ + newPassword, + oldPassword, + userContext, + options, + session, + tenantId, + }): Promise< + | { + status: "OK"; + } + | { status: "WRONG_CREDENTIALS_ERROR" } + | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } + | { status: "USER_DELETED_WHILE_IN_PROGRESS"; reason: string } + | GeneralErrorResponse + > { + /** + * This function will update the user's password by verifying that + * they have provided the correct old password. + */ + + // We need to find the recipe user ID for the emailpassword recipe so we will + // use the userId to get user's login methods and then extract the recipe user + // ID accordingly. + const existingUser = await getUser(session.getUserId(), userContext); + if (existingUser === undefined) { + // Should never come here as we verified above that the user exists. + throw new Error("Should never come here as the user was checked to exist"); + } + const recipeUser = existingUser.loginMethods.find((lm) => lm.recipeId === "emailpassword"); + if (!recipeUser || recipeUser.email === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "A valid session is required to authenticate user for updating password", + }); + } + const email = recipeUser.email; + + // User has provided us the required values so we can go ahead with the rest + // of the logic which is verifying the user's credentials (ie: older password) + const areUserCredentialsValid = async (tenantId: string) => { + return ( + ( + await options.recipeImplementation.verifyCredentials({ + email, + password: oldPassword, + tenantId, + userContext, + }) + ).status === "OK" + ); + }; + if (!areUserCredentialsValid) { + // Seems like user has provided an invalid password, cannot continue. + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + + let updateResponse = await options.recipeImplementation.updateEmailOrPassword({ + tenantIdForPasswordPolicy: tenantId, + recipeUserId: recipeUser.recipeUserId, + password: newPassword, + userContext, + }); + if ( + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + throw new Error("This should never come here because we are not updating the email"); + } else if (updateResponse.status === "UNKNOWN_USER_ID_ERROR") { + // This should happen only cause of a race condition where the user + // might be deleted after user was checked to exist and before their update email + // call was made. + return { + status: "USER_DELETED_WHILE_IN_PROGRESS", + reason: "The user was deleted while the password update was in progress", + }; + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + return { + status: "PASSWORD_POLICY_VIOLATED_ERROR", + failureReason: updateResponse.failureReason, + }; + } + + return { + status: "OK", + }; + }, }; } diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index fc780ea15..4ccdd5b54 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -23,6 +23,7 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import SessionError from "../session/error"; export type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; @@ -334,12 +335,15 @@ export type APIInterface = { session: SessionContainerInterface; options: APIOptions; userContext: UserContext; + tenantId: string; }) => Promise< | { status: "OK"; } | { status: "WRONG_CREDENTIALS_ERROR" } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } + | { status: "USER_DELETED_WHILE_IN_PROGRESS"; reason: string } + | SessionError | GeneralErrorResponse >); From bd417195944da288df688fe8326a6c6dd3680c64 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 14:00:51 +0530 Subject: [PATCH 2/9] Add support for update password handler definition --- .../emailpassword/api/updatePassword.d.ts | 12 ++++ .../emailpassword/api/updatePassword.js | 48 ++++++++++++++++ lib/build/recipe/emailpassword/constants.d.ts | 1 + lib/build/recipe/emailpassword/constants.js | 3 +- lib/build/recipe/emailpassword/recipe.js | 9 +++ .../emailpassword/api/updatePassword.ts | 55 +++++++++++++++++++ lib/ts/recipe/emailpassword/constants.ts | 2 + lib/ts/recipe/emailpassword/recipe.ts | 10 ++++ 8 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 lib/build/recipe/emailpassword/api/updatePassword.d.ts create mode 100644 lib/build/recipe/emailpassword/api/updatePassword.js create mode 100644 lib/ts/recipe/emailpassword/api/updatePassword.ts diff --git a/lib/build/recipe/emailpassword/api/updatePassword.d.ts b/lib/build/recipe/emailpassword/api/updatePassword.d.ts new file mode 100644 index 000000000..cd7d57a6f --- /dev/null +++ b/lib/build/recipe/emailpassword/api/updatePassword.d.ts @@ -0,0 +1,12 @@ +// @ts-nocheck +/** + * This file contains the top-level handler definition for password update + */ +import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; +export default function updatePassword( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/emailpassword/api/updatePassword.js b/lib/build/recipe/emailpassword/api/updatePassword.js new file mode 100644 index 000000000..eaefee19d --- /dev/null +++ b/lib/build/recipe/emailpassword/api/updatePassword.js @@ -0,0 +1,48 @@ +"use strict"; +/** + * This file contains the top-level handler definition for password update + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const error_1 = __importDefault(require("../error")); +const session_1 = __importDefault(require("../../session")); +async function updatePassword(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.updatePasswordPOST === undefined) { + return false; + } + const { newPassword, oldPassword } = await options.req.getJSONBody(); + if (newPassword === undefined) { + throw new error_1.default({ + message: "Missing required parameter 'newPassword'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (oldPassword === undefined) { + throw new error_1.default({ + message: "Missing required parameter 'oldPassword'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + const session = await session_1.default.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext + ); + let result = await apiImplementation.updatePasswordPOST({ + newPassword, + oldPassword, + session, + options, + userContext, + tenantId, + }); + utils_1.send200Response(options.res, result); + return true; +} +exports.default = updatePassword; diff --git a/lib/build/recipe/emailpassword/constants.d.ts b/lib/build/recipe/emailpassword/constants.d.ts index a0685c10f..3354e320c 100644 --- a/lib/build/recipe/emailpassword/constants.d.ts +++ b/lib/build/recipe/emailpassword/constants.d.ts @@ -5,5 +5,6 @@ export declare const SIGN_UP_API = "/signup"; export declare const SIGN_IN_API = "/signin"; export declare const GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; export declare const PASSWORD_RESET_API = "/user/password/reset"; +export declare const PASSWORD_UPDATE_API = "/user/password/update"; export declare const SIGNUP_EMAIL_EXISTS_API_OLD = "/signup/email/exists"; export declare const SIGNUP_EMAIL_EXISTS_API = "/emailpassword/email/exists"; diff --git a/lib/build/recipe/emailpassword/constants.js b/lib/build/recipe/emailpassword/constants.js index 7236bd766..425dcd7e4 100644 --- a/lib/build/recipe/emailpassword/constants.js +++ b/lib/build/recipe/emailpassword/constants.js @@ -14,12 +14,13 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.SIGNUP_EMAIL_EXISTS_API = exports.SIGNUP_EMAIL_EXISTS_API_OLD = exports.PASSWORD_RESET_API = exports.GENERATE_PASSWORD_RESET_TOKEN_API = exports.SIGN_IN_API = exports.SIGN_UP_API = exports.FORM_FIELD_EMAIL_ID = exports.FORM_FIELD_PASSWORD_ID = void 0; +exports.SIGNUP_EMAIL_EXISTS_API = exports.SIGNUP_EMAIL_EXISTS_API_OLD = exports.PASSWORD_UPDATE_API = exports.PASSWORD_RESET_API = exports.GENERATE_PASSWORD_RESET_TOKEN_API = exports.SIGN_IN_API = exports.SIGN_UP_API = exports.FORM_FIELD_EMAIL_ID = exports.FORM_FIELD_PASSWORD_ID = void 0; exports.FORM_FIELD_PASSWORD_ID = "password"; exports.FORM_FIELD_EMAIL_ID = "email"; exports.SIGN_UP_API = "/signup"; exports.SIGN_IN_API = "/signin"; exports.GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; exports.PASSWORD_RESET_API = "/user/password/reset"; +exports.PASSWORD_UPDATE_API = "/user/password/update"; exports.SIGNUP_EMAIL_EXISTS_API_OLD = "/signup/email/exists"; exports.SIGNUP_EMAIL_EXISTS_API = "/emailpassword/email/exists"; diff --git a/lib/build/recipe/emailpassword/recipe.js b/lib/build/recipe/emailpassword/recipe.js index dc968c536..fa5e7a5b9 100644 --- a/lib/build/recipe/emailpassword/recipe.js +++ b/lib/build/recipe/emailpassword/recipe.js @@ -30,6 +30,7 @@ const generatePasswordResetToken_1 = __importDefault(require("./api/generatePass const passwordReset_1 = __importDefault(require("./api/passwordReset")); const utils_2 = require("../../utils"); const emailExists_1 = __importDefault(require("./api/emailExists")); +const updatePassword_1 = __importDefault(require("./api/updatePassword")); const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); const implementation_1 = __importDefault(require("./api/implementation")); const querier_1 = require("../../querier"); @@ -84,6 +85,12 @@ class Recipe extends recipeModule_1.default { id: constants_1.SIGNUP_EMAIL_EXISTS_API_OLD, disabled: this.apiImpl.emailExistsGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.PASSWORD_UPDATE_API), + id: constants_1.PASSWORD_UPDATE_API, + disabled: this.apiImpl.updatePasswordPOST === undefined, + }, ]; }; this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { @@ -107,6 +114,8 @@ class Recipe extends recipeModule_1.default { return await passwordReset_1.default(this.apiImpl, tenantId, options, userContext); } else if (id === constants_1.SIGNUP_EMAIL_EXISTS_API || id === constants_1.SIGNUP_EMAIL_EXISTS_API_OLD) { return await emailExists_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.PASSWORD_UPDATE_API) { + return await updatePassword_1.default(this.apiImpl, tenantId, options, userContext); } return false; }; diff --git a/lib/ts/recipe/emailpassword/api/updatePassword.ts b/lib/ts/recipe/emailpassword/api/updatePassword.ts new file mode 100644 index 000000000..812ca168b --- /dev/null +++ b/lib/ts/recipe/emailpassword/api/updatePassword.ts @@ -0,0 +1,55 @@ +/** + * This file contains the top-level handler definition for password update + */ + +import { send200Response } from "../../../utils"; +import STError from "../error"; +import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; +import Session from "../../session"; + +export default async function updatePassword( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.updatePasswordPOST === undefined) { + return false; + } + + const { newPassword, oldPassword } = await options.req.getJSONBody(); + + if (newPassword === undefined) { + throw new STError({ + message: "Missing required parameter 'newPassword'", + type: STError.BAD_INPUT_ERROR, + }); + } + + if (oldPassword === undefined) { + throw new STError({ + message: "Missing required parameter 'oldPassword'", + type: STError.BAD_INPUT_ERROR, + }); + } + + const session = await Session.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext + ); + + let result = await apiImplementation.updatePasswordPOST({ + newPassword, + oldPassword, + session, + options, + userContext, + tenantId, + }); + + send200Response(options.res, result); + return true; +} diff --git a/lib/ts/recipe/emailpassword/constants.ts b/lib/ts/recipe/emailpassword/constants.ts index f2383c3e0..139ef84ee 100644 --- a/lib/ts/recipe/emailpassword/constants.ts +++ b/lib/ts/recipe/emailpassword/constants.ts @@ -25,6 +25,8 @@ export const GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; export const PASSWORD_RESET_API = "/user/password/reset"; +export const PASSWORD_UPDATE_API = "/user/password/update"; + export const SIGNUP_EMAIL_EXISTS_API_OLD = "/signup/email/exists"; export const SIGNUP_EMAIL_EXISTS_API = "/emailpassword/email/exists"; diff --git a/lib/ts/recipe/emailpassword/recipe.ts b/lib/ts/recipe/emailpassword/recipe.ts index f6f98431d..87b32edf9 100644 --- a/lib/ts/recipe/emailpassword/recipe.ts +++ b/lib/ts/recipe/emailpassword/recipe.ts @@ -26,6 +26,7 @@ import { PASSWORD_RESET_API, SIGNUP_EMAIL_EXISTS_API, SIGNUP_EMAIL_EXISTS_API_OLD, + PASSWORD_UPDATE_API, } from "./constants"; import signUpAPI from "./api/signup"; import signInAPI from "./api/signin"; @@ -33,6 +34,7 @@ import generatePasswordResetTokenAPI from "./api/generatePasswordResetToken"; import passwordResetAPI from "./api/passwordReset"; import { isTestEnv, send200Response } from "../../utils"; import emailExistsAPI from "./api/emailExists"; +import updatePasswordAPI from "./api/updatePassword"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import { Querier } from "../../querier"; @@ -290,6 +292,12 @@ export default class Recipe extends RecipeModule { id: SIGNUP_EMAIL_EXISTS_API_OLD, disabled: this.apiImpl.emailExistsGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(PASSWORD_UPDATE_API), + id: PASSWORD_UPDATE_API, + disabled: this.apiImpl.updatePasswordPOST === undefined, + }, ]; }; @@ -322,6 +330,8 @@ export default class Recipe extends RecipeModule { return await passwordResetAPI(this.apiImpl, tenantId, options, userContext); } else if (id === SIGNUP_EMAIL_EXISTS_API || id === SIGNUP_EMAIL_EXISTS_API_OLD) { return await emailExistsAPI(this.apiImpl, tenantId, options, userContext); + } else if (id === PASSWORD_UPDATE_API) { + return await updatePasswordAPI(this.apiImpl, tenantId, options, userContext); } return false; }; From 1a36ea9cf90d776e1fbd5d9d17d729fb3537e88d Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 15:30:12 +0530 Subject: [PATCH 3/9] Add some fixes for the update password API type definition --- lib/build/recipe/emailpassword/api/implementation.js | 8 +++++--- lib/ts/recipe/emailpassword/api/implementation.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 3fefcd9cb..a53936ba0 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -787,14 +787,16 @@ function getAPIImplementation() { // ID accordingly. const existingUser = await __1.getUser(session.getUserId(), userContext); if (existingUser === undefined) { - // Should never come here as we verified above that the user exists. - throw new Error("Should never come here as the user was checked to exist"); + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session user not found", + }); } const recipeUser = existingUser.loginMethods.find((lm) => lm.recipeId === "emailpassword"); if (!recipeUser || recipeUser.email === undefined) { throw new error_1.default({ type: error_1.default.UNAUTHORISED, - message: "A valid session is required to authenticate user for updating password", + message: "No user found with emailpassword recipe", }); } const email = recipeUser.email; diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index e1e5afd55..ba8e207f6 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -944,6 +944,7 @@ export default function getAPIImplementation(): APIInterface { | { status: "WRONG_CREDENTIALS_ERROR" } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | { status: "USER_DELETED_WHILE_IN_PROGRESS"; reason: string } + | SessionError | GeneralErrorResponse > { /** @@ -956,14 +957,16 @@ export default function getAPIImplementation(): APIInterface { // ID accordingly. const existingUser = await getUser(session.getUserId(), userContext); if (existingUser === undefined) { - // Should never come here as we verified above that the user exists. - throw new Error("Should never come here as the user was checked to exist"); + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); } const recipeUser = existingUser.loginMethods.find((lm) => lm.recipeId === "emailpassword"); if (!recipeUser || recipeUser.email === undefined) { throw new SessionError({ type: SessionError.UNAUTHORISED, - message: "A valid session is required to authenticate user for updating password", + message: "No user found with emailpassword recipe", }); } const email = recipeUser.email; From 08977b6d99f9f9366ac92f6c4a536c2320aa1814 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 15:59:42 +0530 Subject: [PATCH 4/9] Add API type definition for getting all sessions for user --- .../recipe/session/api/implementation.js | 64 +++++++++++++- lib/build/recipe/session/types.d.ts | 3 + lib/ts/recipe/session/api/implementation.ts | 88 ++++++++++++++++++- lib/ts/recipe/session/types.ts | 3 + 4 files changed, 154 insertions(+), 4 deletions(-) diff --git a/lib/build/recipe/session/api/implementation.js b/lib/build/recipe/session/api/implementation.js index b03e57366..c3d334baf 100644 --- a/lib/build/recipe/session/api/implementation.js +++ b/lib/build/recipe/session/api/implementation.js @@ -5,9 +5,12 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +const __1 = __importDefault(require("../")); const utils_1 = require("../../../utils"); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); const sessionRequestFunctions_1 = require("../sessionRequestFunctions"); +const error_1 = __importDefault(require("../error")); +const __2 = require("../../.."); function getAPIInterface() { return { refreshPOST: async function ({ options, userContext }) { @@ -51,7 +54,66 @@ function getAPIInterface() { status: "OK", }; }, - allSessionsGET: undefined, + allSessionsGET: async function ({ session, options, tenantId, userContext }) { + /** + * Get all the active sessions for the logged in user. + */ + // Get the logged in user's userId + const userId = session.getUserId(userContext); + // We need to verify that the user is authenticated because + // the getAllSessionHandlesForUser function doesn't check + // whether the user is logged in or not + const existingUser = await __2.getUser(userId, userContext); + if (existingUser === undefined) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session user not found", + }); + } + // We will first fetch the list of sessionHandles for the user + // and then fetch the information for each of them. + const allSessionHandles = await options.recipeImplementation.getAllSessionHandlesForUser({ + userId, + fetchSessionsForAllLinkedAccounts: true, + tenantId, + fetchAcrossAllTenants: false, + userContext, + }); + // Since we need to fetch multiple sessions information, + // we are creating multiple promises for fetching the details + // and using a Promise.all() to resolve them together. + const userSessions = []; + const sessionGetPromises = []; + allSessionHandles.forEach((sessionHandle) => { + sessionGetPromises.push( + new Promise(async (resolve, reject) => { + try { + const sessionInformation = await __1.default.getSessionInformation( + sessionHandle, + userContext + ); + if (sessionInformation !== undefined) { + userSessions.push(sessionInformation); + } + resolve(); + } catch (err) { + reject(err); + } + }) + ); + }); + // Wait for the sessions to be fetched. + await Promise.all(sessionGetPromises); + // Sort the fetched session based on their timeCreated values + // to ensure that the newer sessions show up at the top. + const sessionsSortedByCreatedTime = userSessions.sort( + (sessionA, sessionB) => sessionB.timeCreated - sessionA.timeCreated + ); + return { + status: "OK", + sessions: sessionsSortedByCreatedTime, + }; + }, revokeSessionPOST: undefined, }; } diff --git a/lib/build/recipe/session/types.d.ts b/lib/build/recipe/session/types.d.ts index ffd51104d..24cc30c9d 100644 --- a/lib/build/recipe/session/types.d.ts +++ b/lib/build/recipe/session/types.d.ts @@ -5,6 +5,7 @@ import OverrideableBuilder from "supertokens-js-override"; import { JSONObject, JSONValue, UserContext } from "../../types"; import { GeneralErrorResponse } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import SessionError from "./error"; export declare type KeyInfo = { publicKey: string; expiryTime: number; @@ -328,12 +329,14 @@ export declare type APIInterface = { | ((input: { session: SessionContainerInterface; options: APIOptions; + tenantId: string; userContext: UserContext; }) => Promise< | { status: "OK"; sessions: SessionInformation[]; } + | SessionError | GeneralErrorResponse >); revokeSessionPOST: diff --git a/lib/ts/recipe/session/api/implementation.ts b/lib/ts/recipe/session/api/implementation.ts index 084e722a7..dc426f2f7 100644 --- a/lib/ts/recipe/session/api/implementation.ts +++ b/lib/ts/recipe/session/api/implementation.ts @@ -1,9 +1,11 @@ -import { APIInterface, APIOptions, VerifySessionOptions } from "../"; +import SessionWrapper, { APIInterface, APIOptions, VerifySessionOptions } from "../"; import { normaliseHttpMethod } from "../../../utils"; import NormalisedURLPath from "../../../normalisedURLPath"; -import { SessionContainerInterface } from "../types"; +import { SessionContainerInterface, SessionInformation } from "../types"; import { GeneralErrorResponse, UserContext } from "../../../types"; import { getSessionFromRequest, refreshSessionInRequest } from "../sessionRequestFunctions"; +import SessionError from "../error"; +import { getUser } from "../../.."; export default function getAPIInterface(): APIInterface { return { @@ -81,7 +83,87 @@ export default function getAPIInterface(): APIInterface { }; }, - allSessionsGET: undefined, + allSessionsGET: async function ({ + session, + options, + tenantId, + userContext, + }): Promise< + | { + status: "OK"; + sessions: SessionInformation[]; + } + | SessionError + | GeneralErrorResponse + > { + /** + * Get all the active sessions for the logged in user. + */ + // Get the logged in user's userId + const userId = session.getUserId(userContext); + + // We need to verify that the user is authenticated because + // the getAllSessionHandlesForUser function doesn't check + // whether the user is logged in or not + const existingUser = await getUser(userId, userContext); + if (existingUser === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); + } + + // We will first fetch the list of sessionHandles for the user + // and then fetch the information for each of them. + const allSessionHandles = await options.recipeImplementation.getAllSessionHandlesForUser({ + userId, + fetchSessionsForAllLinkedAccounts: true, + tenantId, + fetchAcrossAllTenants: false, + userContext, + }); + + // Since we need to fetch multiple sessions information, + // we are creating multiple promises for fetching the details + // and using a Promise.all() to resolve them together. + const userSessions: SessionInformation[] = []; + const sessionGetPromises: Promise[] = []; + + allSessionHandles.forEach((sessionHandle) => { + sessionGetPromises.push( + new Promise(async (resolve, reject) => { + try { + const sessionInformation = await SessionWrapper.getSessionInformation( + sessionHandle, + userContext + ); + if (sessionInformation !== undefined) { + userSessions.push(sessionInformation); + } + + resolve(); + } catch (err) { + reject(err); + } + }) + ); + }); + + // Wait for the sessions to be fetched. + await Promise.all(sessionGetPromises); + + // Sort the fetched session based on their timeCreated values + // to ensure that the newer sessions show up at the top. + const sessionsSortedByCreatedTime = userSessions.sort( + (sessionA, sessionB) => sessionB.timeCreated - sessionA.timeCreated + ); + + return { + status: "OK", + sessions: sessionsSortedByCreatedTime, + }; + }, + revokeSessionPOST: undefined, }; } diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index 253aee8cb..bf8255b90 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -18,6 +18,7 @@ import OverrideableBuilder from "supertokens-js-override"; import { JSONObject, JSONValue, UserContext } from "../../types"; import { GeneralErrorResponse } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import SessionError from "./error"; export type KeyInfo = { publicKey: string; @@ -399,12 +400,14 @@ export type APIInterface = { | ((input: { session: SessionContainerInterface; options: APIOptions; + tenantId: string; userContext: UserContext; }) => Promise< | { status: "OK"; sessions: SessionInformation[]; } + | SessionError | GeneralErrorResponse >); From 087977f5f35b32a52804b39d68b7b00cc76a4a7a Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 16:08:14 +0530 Subject: [PATCH 5/9] Add handler for getting all sessions for user --- .../recipe/session/api/implementation.js | 4 +++ lib/build/recipe/session/api/sessionsGet.d.ts | 12 +++++++ lib/build/recipe/session/api/sessionsGet.js | 27 ++++++++++++++++ lib/build/recipe/session/constants.d.ts | 1 + lib/build/recipe/session/constants.js | 3 +- lib/build/recipe/session/recipe.js | 9 ++++++ lib/ts/recipe/session/api/implementation.ts | 4 +++ lib/ts/recipe/session/api/sessionsGet.ts | 31 +++++++++++++++++++ lib/ts/recipe/session/constants.ts | 1 + lib/ts/recipe/session/recipe.ts | 11 ++++++- 10 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 lib/build/recipe/session/api/sessionsGet.d.ts create mode 100644 lib/build/recipe/session/api/sessionsGet.js create mode 100644 lib/ts/recipe/session/api/sessionsGet.ts diff --git a/lib/build/recipe/session/api/implementation.js b/lib/build/recipe/session/api/implementation.js index c3d334baf..92d5d3e59 100644 --- a/lib/build/recipe/session/api/implementation.js +++ b/lib/build/recipe/session/api/implementation.js @@ -57,6 +57,10 @@ function getAPIInterface() { allSessionsGET: async function ({ session, options, tenantId, userContext }) { /** * Get all the active sessions for the logged in user. + * + * This function will fetched all sessions for the user and + * return them in descending order based on the time the session + * was created at. */ // Get the logged in user's userId const userId = session.getUserId(userContext); diff --git a/lib/build/recipe/session/api/sessionsGet.d.ts b/lib/build/recipe/session/api/sessionsGet.d.ts new file mode 100644 index 000000000..51d22e7e7 --- /dev/null +++ b/lib/build/recipe/session/api/sessionsGet.d.ts @@ -0,0 +1,12 @@ +// @ts-nocheck +/** + * This defines the top-level handler for allSessionsGET type. + */ +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function sessionsGet( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext, + tenantId: string +): Promise; diff --git a/lib/build/recipe/session/api/sessionsGet.js b/lib/build/recipe/session/api/sessionsGet.js new file mode 100644 index 000000000..986bcd3a5 --- /dev/null +++ b/lib/build/recipe/session/api/sessionsGet.js @@ -0,0 +1,27 @@ +"use strict"; +/** + * This defines the top-level handler for allSessionsGET type. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const __1 = __importDefault(require("..")); +async function sessionsGet(apiImplementation, options, userContext, tenantId) { + if (apiImplementation.allSessionsGET === undefined) { + return false; + } + const session = await __1.default.getSession(options.req, options.res, {}, userContext); + let result = await apiImplementation.allSessionsGET({ + session, + options, + tenantId, + userContext, + }); + utils_1.send200Response(options.res, result); + return true; +} +exports.default = sessionsGet; diff --git a/lib/build/recipe/session/constants.d.ts b/lib/build/recipe/session/constants.d.ts index 78a3ec979..14f7f612c 100644 --- a/lib/build/recipe/session/constants.d.ts +++ b/lib/build/recipe/session/constants.d.ts @@ -2,6 +2,7 @@ import { TokenTransferMethod } from "./types"; export declare const REFRESH_API_PATH = "/session/refresh"; export declare const SIGNOUT_API_PATH = "/signout"; +export declare const SESSIONS_GET_API_PATH = "/sessions"; export declare const availableTokenTransferMethods: TokenTransferMethod[]; export declare const oneYearInMs = 31536000000; export declare const JWKCacheCooldownInMs = 500; diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js index 84d5b0225..c4dd72515 100644 --- a/lib/build/recipe/session/constants.js +++ b/lib/build/recipe/session/constants.js @@ -14,9 +14,10 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; +exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SESSIONS_GET_API_PATH = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; exports.REFRESH_API_PATH = "/session/refresh"; exports.SIGNOUT_API_PATH = "/signout"; +exports.SESSIONS_GET_API_PATH = "/sessions"; exports.availableTokenTransferMethods = ["cookie", "header"]; exports.oneYearInMs = 31536000000; exports.JWKCacheCooldownInMs = 500; diff --git a/lib/build/recipe/session/recipe.js b/lib/build/recipe/session/recipe.js index fcec65ac9..0273c5ce5 100644 --- a/lib/build/recipe/session/recipe.js +++ b/lib/build/recipe/session/recipe.js @@ -34,6 +34,7 @@ const supertokens_js_override_1 = __importDefault(require("supertokens-js-overri const logger_1 = require("../../logger"); const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); const utils_2 = require("../../utils"); +const sessionsGet_1 = __importDefault(require("./api/sessionsGet")); // For Express class SessionRecipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { @@ -73,6 +74,12 @@ class SessionRecipe extends recipeModule_1.default { id: constants_1.SIGNOUT_API_PATH, disabled: this.apiImpl.signOutPOST === undefined, }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SESSIONS_GET_API_PATH), + id: constants_1.SESSIONS_GET_API_PATH, + disabled: this.apiImpl.allSessionsGET === undefined, + }, ]; return apisHandled; }; @@ -89,6 +96,8 @@ class SessionRecipe extends recipeModule_1.default { return await refresh_1.default(this.apiImpl, options, userContext); } else if (id === constants_1.SIGNOUT_API_PATH) { return await signout_1.default(this.apiImpl, options, userContext); + } else if (id === constants_1.SESSIONS_GET_API_PATH) { + return await sessionsGet_1.default(this.apiImpl, options, userContext, _tenantId); } else { return false; } diff --git a/lib/ts/recipe/session/api/implementation.ts b/lib/ts/recipe/session/api/implementation.ts index dc426f2f7..a86a4a44d 100644 --- a/lib/ts/recipe/session/api/implementation.ts +++ b/lib/ts/recipe/session/api/implementation.ts @@ -98,6 +98,10 @@ export default function getAPIInterface(): APIInterface { > { /** * Get all the active sessions for the logged in user. + * + * This function will fetched all sessions for the user and + * return them in descending order based on the time the session + * was created at. */ // Get the logged in user's userId const userId = session.getUserId(userContext); diff --git a/lib/ts/recipe/session/api/sessionsGet.ts b/lib/ts/recipe/session/api/sessionsGet.ts new file mode 100644 index 000000000..b234044c9 --- /dev/null +++ b/lib/ts/recipe/session/api/sessionsGet.ts @@ -0,0 +1,31 @@ +/** + * This defines the top-level handler for allSessionsGET type. + */ + +import { send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import Session from ".."; + +export default async function sessionsGet( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext, + tenantId: string +): Promise { + if (apiImplementation.allSessionsGET === undefined) { + return false; + } + + const session = await Session.getSession(options.req, options.res, {}, userContext); + + let result = await apiImplementation.allSessionsGET({ + session, + options, + tenantId, + userContext, + }); + + send200Response(options.res, result); + return true; +} diff --git a/lib/ts/recipe/session/constants.ts b/lib/ts/recipe/session/constants.ts index e7a5c296a..e5545b0ee 100644 --- a/lib/ts/recipe/session/constants.ts +++ b/lib/ts/recipe/session/constants.ts @@ -17,6 +17,7 @@ import { TokenTransferMethod } from "./types"; export const REFRESH_API_PATH = "/session/refresh"; export const SIGNOUT_API_PATH = "/signout"; +export const SESSIONS_GET_API_PATH = "/sessions"; export const availableTokenTransferMethods: TokenTransferMethod[] = ["cookie", "header"]; diff --git a/lib/ts/recipe/session/recipe.ts b/lib/ts/recipe/session/recipe.ts index 7dd003e3a..4df9417e9 100644 --- a/lib/ts/recipe/session/recipe.ts +++ b/lib/ts/recipe/session/recipe.ts @@ -28,7 +28,7 @@ import { validateAndNormaliseUserInput } from "./utils"; import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod, UserContext } from "../../types"; import handleRefreshAPI from "./api/refresh"; import signOutAPI from "./api/signout"; -import { REFRESH_API_PATH, SIGNOUT_API_PATH } from "./constants"; +import { REFRESH_API_PATH, SESSIONS_GET_API_PATH, SIGNOUT_API_PATH } from "./constants"; import NormalisedURLPath from "../../normalisedURLPath"; import { clearSessionFromAllTokenTransferMethods, @@ -43,6 +43,7 @@ import { APIOptions } from "."; import { logDebugMessage } from "../../logger"; import { resetCombinedJWKS } from "../../combinedRemoteJWKSet"; import { hasGreaterThanEqualToFDI, isTestEnv } from "../../utils"; +import sessionsGetAPI from "./api/sessionsGet"; // For Express export default class SessionRecipe extends RecipeModule { @@ -162,6 +163,12 @@ export default class SessionRecipe extends RecipeModule { id: SIGNOUT_API_PATH, disabled: this.apiImpl.signOutPOST === undefined, }, + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(SESSIONS_GET_API_PATH), + id: SESSIONS_GET_API_PATH, + disabled: this.apiImpl.allSessionsGET === undefined, + }, ]; return apisHandled; @@ -188,6 +195,8 @@ export default class SessionRecipe extends RecipeModule { return await handleRefreshAPI(this.apiImpl, options, userContext); } else if (id === SIGNOUT_API_PATH) { return await signOutAPI(this.apiImpl, options, userContext); + } else if (id === SESSIONS_GET_API_PATH) { + return await sessionsGetAPI(this.apiImpl, options, userContext, _tenantId); } else { return false; } From 5ef4f1ae088708da331539ed203294042b77a835 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 16:15:33 +0530 Subject: [PATCH 6/9] Add API type definition for revoking session with sessionHandle --- .../recipe/session/api/implementation.js | 33 ++++++++++++- lib/ts/recipe/session/api/implementation.ts | 47 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/lib/build/recipe/session/api/implementation.js b/lib/build/recipe/session/api/implementation.js index 92d5d3e59..8208d69e0 100644 --- a/lib/build/recipe/session/api/implementation.js +++ b/lib/build/recipe/session/api/implementation.js @@ -118,7 +118,38 @@ function getAPIInterface() { sessions: sessionsSortedByCreatedTime, }; }, - revokeSessionPOST: undefined, + revokeSessionPOST: async function ({ sessionHandle, session, options, userContext }) { + /** + * Revoke the session passed using the sessionHandle. + */ + // Get the logged in user's userId + const userId = session.getUserId(userContext); + // We need to verify that the user is authenticated because + // the revokeSession function doesn't check + // whether the user is logged in or not + const existingUser = await __2.getUser(userId, userContext); + if (existingUser === undefined) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session user not found", + }); + } + const wasRevoked = await options.recipeImplementation.revokeSession({ + sessionHandle, + userContext, + }); + if (!wasRevoked) { + // This is a very unlikely case but we should still consider + // it since the upper level function returns this. + // + // We will just throw an error so that the API consumer + // can understand that session was not removed. + throw new Error("Failed to revoke session"); + } + return { + status: "OK", + }; + }, }; } exports.default = getAPIInterface; diff --git a/lib/ts/recipe/session/api/implementation.ts b/lib/ts/recipe/session/api/implementation.ts index a86a4a44d..72175ae34 100644 --- a/lib/ts/recipe/session/api/implementation.ts +++ b/lib/ts/recipe/session/api/implementation.ts @@ -168,6 +168,51 @@ export default function getAPIInterface(): APIInterface { }; }, - revokeSessionPOST: undefined, + revokeSessionPOST: async function ({ + sessionHandle, + session, + options, + userContext, + }): Promise< + | { + status: "OK"; + } + | GeneralErrorResponse + > { + /** + * Revoke the session passed using the sessionHandle. + */ + // Get the logged in user's userId + const userId = session.getUserId(userContext); + + // We need to verify that the user is authenticated because + // the revokeSession function doesn't check + // whether the user is logged in or not + const existingUser = await getUser(userId, userContext); + if (existingUser === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); + } + + const wasRevoked = await options.recipeImplementation.revokeSession({ + sessionHandle, + userContext, + }); + + if (!wasRevoked) { + // This is a very unlikely case but we should still consider + // it since the upper level function returns this. + // + // We will just throw an error so that the API consumer + // can understand that session was not removed. + throw new Error("Failed to revoke session"); + } + + return { + status: "OK", + }; + }, }; } From 7d74b536e53a0961e1d33bfd0278887f048cab78 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 16:23:05 +0530 Subject: [PATCH 7/9] Add top-level handler for revoking a session through API --- .../recipe/session/api/sessionRevoke.d.ts | 11 +++++ lib/build/recipe/session/api/sessionRevoke.js | 35 ++++++++++++++++ lib/build/recipe/session/constants.d.ts | 1 + lib/build/recipe/session/constants.js | 3 +- lib/build/recipe/session/recipe.js | 9 +++++ lib/ts/recipe/session/api/sessionRevoke.ts | 40 +++++++++++++++++++ lib/ts/recipe/session/constants.ts | 1 + lib/ts/recipe/session/recipe.ts | 11 ++++- 8 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 lib/build/recipe/session/api/sessionRevoke.d.ts create mode 100644 lib/build/recipe/session/api/sessionRevoke.js create mode 100644 lib/ts/recipe/session/api/sessionRevoke.ts diff --git a/lib/build/recipe/session/api/sessionRevoke.d.ts b/lib/build/recipe/session/api/sessionRevoke.d.ts new file mode 100644 index 000000000..919cf7d29 --- /dev/null +++ b/lib/build/recipe/session/api/sessionRevoke.d.ts @@ -0,0 +1,11 @@ +// @ts-nocheck +/** + * Defines top-level handler for revoking a session using it's handle. + */ +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function sessionRevoke( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/session/api/sessionRevoke.js b/lib/build/recipe/session/api/sessionRevoke.js new file mode 100644 index 000000000..c87c7992a --- /dev/null +++ b/lib/build/recipe/session/api/sessionRevoke.js @@ -0,0 +1,35 @@ +"use strict"; +/** + * Defines top-level handler for revoking a session using it's handle. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const __1 = __importDefault(require("..")); +const error_1 = __importDefault(require("../../../error")); +async function sessionRevoke(apiImplementation, options, userContext) { + if (apiImplementation.revokeSessionPOST === undefined) { + return false; + } + let sessionHandle = options.req.getKeyValueFromQuery("sessionHandle"); + if (sessionHandle === undefined || typeof sessionHandle !== "string") { + throw new error_1.default({ + message: "Missing required parameter 'newPassword'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + const session = await __1.default.getSession(options.req, options.res, {}, userContext); + let result = await apiImplementation.revokeSessionPOST({ + sessionHandle, + session, + options, + userContext, + }); + utils_1.send200Response(options.res, result); + return true; +} +exports.default = sessionRevoke; diff --git a/lib/build/recipe/session/constants.d.ts b/lib/build/recipe/session/constants.d.ts index 14f7f612c..e02b59bd6 100644 --- a/lib/build/recipe/session/constants.d.ts +++ b/lib/build/recipe/session/constants.d.ts @@ -3,6 +3,7 @@ import { TokenTransferMethod } from "./types"; export declare const REFRESH_API_PATH = "/session/refresh"; export declare const SIGNOUT_API_PATH = "/signout"; export declare const SESSIONS_GET_API_PATH = "/sessions"; +export declare const SESSION_REVOKE_API_PATH = "/session/revoke"; export declare const availableTokenTransferMethods: TokenTransferMethod[]; export declare const oneYearInMs = 31536000000; export declare const JWKCacheCooldownInMs = 500; diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js index c4dd72515..4286f4e3f 100644 --- a/lib/build/recipe/session/constants.js +++ b/lib/build/recipe/session/constants.js @@ -14,10 +14,11 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SESSIONS_GET_API_PATH = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; +exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SESSION_REVOKE_API_PATH = exports.SESSIONS_GET_API_PATH = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; exports.REFRESH_API_PATH = "/session/refresh"; exports.SIGNOUT_API_PATH = "/signout"; exports.SESSIONS_GET_API_PATH = "/sessions"; +exports.SESSION_REVOKE_API_PATH = "/session/revoke"; exports.availableTokenTransferMethods = ["cookie", "header"]; exports.oneYearInMs = 31536000000; exports.JWKCacheCooldownInMs = 500; diff --git a/lib/build/recipe/session/recipe.js b/lib/build/recipe/session/recipe.js index 0273c5ce5..cbce81893 100644 --- a/lib/build/recipe/session/recipe.js +++ b/lib/build/recipe/session/recipe.js @@ -35,6 +35,7 @@ const logger_1 = require("../../logger"); const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); const utils_2 = require("../../utils"); const sessionsGet_1 = __importDefault(require("./api/sessionsGet")); +const sessionRevoke_1 = __importDefault(require("./api/sessionRevoke")); // For Express class SessionRecipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { @@ -80,6 +81,12 @@ class SessionRecipe extends recipeModule_1.default { id: constants_1.SESSIONS_GET_API_PATH, disabled: this.apiImpl.allSessionsGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SESSION_REVOKE_API_PATH), + id: constants_1.SESSION_REVOKE_API_PATH, + disabled: this.apiImpl.revokeSessionPOST === undefined, + }, ]; return apisHandled; }; @@ -98,6 +105,8 @@ class SessionRecipe extends recipeModule_1.default { return await signout_1.default(this.apiImpl, options, userContext); } else if (id === constants_1.SESSIONS_GET_API_PATH) { return await sessionsGet_1.default(this.apiImpl, options, userContext, _tenantId); + } else if (id === constants_1.SESSION_REVOKE_API_PATH) { + return await sessionRevoke_1.default(this.apiImpl, options, userContext); } else { return false; } diff --git a/lib/ts/recipe/session/api/sessionRevoke.ts b/lib/ts/recipe/session/api/sessionRevoke.ts new file mode 100644 index 000000000..19f20dbaa --- /dev/null +++ b/lib/ts/recipe/session/api/sessionRevoke.ts @@ -0,0 +1,40 @@ +/** + * Defines top-level handler for revoking a session using it's handle. + */ + +import { send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import Session from ".."; +import STError from "../../../error"; + +export default async function sessionRevoke( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.revokeSessionPOST === undefined) { + return false; + } + + let sessionHandle = options.req.getKeyValueFromQuery("sessionHandle"); + + if (sessionHandle === undefined || typeof sessionHandle !== "string") { + throw new STError({ + message: "Missing required parameter 'newPassword'", + type: STError.BAD_INPUT_ERROR, + }); + } + + const session = await Session.getSession(options.req, options.res, {}, userContext); + + let result = await apiImplementation.revokeSessionPOST({ + sessionHandle, + session, + options, + userContext, + }); + + send200Response(options.res, result); + return true; +} diff --git a/lib/ts/recipe/session/constants.ts b/lib/ts/recipe/session/constants.ts index e5545b0ee..f2adb6623 100644 --- a/lib/ts/recipe/session/constants.ts +++ b/lib/ts/recipe/session/constants.ts @@ -18,6 +18,7 @@ import { TokenTransferMethod } from "./types"; export const REFRESH_API_PATH = "/session/refresh"; export const SIGNOUT_API_PATH = "/signout"; export const SESSIONS_GET_API_PATH = "/sessions"; +export const SESSION_REVOKE_API_PATH = "/session/revoke"; export const availableTokenTransferMethods: TokenTransferMethod[] = ["cookie", "header"]; diff --git a/lib/ts/recipe/session/recipe.ts b/lib/ts/recipe/session/recipe.ts index 4df9417e9..62f8992ae 100644 --- a/lib/ts/recipe/session/recipe.ts +++ b/lib/ts/recipe/session/recipe.ts @@ -28,7 +28,7 @@ import { validateAndNormaliseUserInput } from "./utils"; import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod, UserContext } from "../../types"; import handleRefreshAPI from "./api/refresh"; import signOutAPI from "./api/signout"; -import { REFRESH_API_PATH, SESSIONS_GET_API_PATH, SIGNOUT_API_PATH } from "./constants"; +import { REFRESH_API_PATH, SESSION_REVOKE_API_PATH, SESSIONS_GET_API_PATH, SIGNOUT_API_PATH } from "./constants"; import NormalisedURLPath from "../../normalisedURLPath"; import { clearSessionFromAllTokenTransferMethods, @@ -44,6 +44,7 @@ import { logDebugMessage } from "../../logger"; import { resetCombinedJWKS } from "../../combinedRemoteJWKSet"; import { hasGreaterThanEqualToFDI, isTestEnv } from "../../utils"; import sessionsGetAPI from "./api/sessionsGet"; +import sessionRevokeAPI from "./api/sessionRevoke"; // For Express export default class SessionRecipe extends RecipeModule { @@ -169,6 +170,12 @@ export default class SessionRecipe extends RecipeModule { id: SESSIONS_GET_API_PATH, disabled: this.apiImpl.allSessionsGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(SESSION_REVOKE_API_PATH), + id: SESSION_REVOKE_API_PATH, + disabled: this.apiImpl.revokeSessionPOST === undefined, + }, ]; return apisHandled; @@ -197,6 +204,8 @@ export default class SessionRecipe extends RecipeModule { return await signOutAPI(this.apiImpl, options, userContext); } else if (id === SESSIONS_GET_API_PATH) { return await sessionsGetAPI(this.apiImpl, options, userContext, _tenantId); + } else if (id === SESSION_REVOKE_API_PATH) { + return await sessionRevokeAPI(this.apiImpl, options, userContext); } else { return false; } From 8e128849a96d41943f7e4e1c15d156bf4a74756b Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 16:49:21 +0530 Subject: [PATCH 8/9] Add support for storing user-agent details in session data --- lib/build/recipe/session/constants.d.ts | 1 + lib/build/recipe/session/constants.js | 5 ++++- lib/build/recipe/session/index.js | 8 ++++++++ lib/build/recipe/session/utils.d.ts | 1 + lib/build/recipe/session/utils.js | 10 +++++++++- lib/ts/recipe/session/constants.ts | 4 ++++ lib/ts/recipe/session/index.ts | 14 ++++++++++++-- lib/ts/recipe/session/utils.ts | 8 ++++++++ 8 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/build/recipe/session/constants.d.ts b/lib/build/recipe/session/constants.d.ts index e02b59bd6..91be9f43d 100644 --- a/lib/build/recipe/session/constants.d.ts +++ b/lib/build/recipe/session/constants.d.ts @@ -8,3 +8,4 @@ export declare const availableTokenTransferMethods: TokenTransferMethod[]; export declare const oneYearInMs = 31536000000; export declare const JWKCacheCooldownInMs = 500; export declare const protectedProps: string[]; +export declare const USER_AGENT_KEY_FOR_SESSION_DATA = "__user-agent"; diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js index 4286f4e3f..33378da78 100644 --- a/lib/build/recipe/session/constants.js +++ b/lib/build/recipe/session/constants.js @@ -14,7 +14,7 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SESSION_REVOKE_API_PATH = exports.SESSIONS_GET_API_PATH = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; +exports.USER_AGENT_KEY_FOR_SESSION_DATA = exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SESSION_REVOKE_API_PATH = exports.SESSIONS_GET_API_PATH = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; exports.REFRESH_API_PATH = "/session/refresh"; exports.SIGNOUT_API_PATH = "/signout"; exports.SESSIONS_GET_API_PATH = "/sessions"; @@ -34,3 +34,6 @@ exports.protectedProps = [ "tId", "stt", ]; +// Key to store/retrieve user-agent details from session +// data in database. +exports.USER_AGENT_KEY_FOR_SESSION_DATA = "__user-agent"; diff --git a/lib/build/recipe/session/index.js b/lib/build/recipe/session/index.js index eebfd546a..51fbcde1a 100644 --- a/lib/build/recipe/session/index.js +++ b/lib/build/recipe/session/index.js @@ -48,6 +48,14 @@ class SessionWrapper { if (user !== undefined) { userId = user.id; } + // Extract user-agent from header and inject it into + // sessionDataInDatabase. + const userAgent = utils_1.extractUserAgent(req); + // Parse sessionDataInDatabase and make sure that it's initiated + // if it's not present. + sessionDataInDatabase = + sessionDataInDatabase === null || sessionDataInDatabase === undefined ? {} : sessionDataInDatabase; + sessionDataInDatabase[constants_2.USER_AGENT_KEY_FOR_SESSION_DATA] = userAgent; return await sessionRequestFunctions_1.createNewSessionInRequest({ req, res, diff --git a/lib/build/recipe/session/utils.d.ts b/lib/build/recipe/session/utils.d.ts index 24ce87cb5..a6cac1197 100644 --- a/lib/build/recipe/session/utils.d.ts +++ b/lib/build/recipe/session/utils.d.ts @@ -74,3 +74,4 @@ export declare function validateClaimsInPayload( reason: import("../../types").JSONValue; }[] >; +export declare function extractUserAgent(req: BaseRequest): string | undefined; diff --git a/lib/build/recipe/session/utils.js b/lib/build/recipe/session/utils.js index e4bfc3e2b..42745680b 100644 --- a/lib/build/recipe/session/utils.js +++ b/lib/build/recipe/session/utils.js @@ -19,7 +19,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateClaimsInPayload = exports.getRequiredClaimValidators = exports.setAccessTokenInResponse = exports.normaliseSameSiteOrThrowError = exports.validateAndNormaliseUserInput = exports.getURLProtocol = exports.normaliseSessionScopeOrThrowError = exports.sendTokenTheftDetectedResponse = exports.sendInvalidClaimResponse = exports.sendUnauthorisedResponse = exports.sendTryRefreshTokenResponse = void 0; +exports.extractUserAgent = exports.validateClaimsInPayload = exports.getRequiredClaimValidators = exports.setAccessTokenInResponse = exports.normaliseSameSiteOrThrowError = exports.validateAndNormaliseUserInput = exports.getURLProtocol = exports.normaliseSessionScopeOrThrowError = exports.sendTokenTheftDetectedResponse = exports.sendInvalidClaimResponse = exports.sendUnauthorisedResponse = exports.sendTryRefreshTokenResponse = void 0; const cookieAndHeaders_1 = require("./cookieAndHeaders"); const recipe_1 = __importDefault(require("./recipe")); const constants_1 = require("./constants"); @@ -333,3 +333,11 @@ function defaultGetTokenTransferMethod({ req, forCreateNewSession }) { return "any"; } } +function extractUserAgent(req) { + /** + * Extract user agent from the passed request and return it + * accordingly. + */ + return req.getHeaderValue("user-agent"); +} +exports.extractUserAgent = extractUserAgent; diff --git a/lib/ts/recipe/session/constants.ts b/lib/ts/recipe/session/constants.ts index f2adb6623..04b554dc2 100644 --- a/lib/ts/recipe/session/constants.ts +++ b/lib/ts/recipe/session/constants.ts @@ -38,3 +38,7 @@ export const protectedProps = [ "tId", "stt", ]; + +// Key to store/retrieve user-agent details from session +// data in database. +export const USER_AGENT_KEY_FOR_SESSION_DATA = "__user-agent"; diff --git a/lib/ts/recipe/session/index.ts b/lib/ts/recipe/session/index.ts index 683ff99a8..cd77fdddd 100644 --- a/lib/ts/recipe/session/index.ts +++ b/lib/ts/recipe/session/index.ts @@ -29,12 +29,12 @@ import Recipe from "./recipe"; import OpenIdRecipe from "../openid/recipe"; import JWTRecipe from "../jwt/recipe"; import { JSONObject, UserContext } from "../../types"; -import { getRequiredClaimValidators } from "./utils"; +import { extractUserAgent, getRequiredClaimValidators } from "./utils"; import { createNewSessionInRequest, getSessionFromRequest, refreshSessionInRequest } from "./sessionRequestFunctions"; import RecipeUserId from "../../recipeUserId"; import { getUser } from "../.."; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; -import { protectedProps } from "./constants"; +import { protectedProps, USER_AGENT_KEY_FOR_SESSION_DATA } from "./constants"; import { getUserContext } from "../../utils"; export default class SessionWrapper { @@ -61,6 +61,16 @@ export default class SessionWrapper { userId = user.id; } + // Extract user-agent from header and inject it into + // sessionDataInDatabase. + const userAgent = extractUserAgent(req); + + // Parse sessionDataInDatabase and make sure that it's initiated + // if it's not present. + sessionDataInDatabase = + sessionDataInDatabase === null || sessionDataInDatabase === undefined ? {} : sessionDataInDatabase; + sessionDataInDatabase[USER_AGENT_KEY_FOR_SESSION_DATA] = userAgent; + return await createNewSessionInRequest({ req, res, diff --git a/lib/ts/recipe/session/utils.ts b/lib/ts/recipe/session/utils.ts index 8a2538c49..b56b36057 100644 --- a/lib/ts/recipe/session/utils.ts +++ b/lib/ts/recipe/session/utils.ts @@ -424,3 +424,11 @@ function defaultGetTokenTransferMethod({ return "any"; } } + +export function extractUserAgent(req: BaseRequest): string | undefined { + /** + * Extract user agent from the passed request and return it + * accordingly. + */ + return req.getHeaderValue("user-agent"); +} From 5f6ce832dc251c4950d7c69e91f70a6e9630eda0 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 16:55:08 +0530 Subject: [PATCH 9/9] Add support for returning user-agent at top-level in session get endpoint --- lib/build/recipe/session/api/implementation.js | 10 +++++++++- lib/build/recipe/session/types.d.ts | 5 ++++- lib/ts/recipe/session/api/implementation.ts | 13 +++++++++---- lib/ts/recipe/session/types.ts | 6 +++++- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/build/recipe/session/api/implementation.js b/lib/build/recipe/session/api/implementation.js index 8208d69e0..0a172dff8 100644 --- a/lib/build/recipe/session/api/implementation.js +++ b/lib/build/recipe/session/api/implementation.js @@ -11,6 +11,7 @@ const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath" const sessionRequestFunctions_1 = require("../sessionRequestFunctions"); const error_1 = __importDefault(require("../error")); const __2 = require("../../.."); +const constants_1 = require("../constants"); function getAPIInterface() { return { refreshPOST: async function ({ options, userContext }) { @@ -97,7 +98,14 @@ function getAPIInterface() { userContext ); if (sessionInformation !== undefined) { - userSessions.push(sessionInformation); + userSessions.push( + Object.assign(Object.assign({}, sessionInformation), { + userAgent: + sessionInformation.sessionDataInDatabase[ + constants_1.USER_AGENT_KEY_FOR_SESSION_DATA + ], + }) + ); } resolve(); } catch (err) { diff --git a/lib/build/recipe/session/types.d.ts b/lib/build/recipe/session/types.d.ts index 24cc30c9d..6e142362f 100644 --- a/lib/build/recipe/session/types.d.ts +++ b/lib/build/recipe/session/types.d.ts @@ -334,7 +334,7 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - sessions: SessionInformation[]; + sessions: SessionInformationWithExtractedInformation[]; } | SessionError | GeneralErrorResponse @@ -368,6 +368,9 @@ export declare type SessionInformation = { timeCreated: number; tenantId: string; }; +export declare type SessionInformationWithExtractedInformation = SessionInformation & { + userAgent: string | undefined; +}; export declare type ClaimValidationResult = | { isValid: true; diff --git a/lib/ts/recipe/session/api/implementation.ts b/lib/ts/recipe/session/api/implementation.ts index 72175ae34..7a5f81eef 100644 --- a/lib/ts/recipe/session/api/implementation.ts +++ b/lib/ts/recipe/session/api/implementation.ts @@ -1,11 +1,12 @@ import SessionWrapper, { APIInterface, APIOptions, VerifySessionOptions } from "../"; import { normaliseHttpMethod } from "../../../utils"; import NormalisedURLPath from "../../../normalisedURLPath"; -import { SessionContainerInterface, SessionInformation } from "../types"; +import { SessionContainerInterface, SessionInformationWithExtractedInformation } from "../types"; import { GeneralErrorResponse, UserContext } from "../../../types"; import { getSessionFromRequest, refreshSessionInRequest } from "../sessionRequestFunctions"; import SessionError from "../error"; import { getUser } from "../../.."; +import { USER_AGENT_KEY_FOR_SESSION_DATA } from "../constants"; export default function getAPIInterface(): APIInterface { return { @@ -91,7 +92,7 @@ export default function getAPIInterface(): APIInterface { }): Promise< | { status: "OK"; - sessions: SessionInformation[]; + sessions: SessionInformationWithExtractedInformation[]; } | SessionError | GeneralErrorResponse @@ -130,7 +131,7 @@ export default function getAPIInterface(): APIInterface { // Since we need to fetch multiple sessions information, // we are creating multiple promises for fetching the details // and using a Promise.all() to resolve them together. - const userSessions: SessionInformation[] = []; + const userSessions: SessionInformationWithExtractedInformation[] = []; const sessionGetPromises: Promise[] = []; allSessionHandles.forEach((sessionHandle) => { @@ -142,7 +143,11 @@ export default function getAPIInterface(): APIInterface { userContext ); if (sessionInformation !== undefined) { - userSessions.push(sessionInformation); + userSessions.push({ + ...sessionInformation, + userAgent: + sessionInformation.sessionDataInDatabase[USER_AGENT_KEY_FOR_SESSION_DATA], + }); } resolve(); diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index bf8255b90..c4f3378ab 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -405,7 +405,7 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - sessions: SessionInformation[]; + sessions: SessionInformationWithExtractedInformation[]; } | SessionError | GeneralErrorResponse @@ -443,6 +443,10 @@ export type SessionInformation = { tenantId: string; }; +export type SessionInformationWithExtractedInformation = SessionInformation & { + userAgent: string | undefined; +}; + export type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: JSONValue }; export type ClaimValidationError = { id: string;