diff --git a/.changeset/flat-poets-cheat.md b/.changeset/flat-poets-cheat.md deleted file mode 100644 index 2560e2d3ee172..0000000000000 --- a/.changeset/flat-poets-cheat.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@rocket.chat/web-ui-registration': minor -'@rocket.chat/model-typings': minor -'@rocket.chat/core-typings': minor -'@rocket.chat/rest-typings': minor -'@rocket.chat/desktop-api': minor -'@rocket.chat/models': minor -'@rocket.chat/i18n': minor -'@rocket.chat/meteor': minor ---- - -## Phishing-Resistant Multi-Factor Authentication - -Introduces a more secure and reliable server-side OAuth authentication flow. - -### What’s New - -- **Improved OAuth login security** - OAuth authentication now happens fully on the server, reducing the risk of token theft, phishing attacks, and client-side credential interception. - -- **Built-in CSRF, state validation, and PKCE protection** - OAuth logins now include stronger protection against CSRF attacks, request tampering, and authorization code interception through secure state validation and PKCE support. - -- **Improved two-step verification with OAuth logins** - Users with email or TOTP two-factor authentication enabled will now be asked to complete 2FA even when signing in with providers like Google, GitHub, GitLab, and others. - -- **Improved mobile & desktop app login** - Mobile and desktop apps now support a smoother and more secure deep-link OAuth login flow. diff --git a/apps/meteor/app/2fa/server/code/EmailCheck.ts b/apps/meteor/app/2fa/server/code/EmailCheck.ts index c52531eaac781..5967aa518348b 100644 --- a/apps/meteor/app/2fa/server/code/EmailCheck.ts +++ b/apps/meteor/app/2fa/server/code/EmailCheck.ts @@ -10,7 +10,7 @@ import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; export class EmailCheck implements ICodeCheck { - public readonly name: string = 'email'; + public readonly name = 'email'; private getUserVerifiedEmails(user: IUser): string[] { if (!Array.isArray(user.emails)) { @@ -145,6 +145,6 @@ ${t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email')} public async maxFaildedAttemtpsReached(user: IUser) { const maxAttempts = settings.get('Accounts_TwoFactorAuthentication_Max_Invalid_Email_Code_Attempts'); - return Users.maxInvalidEmailCodeAttemptsReached(user._id, maxAttempts); + return (await Users.maxInvalidEmailCodeAttemptsReached(user._id, maxAttempts)) as boolean; } } diff --git a/apps/meteor/app/2fa/server/code/EmailCheckForOAuth.ts b/apps/meteor/app/2fa/server/code/EmailCheckForOAuth.ts deleted file mode 100644 index 68c3e8a05506d..0000000000000 --- a/apps/meteor/app/2fa/server/code/EmailCheckForOAuth.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { TwoFactorChallenges } from '@rocket.chat/models'; - -import { EmailCheck } from './EmailCheck'; - -export class EmailCheckForOAuth extends EmailCheck { - public override readonly name = 'email-oauth'; - - public readonly method = 'email'; - - public async sendTwoFactorChallenge(user: IUser): Promise { - const challengeId = await TwoFactorChallenges.createTwoFactorChallenge(user._id, 'email'); - await this.sendEmailCode(user); - return challengeId; - } - - public async verifyEmailTwoFactorChallenge(user: IUser, challengeId: string, code: string): Promise { - const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); - if (!challenge) { - return false; - } - - if (challenge.expireAt && challenge.expireAt < new Date()) { - throw new Meteor.Error('error-challenge-expired', 'challenge expired'); - } - - const isCodeValid = await this.verify(user, code); - - if (!isCodeValid) { - return false; - } - - await TwoFactorChallenges.removeByPendingChallengeId(challengeId); - - return true; - } -} diff --git a/apps/meteor/app/2fa/server/code/TOTPCheck.ts b/apps/meteor/app/2fa/server/code/TOTPCheck.ts index ae9502569457f..236016883f89e 100644 --- a/apps/meteor/app/2fa/server/code/TOTPCheck.ts +++ b/apps/meteor/app/2fa/server/code/TOTPCheck.ts @@ -5,7 +5,7 @@ import { settings } from '../../../settings/server'; import { TOTP } from '../lib/totp'; export class TOTPCheck implements ICodeCheck { - public readonly name: string = 'totp'; + public readonly name = 'totp'; public isEnabled(user: IUser): boolean { if (!settings.get('Accounts_TwoFactorAuthentication_By_TOTP_Enabled')) { diff --git a/apps/meteor/app/2fa/server/code/TOTPCheckForOAuth.ts b/apps/meteor/app/2fa/server/code/TOTPCheckForOAuth.ts deleted file mode 100644 index 02e8267fb59eb..0000000000000 --- a/apps/meteor/app/2fa/server/code/TOTPCheckForOAuth.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { TwoFactorChallenges } from '@rocket.chat/models'; - -import { TOTPCheck } from './TOTPCheck'; - -export class TOTPCheckForOAuth extends TOTPCheck { - public override readonly name = 'totp-oauth'; - - public readonly method = 'totp'; - - public async sendTwoFactorChallenge(user: IUser): Promise { - return TwoFactorChallenges.createTwoFactorChallenge(user._id, 'totp'); - } - - public async verifyEmailTwoFactorChallenge(user: IUser, challengeId: string, code: string): Promise { - const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); - if (!challenge) { - return false; - } - - if (challenge.expireAt && challenge.expireAt < new Date()) { - throw new Meteor.Error('error-challenge-expired', 'challenge expired'); - } - - const isCodeValid = await this.verify(user, code); - - if (!isCodeValid) { - return false; - } - - await TwoFactorChallenges.removeByPendingChallengeId(challengeId); - - return true; - } -} diff --git a/apps/meteor/app/2fa/server/code/index.ts b/apps/meteor/app/2fa/server/code/index.ts index 0094b25bd321d..8242b1c66185e 100644 --- a/apps/meteor/app/2fa/server/code/index.ts +++ b/apps/meteor/app/2fa/server/code/index.ts @@ -63,8 +63,8 @@ function getFingerprintFromConnection(connection: IMethodConnection): string { return crypto.createHash('md5').update(data).digest('hex'); } -export function getRememberDate(from: Date = new Date()): Date | undefined { - const rememberFor = settings.get('Accounts_TwoFactorAuthentication_RememberFor'); +function getRememberDate(from: Date = new Date()): Date | undefined { + const rememberFor = parseInt(settings.get('Accounts_TwoFactorAuthentication_RememberFor') as string, 10); if (rememberFor <= 0) { return; @@ -118,20 +118,6 @@ function isAuthorizedForToken(connection: IMethodConnection, user: IUser, option return true; } -export async function rememberAuthorizationByToken(token: string, userId: IUser['_id'], connection: IMethodConnection): Promise { - const user = await Users.findOneByIdAndLoginHashedToken(userId, token, { projection: { _id: 1, services: 1 } }); - if (!user) { - throw new Meteor.Error('error-user-not-found', 'user not found'); - } - - const expires = getRememberDate(); - if (!expires) { - return; - } - - await Users.setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(user._id, token, getFingerprintFromConnection(connection), expires); -} - async function rememberAuthorization(connection: IMethodConnection, user: IUser): Promise { const currentToken = Accounts._getLoginToken(connection.id); @@ -160,7 +146,7 @@ interface ICheckCodeForUser { connection?: IMethodConnection; } -export const getSecondFactorMethod = (user: IUser, method: string | undefined, options: ITwoFactorOptions): ICodeCheck | undefined => { +const getSecondFactorMethod = (user: IUser, method: string | undefined, options: ITwoFactorOptions): ICodeCheck | undefined => { // try first getting one of the available methods or the one that was already provided const selectedMethod = getMethodByNameOrFirstActiveForUser(user, method); if (selectedMethod) { @@ -188,6 +174,7 @@ export async function checkCodeForUser({ user, code, method, options = {}, conne } let existingUser: IUser | null; + if (typeof user === 'string') { existingUser = await getUserForCheck(user); } else { diff --git a/apps/meteor/app/api/server/ApiClass.ts b/apps/meteor/app/api/server/ApiClass.ts index 62a2d7c0601c8..d51726049e0dd 100644 --- a/apps/meteor/app/api/server/ApiClass.ts +++ b/apps/meteor/app/api/server/ApiClass.ts @@ -144,7 +144,7 @@ const rateLimiterDictionary: Record< } > = {}; -export const generateConnection = ( +const generateConnection = ( ipAddress: string, httpHeaders: Record, ): { diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index c53c56d797cc5..5a6a6f06cbbab 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -45,7 +45,7 @@ import './v1/mailer'; import './v1/teams'; import './v1/moderation'; import './v1/uploads'; -import './v1/twoFactorChallenges'; + // This has to come last so all endpoints are registered before generating the OpenAPI documentation import './default/openApi'; diff --git a/apps/meteor/app/api/server/v1/twoFactorChallenges.ts b/apps/meteor/app/api/server/v1/twoFactorChallenges.ts deleted file mode 100644 index fd48e22ad7123..0000000000000 --- a/apps/meteor/app/api/server/v1/twoFactorChallenges.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { IMethodConnection } from '@rocket.chat/core-typings'; -import { TwoFactorChallenges } from '@rocket.chat/models'; -import { isTwoFactorChallengesSendEmailCodeParamsPOST, isTwoFactorChallengesVerifyChallengeParamsPOST } from '@rocket.chat/rest-typings'; -import { Accounts } from 'meteor/accounts-base'; - -import { emailCheckForOAuth, getTwoFAMethodForOAuth } from '../../../../server/lib/oauth/twoFactorAuth'; -import { getUserForCheck, rememberAuthorizationByToken } from '../../../2fa/server/code'; -import { generateConnection } from '../ApiClass'; -import { API } from '../api'; - -API.v1.addRoute( - 'twoFactorChallenges.sendEmailCode', - { validateParams: isTwoFactorChallengesSendEmailCodeParamsPOST, rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 5 } }, - { - async post() { - const { challengeId } = this.bodyParams; - - if (!challengeId) { - throw new Meteor.Error('error-parameter-required', 'challengeId is required'); - } - - const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); - - if (!challenge) { - throw new Meteor.Error('error-challenge-not-found', 'challenge not found'); - } - - if (challenge.expireAt && challenge.expireAt < new Date()) { - throw new Meteor.Error('error-challenge-expired', 'challenge expired'); - } - - if (challenge.method !== 'email') { - throw new Meteor.Error('error-invalid-challenge-method', 'invalid challenge method'); - } - - const { userId } = challenge; - - const user = await getUserForCheck(userId); - - if (!user) { - throw new Meteor.Error('error-user-not-found', 'user not found'); - } - - await emailCheckForOAuth.sendEmailCode(user); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'twoFactorChallenges.verifyChallenge', - { - validateParams: isTwoFactorChallengesVerifyChallengeParamsPOST, - rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 5 }, - }, - { - async post() { - const { challengeId, code } = this.bodyParams; - - if (!challengeId || !code) { - throw new Meteor.Error('error-parameter-required', 'challengeId and code are required'); - } - - const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); - - if (!challenge) { - throw new Meteor.Error('error-challenge-not-found', 'challenge not found'); - } - - const { userId } = challenge; - - const user = await getUserForCheck(userId); - - if (!user) { - throw new Meteor.Error('error-user-not-found', 'user not found'); - } - - const twoFAMethod = getTwoFAMethodForOAuth(challenge.method); - - const isCodeValid = await twoFAMethod.verifyEmailTwoFactorChallenge(user, challengeId, code); - - if (!isCodeValid) { - const tooManyAttempts = await twoFAMethod.maxFaildedAttemtpsReached(user); - if (tooManyAttempts) { - await TwoFactorChallenges.removeByPendingChallengeId(challengeId); - throw new Meteor.Error('totp-max-attempts', 'TOTP Maximun Failed Attempts Reached'); - } - return API.v1.failure('error-invalid-code', 'Invalid code'); - } - - const stampedToken = Accounts._generateStampedLoginToken(); - - await Accounts._insertLoginToken(user._id, stampedToken); - - const hashedToken = Accounts._hashLoginToken(stampedToken.token); - - const connection = { - ...generateConnection(this.requestIp, this.request.headers), - token: hashedToken, - } as unknown as IMethodConnection; - - // remember the 2FA authorization for the next requests - await rememberAuthorizationByToken(hashedToken, user._id, connection); - - return API.v1.success({ - loginToken: stampedToken.token, - userId: user._id, - }); - }, - }, -); diff --git a/apps/meteor/app/apple/lib/handleIdentityToken.ts b/apps/meteor/app/apple/lib/handleIdentityToken.ts index 861aa42707077..f3ab1c8f9e66f 100644 --- a/apps/meteor/app/apple/lib/handleIdentityToken.ts +++ b/apps/meteor/app/apple/lib/handleIdentityToken.ts @@ -27,7 +27,7 @@ async function isValidAppleJWT(identityToken: string, header: any): Promise> { +export async function handleIdentityToken(identityToken: string): Promise<{ id: string; email: string; name: string }> { const decodedToken = KJUR.jws.JWS.parse(identityToken); if (!(await isValidAppleJWT(identityToken, decodedToken.headerObj))) { @@ -38,14 +38,15 @@ export async function handleIdentityToken(identityToken: string): Promise { if (!enabled) { - passport.unuse('apple'); return ServiceConfiguration.configurations.removeAsync({ service: 'apple', }); @@ -48,96 +38,43 @@ settings.watchMultiple( return; } - passport.unuse('apple'); - - passport.use( - 'apple', - - new AppleStrategy( - { - clientID: settings.get('Accounts_OAuth_Apple_id'), - teamID: settings.get('Accounts_OAuth_Apple_iss'), - keyID: settings.get('Accounts_OAuth_Apple_kid'), - privateKeyString: settings.get('Accounts_OAuth_Apple_secretKey').replace(/\\n/g, '\n'), - callbackURL: `${settings.get('Site_Url')}/_oauth/apple`, - scope: ['name', 'email'], - passReqToCallback: false, - state: false, - }, - async (accessToken: string, refreshToken: string, idToken: string, profile: Profile, done) => { - try { - const serviceData = await handleIdentityToken(idToken); - if (profile?.name) { - serviceData.name = `${profile.name.firstName}${profile.name.middleName ? ` ${profile.name.middleName}` : ''}${ - profile.name.lastName ? ` ${profile.name.lastName}` : '' - }`; - } - - if (!serviceData.email && profile?.email) { - serviceData.email = profile.email; - } - - const user = await Accounts.updateOrCreateUserFromExternalService( - 'apple', - { - accessToken, - refreshToken, - ...serviceData, - }, - {}, - ); - - if (!user?.userId || typeof user?.userId !== 'string') { - return done(new Error('User not found')); - } - - const userFromDB = await Users.findOneById(user.userId); - - if (!userFromDB) { - return done(new Error('User not found')); - } - - return done(null, userFromDB); - } catch (error: any) { - done(error); - return { - type: 'apple', - error: new MeteorError(Accounts.LoginCancelledError.numericError, error.message), - }; - } - }, - ), + const HEADER = { + kid, + alg: 'ES256', + }; + + const now = new Date(); + const exp = new Date(); + exp.setMonth(exp.getMonth() + 5); // from Apple docs expiration time must no be greater than 6 months + + const secret = KJUR.jws.JWS.sign( + null, + HEADER, + { + iss, + iat: Math.floor(now.getTime() / 1000), + exp: Math.floor(exp.getTime() / 1000), + aud: 'https://appleid.apple.com', + sub: clientId, + }, + serverSecret as string, ); - const callbackHandler = [ - express.urlencoded({ extended: true }), - passport.authenticate('apple', { failWithError: true, session: true, keepSessionInfo: true }), - passportOAuthCallback(settings.get('Site_Url')), - ]; - - oAuthRouter.get( - '/oauth/apple', - (req, _res, next) => { - const { loginClient } = req.query; - if (loginClient === 'mobile' || loginClient === 'desktop') { - req.session.loginClient = loginClient; - req.session.save(() => { - next(); - }); - } else { - //delete stale value from previous sessions if any - delete req.session.loginClient; - next(); - } + await ServiceConfiguration.configurations.upsertAsync( + { + service: 'apple', + }, + { + $set: { + showButton: true, + secret, + enabled: settings.get('Accounts_OAuth_Apple'), + loginStyle: 'popup', + clientId: clientId as string, + buttonColor: '#000', + buttonLabelColor: '#FFF', + }, }, - passport.authenticate('apple', { - scope: ['name', 'email'], - }), ); - - oAuthRouter - .route('/_oauth/apple') - .post(...callbackHandler) - .get(...callbackHandler); }, ); diff --git a/apps/meteor/app/apple/server/loginHandler.ts b/apps/meteor/app/apple/server/loginHandler.ts index dcb17fa06aff9..18ac7ddd75268 100644 --- a/apps/meteor/app/apple/server/loginHandler.ts +++ b/apps/meteor/app/apple/server/loginHandler.ts @@ -29,10 +29,10 @@ Accounts.registerLoginHandler('apple', async (loginRequest) => { profile.name = `${givenName} ${familyName}`; } - const result = await Accounts.updateOrCreateUserFromExternalService('apple', serviceData, { profile }); + const result = Accounts.updateOrCreateUserFromExternalService('apple', serviceData, { profile }); // Ensure processing succeeded - if (result?.userId === undefined) { + if (result === undefined || result.userId === undefined) { return { type: 'apple', error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'User creation failed from Apple response token'), diff --git a/apps/meteor/app/custom-oauth/server/customOAuth.ts b/apps/meteor/app/custom-oauth/server/customOAuth.ts deleted file mode 100644 index 13c4d4d6ca373..0000000000000 --- a/apps/meteor/app/custom-oauth/server/customOAuth.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { LDAP } from '@rocket.chat/core-services'; -import type { IUser, OAuthConfiguration } from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import { Users } from '@rocket.chat/models'; -import { isAbsoluteURL } from '@rocket.chat/tools'; -import { Accounts } from 'meteor/accounts-base'; -import type { DoneCallback } from 'passport'; -import type { VerifyFunction, StrategyOptions } from 'passport-oauth2'; -import { Strategy } from 'passport-oauth2'; - -import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; -import { client } from '../../../server/database/utils'; -import { callbacks } from '../../../server/lib/callbacks'; -import { saveUserIdentity } from '../../lib/server/functions/saveUserIdentity'; -import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; -import { settings } from '../../settings/server/cached'; - -const logger = new Logger('CustomOAuth'); -const BeforeUpdateOrCreateUserFromExternalService: ((serviceName: string, serviceData: Record) => Promise)[] = []; - -export class CustomOAuthStrategy extends Strategy { - options: StrategyOptions; - - config: OAuthConfiguration & { clientSecret: string }; - - identityPath: string; - - emailPath: string; - - tokenSentVia: string; - - identityTokenSentVia: string; - - keyField: string; - - usernameField: string; - - emailField: string; - - nameField: string; - - avatarField: string; - - mergeUsers: boolean; - - mergeUsersDistinctServices: boolean; - - rolesClaim: string; - - accessTokenParam: string; - - serverURL: string; - - tokenPath: string; - - constructor(name: string, config: OAuthConfiguration & { clientSecret: string }, verify: VerifyFunction) { - if (!config.serverURL || typeof config.serverURL !== 'string') { - throw new Meteor.Error('customOAuth: serverURL is required and must be String'); - } - - const options: StrategyOptions = { - authorizationURL: config.serverURL + config.authorizePath, - tokenURL: config.serverURL + config.tokenPath, - clientID: config.clientId, - clientSecret: config.clientSecret, - callbackURL: `${settings.get('Site_Url')}/_oauth/${name}`, - state: true, - pkce: config.pkce ?? true, - scope: config.scope, - }; - - super(options, verify); - - this.serverURL = config.serverURL; - this.tokenPath = config.tokenPath || '/oauth/token'; - this.identityPath = config.identityPath || '/me'; - this.tokenSentVia = config.tokenSentVia; - this.identityTokenSentVia = config.identityTokenSentVia; - this.keyField = config.keyField; - this.usernameField = config.usernameField?.trim(); - this.emailField = config.emailField?.trim(); - this.nameField = config.nameField?.trim(); - this.avatarField = config.avatarField?.trim(); - this.mergeUsers = !!config.mergeUsers; - this.mergeUsersDistinctServices = !!config.mergeUsersDistinctServices; - this.rolesClaim = config.rolesClaim || 'roles'; - this.accessTokenParam = config.accessTokenParam || 'access_token'; - - if (this.identityTokenSentVia == null || this.identityTokenSentVia === 'default') { - this.identityTokenSentVia = this.tokenSentVia; - } - - if (!isAbsoluteURL(this.tokenPath)) { - this.tokenPath = this.serverURL + this.tokenPath; - } - - if (!isAbsoluteURL(this.identityPath)) { - this.identityPath = this.serverURL + this.identityPath; - } - - if (this.emailPath && !isAbsoluteURL(this.emailPath)) { - this.emailPath = this.serverURL + this.emailPath; - } - - if (config.tokenSentVia === 'header') { - this._oauth2.useAuthorizationHeaderforGET(true); - } - - if (config.accessTokenParam && config.accessTokenParam !== 'access_token') { - this._oauth2.setAccessTokenName(config.accessTokenParam); - } - - if (config.addAutopublishFields && typeof config.addAutopublishFields === 'object') { - Accounts.addAutopublishFields(config.addAutopublishFields); - } - - this.name = name; - this.options = options; - this.config = config; - - this.addHookToProcessUser(); - } - - getUsername(data: Record) { - try { - const value = fromTemplate(this.usernameField, data); - - if (!value) { - logger.debug({ msg: 'Username field not found in data', usernameField: this.usernameField, data }); - throw new Meteor.Error('field_not_found', `Username field "${this.usernameField}" not found in data`); - } - - return value as string; - } catch (error) { - throw new Error('CustomOAuth: Failed to extract username', { cause: error }); - } - } - - getEmail(data: Record) { - try { - const value = fromTemplate(this.emailField, data); - - if (!value) { - logger.debug({ msg: 'Email field not found in data', emailField: this.emailField, data }); - throw new Meteor.Error('field_not_found', `Email field "${this.emailField}" not found in data`); - } - return value as string; - } catch (error) { - throw new Error('CustomOAuth: Failed to extract email', { cause: error }); - } - } - - getCustomName(data: Record) { - try { - const value = fromTemplate(this.nameField, data); - - if (!value) { - return this.getName(data); - } - - return value as string; - } catch (error) { - throw new Error('CustomOAuth: Failed to extract custom name', { cause: error }); - } - } - - getAvatarUrl(data: Record) { - try { - const value = fromTemplate(this.avatarField, data); - - if (!value) { - logger.debug({ msg: 'Avatar field not found in data', avatarField: this.avatarField, data }); - } - return value as string; - } catch (error) { - throw new Error('CustomOAuth: Failed to extract avatar url', { cause: error }); - } - } - - getName(identity: Record): string { - const name = (identity.name || - identity.username || - identity.nickname || - identity.CharacterName || - identity.userName || - identity.preferred_username || - identity.user?.name) as string; - return name; - } - - normalizeIdentity(identity: Record) { - if (identity) { - for (const normalizer of Object.values(normalizers)) { - const result = normalizer(identity); - if (result) { - identity = result; - } - } - } - - if (this.usernameField) { - identity.username = this.getUsername(identity); - } - - if (this.emailField) { - identity.email = this.getEmail(identity); - } - - if (this.avatarField) { - identity.avatarUrl = this.getAvatarUrl(identity); - } - - if (this.nameField) { - identity.name = this.getCustomName(identity); - } else { - identity.name = this.getName(identity); - } - - return renameInvalidProperties(identity); - } - - override userProfile(accessToken: string, done: DoneCallback) { - if (!this.identityPath) { - return done(new Error(`identityPath is required for ${this.name} custom oauth`)); - } - - this._oauth2.get(this.identityPath, accessToken, (err, body, res) => { - if (err) { - return done(err); - } - - if ((res && res.statusCode !== 200) || !body) { - return done(new Error(`Failed to fetch identity from ${this.name} at ${this.identityPath}}`)); - } - - try { - const result = JSON.parse(typeof body === 'string' ? body : body.toString()); - const normalizedIdentity = this.normalizeIdentity(result); - //Nextcloud URL needed on addWebdavServer - normalizedIdentity.serverURL = this.serverURL; - return done(null, normalizedIdentity); - } catch (e) { - return done(new Error(`Failed to parse identity from ${this.name} at ${this.identityPath}. ${e}`)); - } - }); - } - - addHookToProcessUser() { - BeforeUpdateOrCreateUserFromExternalService.push(async (serviceName, serviceData) => { - if (serviceName !== this.name) { - return; - } - - if (serviceData.username) { - let user: IUser | null = null; - - if (this.keyField === 'username') { - user = this.mergeUsersDistinctServices - ? await Users.findOneByUsernameIgnoringCase(serviceData.username) - : await Users.findOneByUsernameAndServiceNameIgnoringCase(serviceData.username, serviceData.id, serviceName); - } else if (this.keyField === 'email') { - user = this.mergeUsersDistinctServices - ? await Users.findOneByEmailAddress(serviceData.email) - : await Users.findOneByEmailAddressAndServiceNameIgnoringCase(serviceData.email, serviceData.id, serviceName); - } - - if (!user) { - return; - } - - await callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user }); - // User already created or merged and has identical name as before - if ( - user.services?.[serviceName as keyof NonNullable] && - user.services[serviceName as keyof NonNullable].id === serviceData.id && - user.name === serviceData.name && - (this.keyField === 'email' || !serviceData.email || user.emails?.find(({ address }) => address === serviceData.email)) - ) { - return; - } - - if (this.mergeUsers !== true) { - throw new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`); - } - - const serviceIdKey = `services.${serviceName}.id`; - const successCallbacks = [ - async () => { - const updatedUser = await Users.findOneById(user._id, { projection: { name: 1, emails: 1, [serviceIdKey]: 1 } }); - if (updatedUser) { - const { _id, ...diff } = updatedUser; - void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff }); - } - }, - ]; - - const session = client.startSession(); - try { - // Extend the session to match the ExtendedSession type expected by saveUserIdentity - Object.assign(session, { - onceSuccesfulCommit: (cb: () => Promise) => { - successCallbacks.push(cb); - }, - }); - - session.startTransaction(); - - const updater = Users.getUpdater(); - - if (this.keyField === 'username' && serviceData.email) { - updater.set('emails', [{ address: serviceData.email, verified: true }]); - } - - updater.set(serviceIdKey as keyof IUser['services'], serviceData.id); - - await saveUserIdentity({ - _id: user._id, - name: serviceData.name, - updater, - session, - updateUsernameInBackground: true, - // Username needs to be included otherwise the name won't be updated in some collections - username: user.username, - }); - await Users.updateFromUpdater({ _id: user._id }, updater, { session }); - - await session.commitTransaction(); - } catch (e) { - await session.abortTransaction(); - throw e; - } finally { - await session.endSession(); - } - - void Promise.allSettled(successCallbacks.map((cb) => cb())); - } - }); - - Accounts.validateNewUser((user: IUser & { email: string }) => { - if (!user.services?.[this.name as keyof NonNullable]?.id) { - return true; - } - - if (this.usernameField) { - user.username = user.services[this.name as keyof NonNullable].username; - } - - if (this.emailField) { - user.email = user.services[this.name as keyof NonNullable].email; - } - - if (this.nameField) { - user.name = user.services[this.name as keyof NonNullable].name; - } - - return true; - }); - } -} - -const { updateOrCreateUserFromExternalService } = Accounts; - -Accounts.updateOrCreateUserFromExternalService = async function (...args) { - for (const hook of BeforeUpdateOrCreateUserFromExternalService) { - await hook.apply(this, args as unknown as [string, Record]); - } - - const [serviceName, serviceData] = args; - - const user = await updateOrCreateUserFromExternalService.apply(this, args); - - if (!user?.userId) { - return undefined; - } - - const fullUser = await Users.findOneById(user.userId as string); - - if (!fullUser) { - return undefined; - } - - if (settings.get('LDAP_Update_Data_On_OAuth_Login') && fullUser.username) { - await LDAP.loginAuthenticatedUserRequest(fullUser.username); - } - - await callbacks.run('afterValidateNewOAuthUser', { - identity: serviceData, - serviceName, - user: fullUser, - }); - - return user; -}; diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index aea366ce4e96c..40495449e8b22 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -23,9 +23,6 @@ const logger = new Logger('CustomOAuth'); const Services = {}; const BeforeUpdateOrCreateUserFromExternalService = []; -/** - * @deprecated in favor of new Passport OAuth implementation. - */ export class CustomOAuth { constructor(name, options) { logger.debug({ msg: 'Init CustomOAuth', name, options }); diff --git a/apps/meteor/app/dolphin/server/lib.ts b/apps/meteor/app/dolphin/server/lib.ts index 77622fe43edab..826a277b81e4e 100644 --- a/apps/meteor/app/dolphin/server/lib.ts +++ b/apps/meteor/app/dolphin/server/lib.ts @@ -1,15 +1,13 @@ -import type { IUser, OAuthConfiguration } from '@rocket.chat/core-typings'; +import type { IUser } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import passport from 'passport'; -import _ from 'underscore'; import { callbacks } from '../../../server/lib/callbacks'; import { beforeCreateUserCallback } from '../../../server/lib/callbacks/beforeCreateUserCallback'; -import { addPassportCustomOAuth } from '../../../server/lib/oauth/addPassportCustomOAuth'; +import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; import { settings } from '../../settings/server'; -const config: Partial = { +const config = { serverURL: '', authorizePath: '/m/oauth2/auth/', tokenPath: '/m/oauth2/token/', @@ -22,6 +20,8 @@ const config: Partial = { accessTokenParam: 'access_token', }; +const Dolphin = new CustomOAuth('dolphin', config); + function DolphinOnCreateUser(options: any, user?: IUser) { // TODO: callbacks Fix this if (user?.services?.dolphin?.NickName) { @@ -30,32 +30,11 @@ function DolphinOnCreateUser(options: any, user?: IUser) { return options; } -const configureDolphinOAuth = () => { - passport.unuse('dolphin'); - - const enabled = settings.get('Accounts_OAuth_Dolphin'); - if (!enabled) { - return; - } - - const serverURL = settings.get('Accounts_OAuth_Dolphin_URL').trim().replace(/\/*$/, ''); - const clientId = settings.get('Accounts_OAuth_Dolphin_id'); - const clientSecret = settings.get('Accounts_OAuth_Dolphin_secret'); - - if (!clientId || !clientSecret || !serverURL) { - return; - } - - addPassportCustomOAuth('dolphin', { ...config, serverURL, clientId, clientSecret }); -}; - Meteor.startup(async () => { - const updateConfig = () => _.debounce(configureDolphinOAuth, 300); - - settings.watchMultiple( - ['Accounts_OAuth_Dolphin', 'Accounts_OAuth_Dolphin_URL', 'Accounts_OAuth_Dolphin_id', 'Accounts_OAuth_Dolphin_secret'], - updateConfig, - ); + settings.watch('Accounts_OAuth_Dolphin_URL', (value) => { + config.serverURL = value; + return Dolphin.configure(config); + }); if (settings.get('Accounts_OAuth_Dolphin_URL')) { const data = { diff --git a/apps/meteor/app/drupal/server/lib.ts b/apps/meteor/app/drupal/server/lib.ts index a4f71bd81696f..d137551fb8377 100644 --- a/apps/meteor/app/drupal/server/lib.ts +++ b/apps/meteor/app/drupal/server/lib.ts @@ -1,12 +1,14 @@ -import type { OAuthConfiguration } from '@rocket.chat/core-typings'; +import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import passport from 'passport'; -import _ from 'underscore'; -import { addPassportCustomOAuth } from '../../../server/lib/oauth/addPassportCustomOAuth'; +import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; import { settings } from '../../settings/server'; -const config: Partial = { +// Drupal Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/drupal +// In RocketChat -> Administration the URL needs to be http(s)://{drupal.server}/ + +const config: OauthConfig = { + serverURL: '', identityPath: '/oauth2/UserInfo', authorizePath: '/oauth2/authorize', tokenPath: '/oauth2/token', @@ -21,29 +23,11 @@ const config: Partial = { accessTokenParam: 'access_token', }; -const configureDrupalOAuth = () => { - passport.unuse('drupal'); - const enabled = settings.get('Accounts_OAuth_Drupal'); - if (!enabled) { - return; - } - - const serverURL = settings.get('API_Drupal_URL').trim().replace(/\/*$/, ''); - const clientId = settings.get('Accounts_OAuth_Drupal_id'); - const clientSecret = settings.get('Accounts_OAuth_Drupal_secret'); - - if (!clientId || !clientSecret || !serverURL) { - return; - } - - addPassportCustomOAuth('drupal', { ...config, serverURL, clientId, clientSecret }); -}; +const Drupal = new CustomOAuth('drupal', config); Meteor.startup(() => { - const updateConfig = _.debounce(configureDrupalOAuth, 300); - - settings.watchMultiple( - ['Accounts_OAuth_Drupal', 'API_Drupal_URL', 'Accounts_OAuth_Drupal_id', 'Accounts_OAuth_Drupal_secret'], - updateConfig, - ); + settings.watch('API_Drupal_URL', (value) => { + config.serverURL = value; + Drupal.configure(config); + }); }); diff --git a/apps/meteor/app/gitlab/server/lib.ts b/apps/meteor/app/gitlab/server/lib.ts index 3df1c1ded5496..0f4d330cf5970 100644 --- a/apps/meteor/app/gitlab/server/lib.ts +++ b/apps/meteor/app/gitlab/server/lib.ts @@ -1,12 +1,11 @@ -import type { OAuthConfiguration } from '@rocket.chat/core-typings'; +import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import passport from 'passport'; import _ from 'underscore'; -import { addPassportCustomOAuth } from '../../../server/lib/oauth/addPassportCustomOAuth'; +import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; import { settings } from '../../settings/server'; -const config: Partial = { +const config: OauthConfig = { serverURL: 'https://gitlab.com', identityPath: '/api/v4/user', scope: 'read_user', @@ -18,39 +17,15 @@ const config: Partial = { accessTokenParam: 'access_token', }; -const configureGitlabOAuth = () => { - passport.unuse('gitlab'); - - const enabled = settings.get('Accounts_OAuth_Gitlab'); - if (!enabled) { - return; - } - - const clientId = settings.get('Accounts_OAuth_Gitlab_id'); - const clientSecret = settings.get('Accounts_OAuth_Gitlab_secret'); - const serverURL = settings.get('API_Gitlab_URL').trim().replace(/\/*$/, '') || config.serverURL; - const identityPath = settings.get('Accounts_OAuth_Gitlab_identity_path') || config.identityPath; - const mergeUsers = Boolean(settings.get('Accounts_OAuth_Gitlab_merge_users')); - - if (!clientId || !clientSecret) { - return; - } - - addPassportCustomOAuth('gitlab', { ...config, clientId, clientSecret, serverURL, identityPath, mergeUsers }); -}; +const Gitlab = new CustomOAuth('gitlab', config); Meteor.startup(() => { - const updateConfig = _.debounce(configureGitlabOAuth, 300); - - settings.watchMultiple( - [ - 'Accounts_OAuth_Gitlab', - 'API_Gitlab_URL', - 'Accounts_OAuth_Gitlab_id', - 'Accounts_OAuth_Gitlab_secret', - 'Accounts_OAuth_Gitlab_identity_path', - 'Accounts_OAuth_Gitlab_merge_users', - ], - updateConfig, - ); + const updateConfig = _.debounce(() => { + config.serverURL = settings.get('API_Gitlab_URL').trim().replace(/\/*$/, '') || config.serverURL; + config.identityPath = settings.get('Accounts_OAuth_Gitlab_identity_path') || config.identityPath; + config.mergeUsers = Boolean(settings.get('Accounts_OAuth_Gitlab_merge_users')); + Gitlab.configure(config); + }, 300); + + settings.watchMultiple(['API_Gitlab_URL', 'Accounts_OAuth_Gitlab_identity_path', 'Accounts_OAuth_Gitlab_merge_users'], updateConfig); }); diff --git a/apps/meteor/app/lib/server/methods/createToken.ts b/apps/meteor/app/lib/server/methods/createToken.ts index 6e8e2538ab522..63de5b98f4210 100644 --- a/apps/meteor/app/lib/server/methods/createToken.ts +++ b/apps/meteor/app/lib/server/methods/createToken.ts @@ -16,7 +16,7 @@ export async function generateAccessToken(userId: string, secret: string) { } const token = Accounts._generateStampedLoginToken(); - await Accounts._insertLoginToken(userId, token); + Accounts._insertLoginToken(userId, token); await User.ensureLoginTokensLimit(userId); diff --git a/apps/meteor/app/linkedin/server/index.ts b/apps/meteor/app/linkedin/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/linkedin/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/linkedin/server/lib.ts b/apps/meteor/app/linkedin/server/lib.ts deleted file mode 100644 index 6fb0d450d2fda..0000000000000 --- a/apps/meteor/app/linkedin/server/lib.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { OAuthConfiguration } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import passport from 'passport'; - -import { addPassportCustomOAuth } from '../../../server/lib/oauth/addPassportCustomOAuth'; -import { settings } from '../../settings/server'; - -const config: Partial = { - serverURL: 'https://www.linkedin.com', - authorizePath: '/oauth/v2/authorization', - tokenPath: '/oauth/v2/accessToken', - identityPath: 'https://api.linkedin.com/v2/userinfo', - scope: 'openid email profile', - tokenSentVia: 'header', - addAutopublishFields: { - forLoggedInUser: ['services.linkedin'], - forOtherUsers: ['services.linkedin.name'], - }, - pkce: false, - emailField: 'email', - avatarField: 'picture', -}; - -const serviceKey = 'linkedin'; - -const configureLinkedInOAuth = (): void => { - passport.unuse(serviceKey); - - const enabled = settings.get('Accounts_OAuth_Linkedin'); - if (!enabled) { - return; - } - - const clientId = settings.get('Accounts_OAuth_Linkedin_id'); - const clientSecret = settings.get('Accounts_OAuth_Linkedin_secret'); - - if (!clientId || !clientSecret) { - return; - } - - addPassportCustomOAuth(serviceKey, { ...config, clientId, clientSecret }); -}; - -Meteor.startup(() => { - settings.watchMultiple( - ['Accounts_OAuth_Linkedin', 'Accounts_OAuth_Linkedin_id', 'Accounts_OAuth_Linkedin_secret'], - configureLinkedInOAuth, - ); -}); diff --git a/apps/meteor/app/meteor-developer/server/index.ts b/apps/meteor/app/meteor-developer/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/meteor-developer/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/meteor-developer/server/lib.ts b/apps/meteor/app/meteor-developer/server/lib.ts deleted file mode 100644 index cb532732ead92..0000000000000 --- a/apps/meteor/app/meteor-developer/server/lib.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { OAuthConfiguration } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import passport from 'passport'; - -import { addPassportCustomOAuth } from '../../../server/lib/oauth/addPassportCustomOAuth'; -import { settings } from '../../settings/server'; - -const config: Partial = { - serverURL: 'https://www.meteor.com', - authorizePath: '/oauth2/authorize', - tokenPath: '/oauth2/token', - identityPath: '/api/v1/identity', - scope: 'email', - tokenSentVia: 'header', - addAutopublishFields: { - forLoggedInUser: ['services.meteor-developer'], - forOtherUsers: ['services.meteor-developer.username'], - }, -}; - -const serviceKey = 'meteor-developer'; - -const configureMeteorDeveloperOAuth = (): void => { - passport.unuse(serviceKey); - - const enabled = settings.get('Accounts_OAuth_Meteor'); - if (!enabled) { - return; - } - - const clientId = settings.get('Accounts_OAuth_Meteor_id'); - const clientSecret = settings.get('Accounts_OAuth_Meteor_secret'); - - if (!clientId || !clientSecret) { - return; - } - - addPassportCustomOAuth(serviceKey, { ...config, clientId, clientSecret }); -}; - -Meteor.startup(() => { - settings.watchMultiple( - ['Accounts_OAuth_Meteor', 'Accounts_OAuth_Meteor_id', 'Accounts_OAuth_Meteor_secret'], - configureMeteorDeveloperOAuth, - ); -}); diff --git a/apps/meteor/app/nextcloud/server/lib.ts b/apps/meteor/app/nextcloud/server/lib.ts index fc2b592b7c1ad..28cf52da57a17 100644 --- a/apps/meteor/app/nextcloud/server/lib.ts +++ b/apps/meteor/app/nextcloud/server/lib.ts @@ -1,12 +1,14 @@ -import type { OAuthConfiguration } from '@rocket.chat/core-typings'; +import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; -import { addPassportCustomOAuth } from '../../../server/lib/oauth/addPassportCustomOAuth'; -import { settings } from '../../settings/server/cached'; +import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; +import { settings } from '../../settings/server'; -const NEXTCLOUD_PATHS = { +const config: OauthConfig = { + serverURL: '', tokenPath: '/index.php/apps/oauth2/api/v1/token', - tokenSentVia: 'header' as OAuthConfiguration['tokenSentVia'], + tokenSentVia: 'header', authorizePath: '/index.php/apps/oauth2/authorize', identityPath: '/ocs/v2.php/cloud/user?format=json', scope: 'openid', @@ -16,31 +18,20 @@ const NEXTCLOUD_PATHS = { }, }; -function configureNextcloudOAuth(): void { - const enabled = settings.get('Accounts_OAuth_Nextcloud'); - if (!enabled) { - return; - } +const Nextcloud = new CustomOAuth('nextcloud', config); - const serverURL = settings.get('Accounts_OAuth_Nextcloud_URL')?.trim().replace(/\/*$/, ''); - const clientId = settings.get('Accounts_OAuth_Nextcloud_id'); - const clientSecret = settings.get('Accounts_OAuth_Nextcloud_secret'); - - if (!serverURL || !clientId || !clientSecret) { +const fillServerURL = _.debounce((): void => { + const nextcloudURL = settings.get('Accounts_OAuth_Nextcloud_URL'); + if (!nextcloudURL) { + if (nextcloudURL === undefined) { + return fillServerURL(); + } return; } - - addPassportCustomOAuth('nextcloud', { - ...NEXTCLOUD_PATHS, - serverURL, - clientId, - clientSecret, - }); -} + config.serverURL = nextcloudURL.trim().replace(/\/*$/, ''); + return Nextcloud.configure(config); +}, 1000); Meteor.startup(() => { - settings.watchMultiple( - ['Accounts_OAuth_Nextcloud', 'Accounts_OAuth_Nextcloud_URL', 'Accounts_OAuth_Nextcloud_id', 'Accounts_OAuth_Nextcloud_secret'], - configureNextcloudOAuth, - ); + settings.watch('Accounts_OAuth_Nextcloud_URL', () => fillServerURL()); }); diff --git a/apps/meteor/app/wordpress/server/lib.ts b/apps/meteor/app/wordpress/server/lib.ts index eb1ce2c01ceb2..7777ea1382215 100644 --- a/apps/meteor/app/wordpress/server/lib.ts +++ b/apps/meteor/app/wordpress/server/lib.ts @@ -1,13 +1,12 @@ -import type { OAuthConfiguration } from '@rocket.chat/core-typings'; +import type { OauthConfig } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import passport from 'passport'; import _ from 'underscore'; -import { addPassportCustomOAuth } from '../../../server/lib/oauth/addPassportCustomOAuth'; +import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; import { settings } from '../../settings/server'; -const config: Partial = { +const config: OauthConfig = { serverURL: '', identityPath: '/oauth/me', @@ -18,7 +17,7 @@ const config: Partial = { accessTokenParam: 'access_token', }; -const serviceKey = 'wordpress'; +const WordPress = new CustomOAuth('wordpress', config); const fillSettings = _.debounce(async (): Promise => { config.serverURL = settings.get('API_Wordpress_URL'); @@ -29,7 +28,11 @@ const fillSettings = _.debounce(async (): Promise => { return; } - passport.unuse(serviceKey); + delete config.identityPath; + delete config.identityTokenSentVia; + delete config.authorizePath; + delete config.tokenPath; + delete config.scope; const serverType = settings.get('Accounts_OAuth_Wordpress_server_type'); switch (serverType) { @@ -56,7 +59,7 @@ const fillSettings = _.debounce(async (): Promise => { break; case 'wordpress-com': config.identityPath = 'https://public-api.wordpress.com/rest/v1/me'; - config.identityTokenSentVia = 'header' as OAuthConfiguration['identityTokenSentVia']; + config.identityTokenSentVia = 'header'; config.authorizePath = 'https://public-api.wordpress.com/oauth2/authorize'; config.tokenPath = 'https://public-api.wordpress.com/oauth2/token'; config.scope = 'auth'; @@ -66,13 +69,12 @@ const fillSettings = _.debounce(async (): Promise => { break; } - addPassportCustomOAuth(serviceKey, config); - + const result = WordPress.configure(config); const enabled = settings.get('Accounts_OAuth_Wordpress'); if (enabled) { await ServiceConfiguration.configurations.upsertAsync( { - service: serviceKey, + service: 'wordpress', }, { $set: config, @@ -80,9 +82,11 @@ const fillSettings = _.debounce(async (): Promise => { ); } else { await ServiceConfiguration.configurations.removeAsync({ - service: serviceKey, + service: 'wordpress', }); } + + return result; }, 1000); Meteor.startup(() => { diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx index 14f86a832e075..c80fb09e78ae1 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorEmailModal.tsx @@ -1,7 +1,7 @@ import { Box, Button } from '@rocket.chat/fuselage'; import { FieldGroup, TextInput, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage-forms'; import { GenericModal } from '@rocket.chat/ui-client'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -12,14 +12,14 @@ import { Method } from './TwoFactorModal'; type TwoFactorEmailModalProps = { onConfirm: OnConfirm; onClose: () => void; - resendEmail?: () => Promise; + emailOrUsername: string; }; type TwoFactorEmailFormData = { code: string; }; -const TwoFactorEmailModal = ({ onConfirm, onClose, resendEmail }: TwoFactorEmailModalProps): ReactElement => { +const TwoFactorEmailModal = ({ onConfirm, onClose, emailOrUsername }: TwoFactorEmailModalProps): ReactElement => { const dispatchToastMessage = useToastMessageDispatch(); const { t } = useTranslation(); @@ -33,12 +33,11 @@ const TwoFactorEmailModal = ({ onConfirm, onClose, resendEmail }: TwoFactorEmail defaultValues: { code: '' }, }); + const sendEmailCode = useEndpoint('POST', '/v1/users.2fa.sendEmailCode'); + const onClickResendCode = async (): Promise => { try { - if (!resendEmail) { - throw new Error('resendEmail is not defined'); - } - await resendEmail(); + await sendEmailCode({ emailOrUsername }); dispatchToastMessage({ type: 'success', message: t('Email_sent') }); } catch (error) { dispatchToastMessage({ diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx index 04f603882c531..ef056ee717ccb 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx @@ -21,7 +21,7 @@ type TwoFactorModalProps = { } | { method: 'email'; - resendEmail?: () => Promise; + emailOrUsername: string; } ); @@ -31,9 +31,9 @@ const TwoFactorModal = ({ onConfirm, onClose, ...props }: TwoFactorModalProps): } if (props.method === Method.EMAIL) { - const { resendEmail } = props; + const { emailOrUsername } = props; - return ; + return ; } if (props.method === Method.PASSWORD) { diff --git a/apps/meteor/client/lib/2fa/process2faReturn.ts b/apps/meteor/client/lib/2fa/process2faReturn.ts index 7bdac41bc1f50..50a0d729d770b 100644 --- a/apps/meteor/client/lib/2fa/process2faReturn.ts +++ b/apps/meteor/client/lib/2fa/process2faReturn.ts @@ -5,7 +5,6 @@ import { lazy } from 'react'; import type { LoginCallback } from './overrideLoginMethod'; import type { MeteorErrorLike } from './types'; import { isTotpInvalidError, isTotpRequiredError } from './utils'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { getUser } from '../user'; const TwoFactorModal = lazy(() => import('../../components/TwoFactorModal')); @@ -32,9 +31,7 @@ const hasRequiredTwoFactorMethod = ( function assertModalProps(props: { method: TwoFactorMethod; emailOrUsername?: string; -}): asserts props is - | { method: 'totp' | 'password'; invalidAttempt?: boolean } - | { method: 'email'; emailOrUsername: string; invalidAttempt?: boolean } { +}): asserts props is { method: 'totp' } | { method: 'password' } | { method: 'email'; emailOrUsername: string } { if (props.method === 'email' && typeof props.emailOrUsername !== 'string') { throw new Error('Invalid Two Factor method'); } @@ -164,10 +161,6 @@ export const invokeTwoFactorModal = async ( reject(new Error('totp-canceled')); } }, - ...(props.method === 'email' && - props.emailOrUsername && { - resendEmail: (): Promise => sdk.rest.post('/v1/users.2fa.sendEmailCode', { emailOrUsername: props.emailOrUsername }), - }), }, }); }); diff --git a/apps/meteor/client/lib/buildAuthDeeplinkURL.ts b/apps/meteor/client/lib/buildAuthDeeplinkURL.ts deleted file mode 100644 index e4481fed271d3..0000000000000 --- a/apps/meteor/client/lib/buildAuthDeeplinkURL.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const buildDeepLinkURL = (resumeToken: string, userId: string) => { - const url = new URL(window.location.href); - const { origin } = url; - return `rocketchat://auth?host=${origin}&token=${resumeToken}&userId=${userId}`; -}; diff --git a/apps/meteor/client/lib/sdk/ddpSdk.ts b/apps/meteor/client/lib/sdk/ddpSdk.ts index 09c2b43ac10a7..35ed4169e29e3 100644 --- a/apps/meteor/client/lib/sdk/ddpSdk.ts +++ b/apps/meteor/client/lib/sdk/ddpSdk.ts @@ -68,7 +68,7 @@ export const getDdpSdk = (): DDPSDK => { return instance; }; -export const readStoredLoginToken = (): string | null => getStoredItem(STORAGE_KEYS.LOGIN_TOKEN); +const readStoredLoginToken = (): string | null => getStoredItem(STORAGE_KEYS.LOGIN_TOKEN); let inflightLogin: Promise | undefined; diff --git a/apps/meteor/client/startup/routes.tsx b/apps/meteor/client/startup/routes.tsx index 2b8e1c43e35e2..77f76158cb65d 100644 --- a/apps/meteor/client/startup/routes.tsx +++ b/apps/meteor/client/startup/routes.tsx @@ -3,7 +3,6 @@ import { createElement, lazy, useEffect } from 'react'; import { appLayout } from '../lib/appLayout'; import { router } from '../providers/RouterProvider'; -import OAuthTwoFactorAuthenticationRouter from '../views/OAuthTwoFactorAuthentication/OAuthTwoFactorAuthenticationRouter'; import MainLayout from '../views/root/MainLayout'; const IndexRoute = lazy(() => import('../views/root/IndexRoute')); @@ -40,10 +39,6 @@ declare module '@rocket.chat/ui-contexts' { pathname: `/meet/${string}`; pattern: '/meet/:rid'; }; - '2fa': { - pathname: `/2fa/${string}/${string}`; - pattern: '/2fa/:method/:challengeId'; - }; 'home': { pathname: '/home'; pattern: '/home'; @@ -136,11 +131,6 @@ router.defineRoutes([ return null; }), }, - { - path: '/2fa/:method/:challengeId', - id: '2fa', - element: appLayout.wrap(), - }, { path: '/home', id: 'home', diff --git a/apps/meteor/client/views/OAuthTwoFactorAuthentication/OAuthTwoFactorAuthenticationRouter.tsx b/apps/meteor/client/views/OAuthTwoFactorAuthentication/OAuthTwoFactorAuthenticationRouter.tsx deleted file mode 100644 index 2394d0a8cfdaf..0000000000000 --- a/apps/meteor/client/views/OAuthTwoFactorAuthentication/OAuthTwoFactorAuthenticationRouter.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useStableCallback } from '@rocket.chat/fuselage-hooks'; -import { Page } from '@rocket.chat/ui-client'; -import { - useEndpoint, - useLoginWithToken, - useRouteParameter, - useRouter, - useSetModal, - useToastMessageDispatch, -} from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; - -import TwoFactorModal from '../../components/TwoFactorModal/TwoFactorModal'; - -const throwErrorOnInvalidMethod = (method: never): never => { - throw new Error(`Invalid Two Factor method: ${method}`); -}; - -const OAuthTwoFactorAuthenticationRouter = () => { - const method = useRouteParameter('method') as 'totp' | 'email' | undefined; - const challengeId = useRouteParameter('challengeId'); - const router = useRouter(); - const dispatchToastMessage = useToastMessageDispatch(); - const setModal = useSetModal(); - const loginWithToken = useLoginWithToken(); - const { t } = useTranslation(); - const verifyChallenge = useEndpoint('POST', '/v1/twoFactorChallenges.verifyChallenge'); - const sendEmailCode = useEndpoint('POST', '/v1/twoFactorChallenges.sendEmailCode'); - - const navigateToHome = useStableCallback(() => { - setModal(null); - router.navigate('/home', { replace: true }); - }); - - const resendEmail = useStableCallback(async () => { - if (!challengeId) { - return null; - } - await sendEmailCode({ challengeId }); - return null; - }); - - const onConfirm = useStableCallback(async (code: string) => { - if (!challengeId || !code) { - return; - } - try { - const { loginToken, userId } = await verifyChallenge({ challengeId, code }); - - const { loginClient } = router.getSearchParameters(); - - if (loginClient === 'mobile' || loginClient === 'desktop') { - setModal(null); - router.navigate({ name: 'home', search: { resumeToken: loginToken, userId, loginClient } }, { replace: true }); - return; - } - - await loginWithToken(loginToken); - navigateToHome(); - } catch (error: any) { - console.error('Failed to verify challenge', error); - if (error.errorType === 'totp-max-attempts') { - setModal(null); - dispatchToastMessage({ type: 'error', message: t('Maximum_number_of_attempts_reached_please_try_again_later') }); - router.navigate('/login', { replace: true }); - return; - } - if (error.errorType === 'error-challenge-expired' || error.errorType === 'error-challenge-not-found') { - setModal(null); - dispatchToastMessage({ type: 'error', message: t('Challenge_expired_please_try_again_later') }); - router.navigate('/login', { replace: true }); - return; - } - throw error; - } - }); - - useEffect(() => { - if (!method || !challengeId) { - router.navigate('/home'); - return; - } - - if (method === 'email') { - setModal(); - return; - } - - if (method === 'totp') { - setModal(); - return; - } - - throwErrorOnInvalidMethod(method); - }, [method, challengeId, router, setModal, onConfirm, resendEmail, navigateToHome]); - - return ; -}; - -export default OAuthTwoFactorAuthenticationRouter; diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 250690e98d78f..b355d9c7b44f2 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -27,13 +27,11 @@ import { useKeyboardShortcutsHotkey } from './hooks/useKeyboardShortcutsHotkey'; import { useLivechatEnterprise } from './hooks/useLivechatEnterprise'; import { useLoadMissedMessages } from './hooks/useLoadMissedMessages'; import { useLoadRoomForAllowedAnonymousRead } from './hooks/useLoadRoomForAllowedAnonymousRead'; -import { useLoginOtherClients } from './hooks/useLoginOtherClients'; import { useLoginViaQuery } from './hooks/useLoginViaQuery'; import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; import { useNotificationPermission } from './hooks/useNotificationPermission'; import { useRedirectToSetupWizard } from './hooks/useRedirectToSetupWizard'; import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; -import { useShareSessionWithOtherClients } from './hooks/useShareSessionWithOtherClients'; import { useStartupEvent } from './hooks/useStartupEvent'; import { appLayout } from '../../lib/appLayout'; @@ -72,8 +70,6 @@ const AppLayout = () => { useAutoupdate(); useCodeHighlight(); useLoginViaQuery(); - useLoginOtherClients(); - useShareSessionWithOtherClients(); useLoadMissedMessages(); useDesktopFavicon(); useDesktopTitle(); diff --git a/apps/meteor/client/views/root/hooks/useLoginOtherClients.ts b/apps/meteor/client/views/root/hooks/useLoginOtherClients.ts deleted file mode 100644 index a826091886772..0000000000000 --- a/apps/meteor/client/views/root/hooks/useLoginOtherClients.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useRouter, useSearchParameter } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; - -import { buildDeepLinkURL } from '../../../lib/buildAuthDeeplinkURL'; - -export const useLoginOtherClients = () => { - const router = useRouter(); - const resumeToken = useSearchParameter('resumeToken'); - const loginClient = useSearchParameter('loginClient'); - const userId = useSearchParameter('userId'); - - useEffect(() => { - if (!resumeToken || !userId) { - return; - } - - if (loginClient !== 'desktop' && loginClient !== 'mobile') { - return; - } - - const loginURL = buildDeepLinkURL(resumeToken, userId); - window.location.href = loginURL; - - const timeout = setTimeout(() => { - router.navigate('/home', { replace: true }); - }, 0); - - return () => clearTimeout(timeout); - }, [resumeToken, userId, loginClient, router]); -}; diff --git a/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts b/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts index 7bc3b2b93ecb0..a67739eb7b025 100644 --- a/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts +++ b/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts @@ -7,17 +7,12 @@ export const useLoginViaQuery = () => { useEffect(() => { const handleLogin = async () => { - const { resumeToken, loginClient } = router.getSearchParameters(); + const { resumeToken } = router.getSearchParameters(); if (!resumeToken) { return; } - //Case handled by useLoginOtherClients, we don't want to login here. - if (loginClient) { - return; - } - try { await loginWithToken(resumeToken); diff --git a/apps/meteor/client/views/root/hooks/useShareSessionWithOtherClients.ts b/apps/meteor/client/views/root/hooks/useShareSessionWithOtherClients.ts deleted file mode 100644 index 62fd2fee04818..0000000000000 --- a/apps/meteor/client/views/root/hooks/useShareSessionWithOtherClients.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useRouter, useSearchParameter, useUserId } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; - -import { buildDeepLinkURL } from '../../../lib/buildAuthDeeplinkURL'; -import { readStoredLoginToken } from '../../../lib/sdk/ddpSdk'; - -export const useShareSessionWithOtherClients = () => { - const router = useRouter(); - const userId = useUserId(); - const resumeToken = useSearchParameter('resumeToken'); - const loginClient = useSearchParameter('loginClient'); - - useEffect(() => { - if (!userId) { - return; - } - - const loginToken = readStoredLoginToken(); - - if (!loginToken) { - return; - } - - if (resumeToken) { - return; - } - - if (loginClient !== 'desktop' && loginClient !== 'mobile') { - return; - } - - const loginURL = buildDeepLinkURL(loginToken, userId); - window.location.href = loginURL; - - const timeout = setTimeout(() => { - router.navigate('/home', { replace: true }); - }, 100); - - return () => clearTimeout(timeout); - }, [resumeToken, loginClient, router, userId]); -}; diff --git a/apps/meteor/definition/externals/express-session.d.ts b/apps/meteor/definition/externals/express-session.d.ts deleted file mode 100644 index 6a59655d241a7..0000000000000 --- a/apps/meteor/definition/externals/express-session.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import 'express-session'; - -declare module 'express-session' { - interface SessionData { - loginClient?: string; - } -} diff --git a/apps/meteor/definition/externals/express.d.ts b/apps/meteor/definition/externals/express.d.ts index 251d8310daf9f..1661385d94f77 100644 --- a/apps/meteor/definition/externals/express.d.ts +++ b/apps/meteor/definition/externals/express.d.ts @@ -9,9 +9,3 @@ declare module 'express' { unauthorized?: boolean; } } -declare global { - namespace Express { - // eslint-disable-next-line @typescript-eslint/no-empty-interface -- merges with Passport's Express.User - interface User extends IUser {} - } -} diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 9676c1c6d8678..39ea55788e9a7 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -23,7 +23,7 @@ declare module 'meteor/accounts-base' { function _generateStampedLoginToken(): { token: string; when: Date }; - function _insertLoginToken(userId: string, token: { token: string; when: Date }): Promise; + function _insertLoginToken(userId: string, token: { token: string; when: Date }): void; function _runLoginHandlers(methodInvocation: T, loginRequest: Record): Promise; @@ -41,9 +41,7 @@ declare module 'meteor/accounts-base' { serviceName: string, serviceData: Record, options: Record, - ): Promise | undefined>; - - function addAutopublishFields(options: Record): void; + ): Record; function _clearAllLoginTokens(userId: string | null): void; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 4426e80fa582f..3728e84a89348 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -183,7 +183,6 @@ "colorette": "^2.0.20", "colors": "^1.4.0", "connect": "^3.7.0", - "connect-mongo": "5.1.0", "cookie": "^0.7.2", "cookie-parser": "^1.4.7", "cors": "^2.8.6", @@ -206,7 +205,6 @@ "expiry-map": "^2.0.0", "express": "^4.21.2", "express-rate-limit": "^5.5.1", - "express-session": "^1.19.0", "fast-redact": "^3.5.0", "fastq": "^1.17.1", "fflate": "^0.8.2", @@ -263,13 +261,6 @@ "object-path": "^0.11.8", "overlayscrollbars": "~2.11.5", "overlayscrollbars-react": "^0.5.6", - "passport": "^0.7.0", - "passport-apple": "^2.0.2", - "passport-facebook": "^3.0.0", - "passport-github2": "^0.1.12", - "passport-google-oauth20": "^2.0.0", - "passport-oauth2": "^1.8.0", - "passport-twitter": "^1.0.4", "path": "^0.12.7", "path-to-regexp": "^6.3.0", "pino": "10.3.1", @@ -365,7 +356,6 @@ "@types/ejson": "^2.2.2", "@types/express": "^4.17.25", "@types/express-rate-limit": "^5.1.3", - "@types/express-session": "^1", "@types/fast-redact": "^3", "@types/google-libphonenumber": "^7.4.30", "@types/gravatar": "^1.8.6", @@ -396,13 +386,6 @@ "@types/oauth2-server": "^3.0.18", "@types/object-path": "^0.11.4", "@types/parseurl": "^1.3.3", - "@types/passport": "^1.0.17", - "@types/passport-apple": "^2", - "@types/passport-facebook": "^3.0.4", - "@types/passport-github2": "^1.2.9", - "@types/passport-google-oauth20": "^2", - "@types/passport-oauth2": "^1", - "@types/passport-twitter": "^1", "@types/prometheus-gc-stats": "^0.6.4", "@types/proxy-from-env": "^1.0.4", "@types/proxyquire": "^1.3.31", diff --git a/apps/meteor/server/configuration/accounts_meld.js b/apps/meteor/server/configuration/accounts_meld.js index 2c8fff11a820b..037150def37ee 100644 --- a/apps/meteor/server/configuration/accounts_meld.js +++ b/apps/meteor/server/configuration/accounts_meld.js @@ -18,6 +18,10 @@ export async function configureAccounts() { } } + if (serviceName === 'linkedin') { + serviceData.email = serviceData.emailAddress; + } + if (serviceData.email) { const user = await Users.findOneByEmailAddress(serviceData.email); if (user != null && user.services?.[serviceName]?.id !== serviceData.id) { diff --git a/apps/meteor/server/configuration/configurePassport.ts b/apps/meteor/server/configuration/configurePassport.ts deleted file mode 100644 index 46adff48c78de..0000000000000 --- a/apps/meteor/server/configuration/configurePassport.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Users } from '@rocket.chat/models'; -import bodyParser from 'body-parser'; -import MongoStore from 'connect-mongo'; -import express from 'express'; -import rateLimit from 'express-rate-limit'; -import session from 'express-session'; -import { MongoInternals } from 'meteor/mongo'; -import { WebApp } from 'meteor/webapp'; -import passport from 'passport'; - -import type { ICachedSettings } from '../../app/settings/server/CachedSettings'; -import { configureOAuthServices } from '../lib/oauth/configureOAuthServices'; -import { createOAuthServiceConfig } from '../lib/oauth/createOAuthServiceConfig'; -import { getOAuthServices } from '../lib/oauth/getOAuthServices'; - -const oAuthPaths = ['/oauth', '/_oauth']; - -const { Router: router } = express; - -export const oAuthRouter = router(); - -const oAuthApp = express(); -oAuthApp.set('trust proxy', true); - -export const configurePassport = (settings: ICachedSettings) => { - const { client } = MongoInternals.defaultRemoteCollectionDriver().mongo; - - oAuthApp.use( - oAuthPaths, - session({ - name: 'oauth', - secret: settings.get('Accounts_OAuth_Session_Secret'), - resave: false, - saveUninitialized: false, - proxy: true, - store: MongoStore.create({ - client, - collectionName: 'rocketchat_oauth_sessions', - ttl: 5 * 60, - autoRemove: 'native', - }), - cookie: { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 5 * 60 * 1000, // 5 minutes - sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax', - }, - }), - ); - - oAuthApp.use(oAuthPaths, passport.initialize()); - oAuthApp.use(oAuthPaths, passport.session()); - oAuthApp.use(oAuthPaths, bodyParser.urlencoded({ extended: true })); - - const oauthRateLimiter = rateLimit({ - windowMs: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), - max: settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default'), - skip: () => - process.env.TEST_MODE === 'true' || - process.env.TEST_MODE === 'api' || - settings.get('API_Enable_Rate_Limiter') !== true || - (process.env.NODE_ENV === 'development' && settings.get('API_Enable_Rate_Limiter_Dev') !== true), - handler: (_req, res) => { - res.status(429).json({ - success: false, - error: 'Too many requests. Please try again later.', - }); - }, - }); - - oAuthRouter.use(oAuthPaths, oauthRateLimiter); - - // Register OAuth Routes - oAuthApp.use(oAuthRouter); - - passport.serializeUser((user: any, done) => { - done(null, user._id); - }); - - passport.deserializeUser(async (id, done) => { - const user = await Users.findOneById(id as string); - // we don’t actually use this user later - done(null, user); - }); - - settings.watchByRegex(/^(Accounts_OAuth_)[a-z0-9_]+$/i, () => { - const services = getOAuthServices(settings); - const oauthServiceConfigs = createOAuthServiceConfig(settings, services); - configureOAuthServices(oauthServiceConfigs, settings); - }); - - WebApp.rawConnectHandlers.use(oAuthApp); -}; diff --git a/apps/meteor/server/configuration/index.ts b/apps/meteor/server/configuration/index.ts index 79f3160120ba1..491c410e26f4a 100644 --- a/apps/meteor/server/configuration/index.ts +++ b/apps/meteor/server/configuration/index.ts @@ -7,7 +7,6 @@ import { configureCORS } from './configureCORS'; import { configureDirectReply } from './configureDirectReply'; import { configureIRC } from './configureIRC'; import { configureLogLevel } from './configureLogLevel'; -import { configurePassport } from './configurePassport'; import { configureSMTP } from './configureSMTP'; import { configureLDAP } from './ldap'; import { configureOAuth } from './oauth'; @@ -29,6 +28,5 @@ export async function configureServer(settings: ICachedSettings) { configureDirectReply(settings), configureSMTP(settings), configureIRC(settings), - configurePassport(settings), ]); } diff --git a/apps/meteor/server/importPackages.ts b/apps/meteor/server/importPackages.ts index 228d809fe6dc2..645ff19932e0a 100644 --- a/apps/meteor/server/importPackages.ts +++ b/apps/meteor/server/importPackages.ts @@ -33,8 +33,6 @@ import '../app/importer-slack-users/server'; import '../app/integrations/server'; import '../app/irc/server'; import '../app/lib/server'; -import '../app/meteor-developer/server'; -import '../app/linkedin/server'; import '../app/token-login/server'; import '../app/mailer/server/api'; import '../app/markdown/server'; diff --git a/apps/meteor/server/lib/oauth/addPassportCustomOAuth.ts b/apps/meteor/server/lib/oauth/addPassportCustomOAuth.ts deleted file mode 100644 index e26f51bdf9c72..0000000000000 --- a/apps/meteor/server/lib/oauth/addPassportCustomOAuth.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { OAuthConfiguration } from '@rocket.chat/core-typings'; -import passport from 'passport'; -import type { DoneCallback, Profile } from 'passport'; - -import { passportOAuthCallback } from './passportOAuthCallback'; -import { verifyFunction } from './verifyFunction'; -import { CustomOAuthStrategy } from '../../../app/custom-oauth/server/customOAuth'; -import { settings } from '../../../app/settings/server'; -import { oAuthRouter } from '../../configuration/configurePassport'; - -export const addPassportCustomOAuth = (serviceName: string, config: Partial) => { - passport.unuse(serviceName); - - if (!config.clientId || !config.clientSecret || !config.serverURL) { - return; - } - - passport.use( - serviceName, - new CustomOAuthStrategy( - serviceName, - config as OAuthConfiguration & { clientSecret: string }, - (accessToken: string, refreshToken: string, profile: Profile, done: DoneCallback) => - verifyFunction(accessToken, refreshToken, profile, done, serviceName), - ), - ); - - const siteUrl = settings.get('Site_Url'); - - oAuthRouter.get( - `/oauth/${serviceName}`, - (req, _res, next) => { - const { loginClient } = req.query; - if (loginClient === 'mobile' || loginClient === 'desktop') { - req.session.loginClient = loginClient; - req.session.save(() => { - next(); - }); - } else { - //delete stale value from previous sessions if any - delete req.session.loginClient; - next(); - } - }, - passport.authenticate(serviceName, { scope: config.scope, prompt: 'consent', failureRedirect: '/login', keepSessionInfo: true }), - ); - - oAuthRouter.get( - `/_oauth/${serviceName}`, - passport.authenticate(serviceName, { failureRedirect: '/login', failureFlash: true, failWithError: true, keepSessionInfo: true }), - passportOAuthCallback(siteUrl), - ); -}; diff --git a/apps/meteor/server/lib/oauth/configureOAuthServices.ts b/apps/meteor/server/lib/oauth/configureOAuthServices.ts deleted file mode 100644 index 5ff0b202a2e89..0000000000000 --- a/apps/meteor/server/lib/oauth/configureOAuthServices.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Users } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import passport from 'passport'; -import type { Profile, DoneCallback } from 'passport'; - -import type { OAuthServiceConfig } from './createOAuthServiceConfig'; -import { passportOAuthCallback } from './passportOAuthCallback'; -import type { ICachedSettings } from '../../../app/settings/server/CachedSettings'; -import { oAuthRouter } from '../../configuration/configurePassport'; - -export const configureOAuthServices = (oauthServiceConfig: OAuthServiceConfig[], settings: ICachedSettings) => { - oauthServiceConfig.forEach((config) => { - const Strategy = config.strategy; - const siteUrl = settings.get('Site_Url'); - - passport.unuse(config.provider); - - passport.use( - config.provider, - new Strategy( - { - ...config, - clientID: config.clientId, - clientSecret: config.clientSecret, - consumerKey: config.clientId, - consumerSecret: config.clientSecret, - callbackURL: `${siteUrl}/_oauth/${config.provider}`, - state: true, - pkce: true, - profileFields: ['id', 'displayName', 'emails'], - }, - async (accessToken: string, refreshToken: string, profile: Profile, done: DoneCallback) => { - const profileWithRaw = profile as Profile & { _json?: Record; _raw?: string }; - const { _json, _raw, ...restProfile } = profileWithRaw; - - const user = await Accounts.updateOrCreateUserFromExternalService( - config.provider, - { - accessToken, - refreshToken, - name: profile.displayName, - ...restProfile, - ..._json, - email: profile?.emails?.[0]?.value, - }, - {}, - ); - - if (!user?.userId || typeof user?.userId !== 'string') { - return done(new Error('User not found')); - } - - const userFromDB = await Users.findOneById(user.userId); - - if (!userFromDB) { - return done(new Error('User not found')); - } - - return done(null, userFromDB); - }, - ), - ); - - oAuthRouter.get( - `/oauth/${config.provider}`, - (req, _res, next) => { - const { loginClient } = req.query; - if (loginClient === 'mobile' || loginClient === 'desktop') { - req.session.loginClient = loginClient; - req.session.save(() => { - next(); - }); - } else { - //delete stale value from previous sessions if any - delete req.session.loginClient; - next(); - } - }, - passport.authenticate(config.provider, { scope: config.scope, prompt: 'consent', failureRedirect: '/login', keepSessionInfo: true }), - ); - oAuthRouter.get( - `/_oauth/${config.provider}`, - passport.authenticate(config.provider, { failureRedirect: '/login', failureFlash: true, failWithError: true, keepSessionInfo: true }), - passportOAuthCallback(siteUrl), - ); - }); -}; diff --git a/apps/meteor/server/lib/oauth/createOAuthServiceConfig.ts b/apps/meteor/server/lib/oauth/createOAuthServiceConfig.ts deleted file mode 100644 index 3ea8213140b95..0000000000000 --- a/apps/meteor/server/lib/oauth/createOAuthServiceConfig.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { capitalize } from '@rocket.chat/string-helpers'; -import { isTruthy } from '@rocket.chat/tools'; -import type { Strategy } from 'passport'; - -import { OAuthConfigs } from './oauthConfigs'; -import { type ICachedSettings } from '../../../app/settings/server/CachedSettings'; - -export type OAuthServiceConfig = { - provider: string; - strategy: new (...args: any[]) => Strategy; - clientId: string; - clientSecret: string; - scope?: string[]; -}; - -export const createOAuthServiceConfig = (settings: ICachedSettings, services: string[]): OAuthServiceConfig[] => { - return services - .map((service) => { - if (!OAuthConfigs[service]) { - return; - } - - if (service === 'github_enterprise') { - const clientId = settings.get('Accounts_OAuth_GitHub_Enterprise_id'); - const clientSecret = settings.get('Accounts_OAuth_GitHub_Enterprise_secret'); - const serverUrl = settings.get('API_GitHub_Enterprise_URL'); - - if (!clientId || !clientSecret || !serverUrl) { - return; - } - - return { - provider: service, - clientId, - clientSecret, - authorizationURL: `${serverUrl}/login/oauth/authorize`, - tokenURL: `${serverUrl}/login/oauth/access_token`, - userProfileURL: `${serverUrl}/api/v3/user`, - strategy: OAuthConfigs.github_enterprise.strategy, - scope: OAuthConfigs.github_enterprise.scope, - }; - } - - const clientId = settings.get(`Accounts_OAuth_${capitalize(service)}_id`); - const clientSecret = settings.get(`Accounts_OAuth_${capitalize(service)}_secret`); - - if (!clientId || !clientSecret) { - return; - } - - return { - provider: service, - clientId, - clientSecret, - ...OAuthConfigs[service], - }; - }) - .filter(isTruthy); -}; diff --git a/apps/meteor/server/lib/oauth/getOAuthServices.ts b/apps/meteor/server/lib/oauth/getOAuthServices.ts deleted file mode 100644 index 7a431982d9f71..0000000000000 --- a/apps/meteor/server/lib/oauth/getOAuthServices.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { isTruthy } from '@rocket.chat/tools'; - -import { OAuthConfigs } from './oauthConfigs'; -import { type ICachedSettings } from '../../../app/settings/server/CachedSettings'; - -export const getOAuthServices = (settings: ICachedSettings) => { - const services = settings.getByRegexp(/^(Accounts_OAuth_|Accounts_OAuth_Custom-)[a-z0-9_]+$/i); - const filteredServices = services.filter(([, value]) => typeof value === 'boolean' && value === true); - return filteredServices - .map(([key, value]) => { - if (!value) { - return; - } - - let serviceName = key.replace('Accounts_OAuth_', ''); - if (serviceName === 'Meteor') { - serviceName = 'meteor-developer'; - } - - if (/Accounts_OAuth_Custom-/.test(key)) { - return; - } - - const serviceKey = serviceName.toLowerCase(); - - const oauthConfig = OAuthConfigs[serviceKey]; - if (!oauthConfig) { - return; - } - - return serviceKey; - }) - .filter(isTruthy); -}; diff --git a/apps/meteor/server/lib/oauth/oauthConfigs.ts b/apps/meteor/server/lib/oauth/oauthConfigs.ts deleted file mode 100644 index b4c8fe6604fa5..0000000000000 --- a/apps/meteor/server/lib/oauth/oauthConfigs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Strategy } from 'passport'; -import { Strategy as FacebookStrategy } from 'passport-facebook'; -import { Strategy as GitHubStrategy } from 'passport-github2'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import { Strategy as TwitterStrategy } from 'passport-twitter'; - -export type OAuthConfig = { - strategy: new (...args: any[]) => Strategy; - scope?: string[]; - includeEmail?: boolean; -}; - -export const OAuthConfigs: Record = { - github: { - strategy: GitHubStrategy, - scope: ['user:email'], - }, - facebook: { - strategy: FacebookStrategy, - scope: ['email'], - }, - google: { - strategy: GoogleStrategy, - scope: ['email', 'profile'], - }, - twitter: { - strategy: TwitterStrategy, - includeEmail: true, - }, - github_enterprise: { - strategy: GitHubStrategy, - scope: ['user:email'], - }, -} as const; - -export type Provider = keyof typeof OAuthConfigs; diff --git a/apps/meteor/server/lib/oauth/passportOAuthCallback.ts b/apps/meteor/server/lib/oauth/passportOAuthCallback.ts deleted file mode 100644 index 7bd128f6ebf7a..0000000000000 --- a/apps/meteor/server/lib/oauth/passportOAuthCallback.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import type { Request, Response } from 'express'; -import { Accounts } from 'meteor/accounts-base'; - -import { doesUserRequire2FA } from './twoFactorAuth'; - -const logger = new Logger('OAuth'); - -export const passportOAuthCallback = (siteUrl: string) => async (req: Request, res: Response) => { - const oAuthUser = req.user as IUser; - - if (!oAuthUser) { - return res.redirect('/login'); - } - - const { loginClient } = req.session; - - const secondFactorMethod = doesUserRequire2FA(oAuthUser); - - if (secondFactorMethod) { - const challengeId = await secondFactorMethod.sendTwoFactorChallenge(oAuthUser); - const twoFARedirectUrl = new URL(`/2fa/${secondFactorMethod.method}/${challengeId}`, siteUrl); - - if (loginClient) { - twoFARedirectUrl.searchParams.set('loginClient', loginClient); - } - - return res.redirect(twoFARedirectUrl.toString()); - } - - const stampedToken = Accounts._generateStampedLoginToken(); - await Accounts._insertLoginToken(oAuthUser._id, stampedToken); - - const redirectUrl = new URL(`/home`, siteUrl); - - redirectUrl.searchParams.set('resumeToken', stampedToken.token); - redirectUrl.searchParams.set('userId', oAuthUser._id); - - if (loginClient) { - redirectUrl.searchParams.set('loginClient', loginClient); - } - - setImmediate(() => res.redirect(redirectUrl.toString())); - - req.session.destroy((err) => { - if (err) { - logger.error({ msg: 'Error destroying OAuth session', err }); - } - }); -}; diff --git a/apps/meteor/server/lib/oauth/twoFactorAuth.ts b/apps/meteor/server/lib/oauth/twoFactorAuth.ts deleted file mode 100644 index 2c97ff5fc5244..0000000000000 --- a/apps/meteor/server/lib/oauth/twoFactorAuth.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -import { getRememberDate } from '../../../app/2fa/server/code'; -import { EmailCheckForOAuth } from '../../../app/2fa/server/code/EmailCheckForOAuth'; -import { TOTPCheckForOAuth } from '../../../app/2fa/server/code/TOTPCheckForOAuth'; - -export const emailCheckForOAuth = new EmailCheckForOAuth(); -export const totpCheckForOAuth = new TOTPCheckForOAuth(); - -const twoFACheckMethodsForOAuth = { - [emailCheckForOAuth.method]: emailCheckForOAuth, - [totpCheckForOAuth.method]: totpCheckForOAuth, -}; - -export const getTwoFAMethodForOAuth = (method: 'email' | 'totp') => { - return twoFACheckMethodsForOAuth[method]; -}; - -const getSecondFactorMethod = (user: IUser) => { - return Array.from(Object.values(twoFACheckMethodsForOAuth)).find((method) => method.isEnabled(user)); -}; - -export const doesUserRequire2FA = (user: IUser) => { - const rememberAfterRegistration = getRememberDate(user.createdAt); - - if (rememberAfterRegistration && rememberAfterRegistration > new Date()) { - return false; - } - - const secondFactorMethod = getSecondFactorMethod(user); - - if (!secondFactorMethod) { - return false; - } - - return secondFactorMethod; -}; diff --git a/apps/meteor/server/lib/oauth/updateOAuthServices.ts b/apps/meteor/server/lib/oauth/updateOAuthServices.ts index f501b9c56888a..784628cae44a9 100644 --- a/apps/meteor/server/lib/oauth/updateOAuthServices.ts +++ b/apps/meteor/server/lib/oauth/updateOAuthServices.ts @@ -7,7 +7,6 @@ import type { } from '@rocket.chat/core-typings'; import { LoginServiceConfiguration } from '@rocket.chat/models'; -import { addPassportCustomOAuth } from './addPassportCustomOAuth'; import { logger } from './logger'; import { CustomOAuth } from '../../../app/custom-oauth/server/custom_oauth_server'; import { @@ -69,7 +68,7 @@ export async function updateOAuthServices(): Promise { data.rolesToSync = settings.get(`${key}-roles_to_sync`); data.showButton = settings.get(`${key}-show_button`); - const config = { + new CustomOAuth(serviceKey, { serverURL: data.serverURL, tokenPath: data.tokenPath, identityPath: data.identityPath, @@ -94,12 +93,7 @@ export async function updateOAuthServices(): Promise { rolesToSync: data.rolesToSync, accessTokenParam: data.accessTokenParam, showButton: data.showButton, - clientSecret: data.secret, - clientId: data.clientId, - }; - - new CustomOAuth(serviceKey, config); - addPassportCustomOAuth(serviceKey, config); + }); } if (serviceName === 'Facebook') { (data as FacebookOAuthConfiguration).appId = data.clientId as string; diff --git a/apps/meteor/server/lib/oauth/verifyFunction.ts b/apps/meteor/server/lib/oauth/verifyFunction.ts deleted file mode 100644 index 74d8c051e5992..0000000000000 --- a/apps/meteor/server/lib/oauth/verifyFunction.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Users } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import type { DoneCallback, Profile } from 'passport'; - -export const verifyFunction = async ( - accessToken: string, - refreshToken: string, - profile: Profile, - done: DoneCallback, - serviceName: string, -) => { - const profileWithRaw = profile as Profile & { _json?: Record; _raw?: string }; - const { _json, _raw, ...restProfile } = profileWithRaw; - - const user = await Accounts.updateOrCreateUserFromExternalService( - serviceName, - { - accessToken, - refreshToken, - name: profile.displayName, - email: profile?.emails?.[0]?.value, - ...profile, - ...restProfile, - ..._json, - }, - {}, - ); - - if (!user?.userId || typeof user?.userId !== 'string') { - return done(new Error('User not found')); - } - - const userFromDB = await Users.findOneById(user.userId); - - if (!userFromDB) { - return done(new Error('User not found')); - } - - return done(null, userFromDB); -}; diff --git a/apps/meteor/server/models.ts b/apps/meteor/server/models.ts index 1d8481923b987..49b5b98b19f08 100644 --- a/apps/meteor/server/models.ts +++ b/apps/meteor/server/models.ts @@ -75,7 +75,6 @@ import { WebdavAccountsRaw, WorkspaceCredentialsRaw, AbacAttributesRaw, - TwoFactorChallengesRaw, } from '@rocket.chat/models'; import type { Collection } from 'mongodb'; @@ -162,4 +161,3 @@ registerModel('IVideoConferenceModel', new VideoConferenceRaw(db)); registerModel('IWebdavAccountsModel', new WebdavAccountsRaw(db)); registerModel('IWorkspaceCredentialsModel', new WorkspaceCredentialsRaw(db)); registerModel('IAbacAttributesModel', new AbacAttributesRaw(db)); -registerModel('ITwoFactorChallengesModel', new TwoFactorChallengesRaw(db)); diff --git a/apps/meteor/server/settings/oauth.ts b/apps/meteor/server/settings/oauth.ts index 8ee583614882b..2aea9b119cf83 100644 --- a/apps/meteor/server/settings/oauth.ts +++ b/apps/meteor/server/settings/oauth.ts @@ -1,5 +1,3 @@ -import { Random } from '@rocket.chat/random'; - import { settingsRegistry } from '../../app/settings/server'; export const createOauthSettings = () => @@ -407,11 +405,6 @@ export const createOauthSettings = () => enableQuery, }); }); - await this.add('Accounts_OAuth_Session_Secret', Random.secret(), { - type: 'string', - secret: true, - hidden: true, - }); return this.section('Proxy', async function () { await this.add('Accounts_OAuth_Proxy_host', 'https://oauth-proxy.rocket.chat', { type: 'string', diff --git a/apps/meteor/tests/e2e/fixtures/addCustomOAuth.ts b/apps/meteor/tests/e2e/fixtures/addCustomOAuth.ts index 70885f1a634fb..5721266b728d5 100644 --- a/apps/meteor/tests/e2e/fixtures/addCustomOAuth.ts +++ b/apps/meteor/tests/e2e/fixtures/addCustomOAuth.ts @@ -15,7 +15,4 @@ export default async function addCustomOAuth(): Promise { await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test`, { data: { value: false }, headers }); await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-url`, { data: { value: 'https://rocket.chat' }, headers }); await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-login_style`, { data: { value: 'redirect' }, headers }); - await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-id`, { data: { value: 'abc' }, headers }); - await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-secret`, { data: { value: 'def' }, headers }); - await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-authorize_path`, { data: { value: '/authorize' }, headers }); } diff --git a/packages/core-typings/src/ILoginServiceConfiguration.ts b/packages/core-typings/src/ILoginServiceConfiguration.ts index c3fb05fa8e18f..76a4779e806e9 100644 --- a/packages/core-typings/src/ILoginServiceConfiguration.ts +++ b/packages/core-typings/src/ILoginServiceConfiguration.ts @@ -34,11 +34,6 @@ export type OAuthConfiguration = { mergeRoles: boolean; rolesToSync: string; showButton: boolean; - addAutopublishFields?: { - forLoggedInUser: string[]; - forOtherUsers: string[]; - }; - pkce?: boolean; }; export type FacebookOAuthConfiguration = Omit, 'clientId'> & { diff --git a/packages/core-typings/src/ITwoFactorChallenge.ts b/packages/core-typings/src/ITwoFactorChallenge.ts deleted file mode 100644 index 783309001ee5d..0000000000000 --- a/packages/core-typings/src/ITwoFactorChallenge.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ITwoFactorChallenge { - _id: string; - userId: string; - method: 'email' | 'totp'; - expireAt: Date; - createdAt: Date; -} diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index bd240e0cdc7bb..857d4a07fd91b 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -233,7 +233,7 @@ export interface IUser extends IRocketChatRecord { isOAuthUser?: boolean; // client only field __rooms?: string[]; inactiveReason?: 'deactivated' | 'pending_approval' | 'idle_too_long'; - providerId?: string; + abacAttributes?: IAbacAttributeDefinition[]; } diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 0b4cd9a471675..90dd9c4e638a8 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -134,5 +134,3 @@ export type * from './ServerAudit/IAuditServerAbacAction'; export type * from './ServerAudit/IAuditUserChangedEvent'; export { schemas } from './Ajv'; - -export type * from './ITwoFactorChallenge'; diff --git a/packages/desktop-api/src/index.ts b/packages/desktop-api/src/index.ts index 5344ad5d18005..9d10c1f89d1c5 100644 --- a/packages/desktop-api/src/index.ts +++ b/packages/desktop-api/src/index.ts @@ -63,5 +63,4 @@ export interface IRocketChatDesktop { setUserToken: (token: string, userId: string) => void; openDocumentViewer: (url: string, format: string, options: any) => void; reloadServer: () => void; - openInBrowser: (url: string) => void; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 99f7b4b067fb8..3d361dc9ac675 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1080,7 +1080,6 @@ "Categories": "Categories", "Categories*": "Categories*", "Certificates_and_Keys": "Certificates and Keys", - "Challenge_expired_please_try_again_later": "Challenge expired. Please try again later.", "Change_Room_Type": "Changing the Room Type", "Changed_from": "Changed from", "Changed_to": "Changed to", @@ -3413,7 +3412,6 @@ "Max_number_of_chats_per_agent_description": "The max. number of simultaneous chats that the agents can attend", "Max_number_of_uses": "Max number of uses", "Maximum": "Maximum", - "Maximum_number_of_attempts_reached_please_try_again_later": "Maximum number of attempts reached. Please try again later.", "Maximum_number_of_guests_reached": "Maximum number of guests reached", "Me": "Me", "Media": "Media", @@ -6838,7 +6836,6 @@ "registration.component.form.usernameAlreadyInUse": "Username already in use", "registration.component.form.usernameContainsInvalidChars": "Username contains invalid characters", "registration.component.login": "Login", - "registration.component.login.onWeb": "Login on web", "registration.component.login.incorrectPassword": "Incorrect password", "registration.component.login.userNotFound": "User not found", "registration.component.resetPassword": "Reset password", diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 7e459803350b9..439447c49ea02 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -81,4 +81,3 @@ export type * from './updater'; export type * from './models/IWorkspaceCredentialsModel'; export type * from './models/ICallHistoryModel'; export type * from './models/IAbacAttributesModel'; -export type * from './models/ITwoFactorChallengesModel'; diff --git a/packages/model-typings/src/models/ITwoFactorChallengesModel.ts b/packages/model-typings/src/models/ITwoFactorChallengesModel.ts deleted file mode 100644 index e0f523ae49104..0000000000000 --- a/packages/model-typings/src/models/ITwoFactorChallengesModel.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ITwoFactorChallenge } from '@rocket.chat/core-typings'; -import type { DeleteResult, FindOptions } from 'mongodb'; - -import type { IBaseModel } from './IBaseModel'; - -export interface ITwoFactorChallengesModel extends IBaseModel { - findOneByPendingChallengeId(id: string, options?: FindOptions): Promise; - removeByPendingChallengeId(id: string): Promise; - createTwoFactorChallenge(userId: string, method: ITwoFactorChallenge['method']): Promise; -} diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 753e851d67a62..099420cd2f7ae 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -80,7 +80,6 @@ import type { IMediaCallNegotiationsModel, ICallHistoryModel, IAbacAttributesModel, - ITwoFactorChallengesModel, } from '@rocket.chat/model-typings'; import type { Collection, Db } from 'mongodb'; @@ -208,7 +207,6 @@ export const Migrations = proxify('IMigrationsModel'); export const ModerationReports = proxify('IModerationReportsModel'); export const WorkspaceCredentials = proxify('IWorkspaceCredentialsModel'); export const AbacAttributes = proxify('IAbacAttributesModel'); -export const TwoFactorChallenges = proxify('ITwoFactorChallengesModel'); export function registerServiceModels(db: Db, trash?: Collection>): void { registerModel('IUsersSessionsModel', () => new UsersSessionsRaw(db)); diff --git a/packages/models/src/modelClasses.ts b/packages/models/src/modelClasses.ts index b9d446e64c1ac..c554fc9698550 100644 --- a/packages/models/src/modelClasses.ts +++ b/packages/models/src/modelClasses.ts @@ -73,4 +73,3 @@ export * from './models/MediaCallNegotiations'; export * from './models/WorkspaceCredentials'; export * from './models/Trash'; export * from './models/CallHistory'; -export * from './models/TwoFactorChallenges'; diff --git a/packages/models/src/models/TwoFactorChallenges.ts b/packages/models/src/models/TwoFactorChallenges.ts deleted file mode 100644 index 2d4fb748c1d15..0000000000000 --- a/packages/models/src/models/TwoFactorChallenges.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { randomBytes } from 'crypto'; - -import type { ITwoFactorChallenge } from '@rocket.chat/core-typings'; -import type { ITwoFactorChallengesModel } from '@rocket.chat/model-typings'; -import type { Db, FindOptions, IndexDescription } from 'mongodb'; - -import { BaseRaw } from './BaseRaw'; - -export class TwoFactorChallengesRaw extends BaseRaw implements ITwoFactorChallengesModel { - constructor(db: Db) { - super(db, 'two_factor_challenges'); - } - - override modelIndexes(): IndexDescription[] { - return [{ key: { expireAt: 1 }, expireAfterSeconds: 0 }]; - } - - findOneByPendingChallengeId(pendingChallengeId: string, options?: FindOptions) { - return this.findOne({ _id: pendingChallengeId }, options); - } - - removeByPendingChallengeId(pendingChallengeId: string) { - return this.deleteOne({ _id: pendingChallengeId }); - } - - async createTwoFactorChallenge(userId: string, method: ITwoFactorChallenge['method']): Promise { - const now = new Date(); - const challengeId = randomBytes(32).toString('hex'); - await this.insertOne({ - _id: challengeId, - userId, - method, - createdAt: now, - expireAt: new Date(now.getTime() + 1000 * 60 * 5), - }); - return challengeId; - } -} diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index ef1f94da30eb1..9c25e22e983f8 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -41,7 +41,6 @@ import type { SettingsEndpoints } from './v1/settings'; import type { StatisticsEndpoints } from './v1/statistics'; import type { SubscriptionsEndpoints } from './v1/subscriptionsEndpoints'; import type { TeamsEndpoints } from './v1/teams'; -import type { TwoFactorChallengesEndpoints } from './v1/twoFactorChallenges'; import type { UsersEndpoints } from './v1/users'; import type { VideoConferenceEndpoints } from './v1/videoConference'; @@ -91,7 +90,6 @@ export interface Endpoints AuthEndpoints, ImportEndpoints, ServerEventsEndpoints, - TwoFactorChallengesEndpoints, DefaultEndpoints {} type OperationsByPathPatternAndMethod< @@ -266,7 +264,6 @@ export * from './v1/auth'; export * from './v1/cloud'; export * from './v1/banners'; export * from './default'; -export * from './v1/twoFactorChallenges'; // Export the ajv instance for use in other packages export * from './v1/Ajv'; diff --git a/packages/rest-typings/src/v1/twoFactorChallenges.ts b/packages/rest-typings/src/v1/twoFactorChallenges.ts deleted file mode 100644 index 42b471b244a48..0000000000000 --- a/packages/rest-typings/src/v1/twoFactorChallenges.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ajv } from './Ajv'; - -type TwoFactorChallengesSendEmailCode = { challengeId: string }; - -const TwoFactorChallengesSendEmailCodeSchema = { - type: 'object', - properties: { - challengeId: { - type: 'string', - }, - }, - required: ['challengeId'], - additionalProperties: false, -}; - -export const isTwoFactorChallengesSendEmailCodeParamsPOST = ajv.compile( - TwoFactorChallengesSendEmailCodeSchema, -); - -type TwoFactorChallengesVerifyChallenge = { challengeId: string; code: string }; - -const TwoFactorChallengesVerifyChallengeSchema = { - type: 'object', - properties: { - challengeId: { type: 'string' }, - code: { type: 'string' }, - }, - required: ['challengeId', 'code'], - additionalProperties: false, -}; - -export const isTwoFactorChallengesVerifyChallengeParamsPOST = ajv.compile( - TwoFactorChallengesVerifyChallengeSchema, -); - -export type TwoFactorChallengesEndpoints = { - '/v1/twoFactorChallenges.sendEmailCode': { - POST: (params: TwoFactorChallengesSendEmailCode) => void; - }; - '/v1/twoFactorChallenges.verifyChallenge': { - POST: (params: TwoFactorChallengesVerifyChallenge) => { loginToken: string; userId: string }; - }; -}; diff --git a/packages/web-ui-registration/global.d.ts b/packages/web-ui-registration/global.d.ts deleted file mode 100644 index 51c30e263ccad..0000000000000 --- a/packages/web-ui-registration/global.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { IRocketChatDesktop } from '@rocket.chat/desktop-api'; - -declare global { - interface Window { - RocketChatDesktop?: IRocketChatDesktop; - } -} diff --git a/packages/web-ui-registration/src/LoginServices.tsx b/packages/web-ui-registration/src/LoginServices.tsx index df490dfcd4d59..e36c032e0436f 100644 --- a/packages/web-ui-registration/src/LoginServices.tsx +++ b/packages/web-ui-registration/src/LoginServices.tsx @@ -1,13 +1,11 @@ -import { Button, ButtonGroup, Divider } from '@rocket.chat/fuselage'; +import { ButtonGroup, Divider } from '@rocket.chat/fuselage'; import { useLoginServices, useSetting } from '@rocket.chat/ui-contexts'; -import { useMemo, type Dispatch, type ReactElement, type SetStateAction } from 'react'; +import type { Dispatch, ReactElement, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import type { LoginErrorState } from './LoginForm'; import LoginServicesButton from './LoginServicesButton'; -const servicesToBeShownOnDesktop = ['saml', 'cas', 'ldap']; - const LoginServices = ({ disabled, setError, @@ -19,28 +17,10 @@ const LoginServices = ({ const services = useLoginServices(); const showFormLogin = useSetting('Accounts_ShowFormLogin'); - const isDesktopApp = !!window.RocketChatDesktop?.openInBrowser; - - const servicesToShow = useMemo( - () => (isDesktopApp ? services.filter(({ service }) => servicesToBeShownOnDesktop.includes(service)) : services), - [isDesktopApp, services], - ); - if (services.length === 0) { return null; } - const handleLoginOnWeb = () => { - if (!isDesktopApp) { - return; - } - - const redirectUrl = new URL(window.location.href); - redirectUrl.searchParams.set('loginClient', 'desktop'); - - window.RocketChatDesktop?.openInBrowser(redirectUrl.toString()); - }; - return ( <> {showFormLogin && ( @@ -48,20 +28,11 @@ const LoginServices = ({ {t('registration.component.form.divider')} )} - - {servicesToShow.length > 0 && ( - - {servicesToShow.map((service) => ( - - ))} - - )} - - {isDesktopApp && ( - - )} + + {services.map((service) => ( + + ))} + ); }; diff --git a/packages/web-ui-registration/src/LoginServicesButton.tsx b/packages/web-ui-registration/src/LoginServicesButton.tsx index 62d3d9856cd17..7eb22ad5dbfc6 100644 --- a/packages/web-ui-registration/src/LoginServicesButton.tsx +++ b/packages/web-ui-registration/src/LoginServicesButton.tsx @@ -8,8 +8,6 @@ import { useTranslation } from 'react-i18next'; import type { LoginErrorState, LoginErrors } from './LoginForm'; -const servicesSupportedByMeteor = ['saml', 'cas', 'ldap']; - const LoginServicesButton = ({ buttonLabelText, icon, @@ -30,28 +28,13 @@ const LoginServicesButton = ({ const handler = useLoginWithService({ service, buttonLabelText, ...props }); const handleOnClick = useCallback(() => { - if (!servicesSupportedByMeteor.includes(service)) { - const url = new URL(window.location.href); - const queryParams = url.searchParams; - const loginClient = queryParams.get('loginClient'); - - const redirectUrl = new URL(`/oauth/${service}`, window.location.origin); - - if (loginClient) { - redirectUrl.searchParams.set('loginClient', loginClient); - } - - window.location.href = redirectUrl.toString(); - return; - } - handler().catch((e: { error?: LoginErrors; reason?: string }) => { if (!e.error || typeof e.error !== 'string') { return; } setError?.([e.error, e.reason]); }); - }, [handler, setError, service]); + }, [handler, setError]); return (