diff --git a/.changeset/afraid-parents-bake.md b/.changeset/afraid-parents-bake.md new file mode 100644 index 0000000000000..af2b8a10d4899 --- /dev/null +++ b/.changeset/afraid-parents-bake.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +REST endpoint `/v1/users.createToken` is not deprecated anymore. It now requires a `secret` parameter to generate a token for a user. This change is part of the effort to enhance security by ensuring that tokens are generated with an additional layer of validation. The `secret` parameter is validated against a new environment variable `CREATE_TOKENS_FOR_USERS_SECRET`. diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 468232e3b3b2b..a9dfc04d9f6e3 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -18,6 +18,7 @@ import { isUsersSetPreferencesParamsPOST, isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, + ajv, } from '@rocket.chat/rest-typings'; import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; @@ -69,6 +70,7 @@ import { deleteUserOwnAccount } from '../../../lib/server/methods/deleteUserOwnA import { settings } from '../../../settings/server'; import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; import { getURL } from '../../../utils/server/getURL'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams } from '../helpers/getUserFromParams'; @@ -756,17 +758,70 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +const usersEndpoints = API.v1.post( 'users.createToken', - { authRequired: true, deprecationVersion: '8.0.0' }, { - async post() { - const user = await getUserFromParams(this.bodyParams); + authRequired: true, + body: ajv.compile<{ userId: string; secret: string }>({ + type: 'object', + properties: { + userId: { + type: 'string', + minLength: 1, + }, + secret: { + type: 'string', + minLength: 1, + }, + }, + required: ['userId', 'secret'], + additionalProperties: false, + }), + response: { + 200: ajv.compile<{ data: { userId: string; authToken: string } }>({ + type: 'object', + properties: { + data: { + type: 'object', + properties: { + userId: { + type: 'string', + minLength: 1, + }, + authToken: { + type: 'string', + minLength: 1, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['data', 'success'], + additionalProperties: false, + }), + 400: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const user = await getUserFromParams(this.bodyParams); - const data = await generateAccessToken(this.userId, user._id); + const data = await generateAccessToken(user._id, this.bodyParams.secret); - return data ? API.v1.success({ data }) : API.v1.forbidden(); - }, + return API.v1.success({ data }); }, ); @@ -1429,3 +1484,10 @@ settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); }); + +type UsersEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends UsersEndpoints {} +} diff --git a/apps/meteor/app/lib/server/methods/createToken.ts b/apps/meteor/app/lib/server/methods/createToken.ts index 369a3607fa265..63de5b98f4210 100644 --- a/apps/meteor/app/lib/server/methods/createToken.ts +++ b/apps/meteor/app/lib/server/methods/createToken.ts @@ -1,14 +1,18 @@ -import { User } from '@rocket.chat/core-services'; +import { MeteorError, User } from '@rocket.chat/core-services'; import { Accounts } from 'meteor/accounts-base'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +declare module '@rocket.chat/ddp-client' { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface ServerMethods { + createToken(userId: string): { userId: string; authToken: string }; + } +} + +const { CREATE_TOKENS_FOR_USERS_SECRET } = process.env; -export async function generateAccessToken(callee: string, userId: string) { - if ( - !['yes', 'true'].includes(String(process.env.CREATE_TOKENS_FOR_USERS)) || - (callee !== userId && !(await hasPermissionAsync(callee, 'user-generate-access-token'))) - ) { - throw new Meteor.Error('error-not-authorized', 'Not authorized'); +export async function generateAccessToken(userId: string, secret: string) { + if (secret !== CREATE_TOKENS_FOR_USERS_SECRET) { + throw new MeteorError('error-not-authorized', 'Not authorized'); } const token = Accounts._generateStampedLoginToken(); diff --git a/apps/meteor/server/services/user/service.ts b/apps/meteor/server/services/user/service.ts index 6e2d5c58e1c72..13371c175f2f4 100644 --- a/apps/meteor/server/services/user/service.ts +++ b/apps/meteor/server/services/user/service.ts @@ -9,7 +9,7 @@ export class UserService extends ServiceClassInternal implements IUserService { protected name = 'user'; async ensureLoginTokensLimit(uid: string): Promise { - const [{ tokens }] = await Users.findAllResumeTokensByUserId(uid); + const [{ tokens } = { tokens: [] }] = await Users.findAllResumeTokensByUserId(uid); if (tokens.length < getMaxLoginTokens()) { return; } diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 6d4e62331bcf8..e2ac5b5aac75c 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -289,15 +289,6 @@ export type UsersEndpoints = { }; }; - '/v1/users.createToken': { - POST: (params: { userId?: string; username?: string; user?: string }) => { - data: { - userId: string; - authToken: string; - }; - }; - }; - '/v1/users.create': { POST: (params: UserCreateParamsPOST) => { user: IUser;