Skip to content
Snippets Groups Projects
Unverified Commit 1b486a1c authored by Tasso Evangelista's avatar Tasso Evangelista Committed by GitHub
Browse files

refactor(client): Move Meteor overrides (#28366)

parent 9a6e9b4e
No related branches found
No related tags found
No related merge requests found
Showing
with 35 additions and 522 deletions
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import '../../crowd/client/index';
import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod';
import { reportError } from '../../../client/lib/2fa/utils';
Meteor.loginWithCrowdAndTOTP = function (username, password, code, callback) {
const loginRequest = {
crowd: true,
username,
crowdPassword: password,
};
Accounts.callLoginMethod({
methodArguments: [
{
totp: {
login: loginRequest,
code,
},
},
],
userCallback(error) {
if (error) {
reportError(error, callback);
} else {
callback && callback();
}
},
});
};
const { loginWithCrowd } = Meteor;
Meteor.loginWithCrowd = function (username, password, callback) {
overrideLoginMethod(loginWithCrowd, [username, password], callback, Meteor.loginWithCrowdAndTOTP);
};
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import '../../../client/startup/ldap';
import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod';
import { reportError } from '../../../client/lib/2fa/utils';
Meteor.loginWithLDAPAndTOTP = function (...args) {
// Pull username and password
const username = args.shift();
const ldapPass = args.shift();
// Check if last argument is a function. if it is, pop it off and set callback to it
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null;
// The last argument before the callback is the totp code
const code = args.pop();
// if args still holds options item, grab it
const ldapOptions = args.length > 0 ? args.shift() : {};
// Set up loginRequest object
const loginRequest = {
ldap: true,
username,
ldapPass,
ldapOptions,
};
Accounts.callLoginMethod({
methodArguments: [
{
totp: {
login: loginRequest,
code,
},
},
],
userCallback(error) {
if (error) {
reportError(error, callback);
} else {
callback && callback();
}
},
});
};
const { loginWithLDAP } = Meteor;
Meteor.loginWithLDAP = function (...args) {
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null;
overrideLoginMethod(loginWithLDAP, args, callback, Meteor.loginWithLDAPAndTOTP, args[0]);
};
import { capitalize } from '@rocket.chat/string-helpers';
import { Accounts } from 'meteor/accounts-base';
import { Facebook } from 'meteor/facebook-oauth';
import { Github } from 'meteor/github-oauth';
import { Meteor } from 'meteor/meteor';
import { MeteorDeveloperAccounts } from 'meteor/meteor-developer-oauth';
import { OAuth } from 'meteor/oauth';
import { Linkedin } from 'meteor/pauli:linkedin-oauth';
import { Twitter } from 'meteor/twitter-oauth';
import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod';
import { process2faReturn } from '../../../client/lib/2fa/process2faReturn';
import { convertError } from '../../../client/lib/2fa/utils';
import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client';
let lastCredentialToken = null;
let lastCredentialSecret = null;
Accounts.oauth.tryLoginAfterPopupClosed = function (credentialToken, callback, totpCode, credentialSecret = null) {
credentialSecret = credentialSecret || OAuth._retrieveCredentialSecret(credentialToken) || null;
const methodArgument = {
oauth: {
credentialToken,
credentialSecret,
},
};
lastCredentialToken = credentialToken;
lastCredentialSecret = credentialSecret;
if (totpCode && typeof totpCode === 'string') {
methodArgument.totp = {
code: totpCode,
};
}
Accounts.callLoginMethod({
methodArguments: [methodArgument],
userCallback:
callback &&
function (err) {
callback(convertError(err));
},
});
};
Accounts.oauth.credentialRequestCompleteHandler = function (callback, totpCode) {
return function (credentialTokenOrError) {
if (credentialTokenOrError && credentialTokenOrError instanceof Error) {
callback && callback(credentialTokenOrError);
} else {
Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback, totpCode);
}
};
};
const createOAuthTotpLoginMethod = (credentialProvider) => (options, code, callback) => {
// support a callback without options
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
if (lastCredentialToken && lastCredentialSecret) {
Accounts.oauth.tryLoginAfterPopupClosed(lastCredentialToken, callback, code, lastCredentialSecret);
} else {
const provider = (credentialProvider && credentialProvider()) || this;
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback, code);
provider.requestCredential(options, credentialRequestCompleteCallback);
}
lastCredentialToken = null;
lastCredentialSecret = null;
};
const loginWithOAuthTokenAndTOTP = createOAuthTotpLoginMethod();
const loginWithFacebookAndTOTP = createOAuthTotpLoginMethod(() => Facebook);
const { loginWithFacebook } = Meteor;
Meteor.loginWithFacebook = function (options, cb) {
overrideLoginMethod(loginWithFacebook, [options], cb, loginWithFacebookAndTOTP);
};
const loginWithGithubAndTOTP = createOAuthTotpLoginMethod(() => Github);
const { loginWithGithub } = Meteor;
Meteor.loginWithGithub = function (options, cb) {
overrideLoginMethod(loginWithGithub, [options], cb, loginWithGithubAndTOTP);
};
const loginWithMeteorDeveloperAccountAndTOTP = createOAuthTotpLoginMethod(() => MeteorDeveloperAccounts);
const { loginWithMeteorDeveloperAccount } = Meteor;
Meteor.loginWithMeteorDeveloperAccount = function (options, cb) {
overrideLoginMethod(loginWithMeteorDeveloperAccount, [options], cb, loginWithMeteorDeveloperAccountAndTOTP);
};
const loginWithTwitterAndTOTP = createOAuthTotpLoginMethod(() => Twitter);
const { loginWithTwitter } = Meteor;
Meteor.loginWithTwitter = function (options, cb) {
overrideLoginMethod(loginWithTwitter, [options], cb, loginWithTwitterAndTOTP);
};
const loginWithLinkedinAndTOTP = createOAuthTotpLoginMethod(() => Linkedin);
const { loginWithLinkedin } = Meteor;
Meteor.loginWithLinkedin = function (options, cb) {
overrideLoginMethod(loginWithLinkedin, [options], cb, loginWithLinkedinAndTOTP);
};
Accounts.onPageLoadLogin(async (loginAttempt) => {
if (loginAttempt?.error?.error !== 'totp-required') {
return;
}
const { methodArguments } = loginAttempt;
if (!methodArguments?.length) {
return;
}
const oAuthArgs = methodArguments.find((arg) => arg.oauth);
const { credentialToken, credentialSecret } = oAuthArgs.oauth;
const cb = loginAttempt.userCallback;
await process2faReturn({
error: loginAttempt.error,
originalCallback: cb,
onCode: (code) => {
Accounts.oauth.tryLoginAfterPopupClosed(credentialToken, cb, code, credentialSecret);
},
});
});
const oldConfigureLogin = CustomOAuth.prototype.configureLogin;
CustomOAuth.prototype.configureLogin = function (...args) {
const loginWithService = `loginWith${capitalize(String(this.name || ''))}`;
oldConfigureLogin.apply(this, args);
const oldMethod = Meteor[loginWithService];
Meteor[loginWithService] = function (options, cb) {
overrideLoginMethod(oldMethod, [options], cb, loginWithOAuthTokenAndTOTP);
};
};
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { process2faReturn } from '../../../client/lib/2fa/process2faReturn';
import { isTotpInvalidError, isTotpMaxAttemptsError, reportError } from '../../../client/lib/2fa/utils';
import { dispatchToastMessage } from '../../../client/lib/toast';
import { t } from '../../utils/lib/i18n';
Meteor.loginWithPasswordAndTOTP = function (selector, password, code, callback) {
if (typeof selector === 'string') {
if (selector.indexOf('@') === -1) {
selector = { username: selector };
} else {
selector = { email: selector };
}
}
Accounts.callLoginMethod({
methodArguments: [
{
totp: {
login: {
user: selector,
password: Accounts._hashPassword(password),
},
code,
},
},
],
userCallback(error) {
if (error) {
reportError(error, callback);
} else {
callback && callback();
}
},
});
};
const { loginWithPassword } = Meteor;
Meteor.loginWithPassword = function (email, password, cb) {
loginWithPassword(email, password, async (error) => {
await process2faReturn({
error,
originalCallback: cb,
emailOrUsername: email,
onCode: (code) => {
Meteor.loginWithPasswordAndTOTP(email, password, code, (error) => {
if (isTotpMaxAttemptsError(error)) {
dispatchToastMessage({
type: 'error',
message: t('totp-max-attempts'),
});
cb();
return;
}
if (isTotpInvalidError(error)) {
dispatchToastMessage({
type: 'error',
message: t('Invalid_two_factor_code'),
});
cb();
return;
}
cb(error);
});
},
});
});
};
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import '../../meteor-accounts-saml/client/saml_client';
import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod';
import { reportError } from '../../../client/lib/2fa/utils';
Meteor.loginWithSamlTokenAndTOTP = function (credentialToken, code, callback) {
Accounts.callLoginMethod({
methodArguments: [
{
totp: {
login: {
saml: true,
credentialToken,
},
code,
},
},
],
userCallback(error) {
if (error) {
reportError(error, callback);
} else {
callback && callback();
}
},
});
};
const { loginWithSamlToken } = Meteor;
Meteor.loginWithSamlToken = function (options, callback) {
overrideLoginMethod(loginWithSamlToken, [options], callback, Meteor.loginWithSamlTokenAndTOTP);
};
import './TOTPPassword';
import './TOTPOAuth';
import './TOTPGoogle';
import './TOTPSaml';
import './TOTPLDAP';
import './TOTPCrowd';
import './overrideMeteorCall';
import { Meteor } from 'meteor/meteor';
import { process2faReturn, process2faAsyncReturn } from '../../../client/lib/2fa/process2faReturn';
import { isTotpInvalidError } from '../../../client/lib/2fa/utils';
import { t } from '../../utils/lib/i18n';
const { call, callAsync } = Meteor;
type Callback = {
(error: unknown): void;
(error: unknown, result: unknown): void;
};
const callWithTotp =
(methodName: string, args: unknown[], callback: Callback) =>
(twoFactorCode: string, twoFactorMethod: string): unknown =>
call(methodName, ...args, { twoFactorCode, twoFactorMethod }, (error: unknown, result: unknown): void => {
if (isTotpInvalidError(error)) {
callback(new Error(twoFactorMethod === 'password' ? t('Invalid_password') : t('Invalid_two_factor_code')));
return;
}
callback(error, result);
});
const callWithoutTotp = (methodName: string, args: unknown[], callback: Callback) => (): unknown =>
call(methodName, ...args, async (error: unknown, result: unknown): Promise<void> => {
await process2faReturn({
error,
result,
onCode: callWithTotp(methodName, args, callback),
originalCallback: callback,
emailOrUsername: undefined,
});
});
Meteor.call = function (methodName: string, ...args: unknown[]): unknown {
const callback = args.length > 0 && typeof args[args.length - 1] === 'function' ? (args.pop() as Callback) : (): void => undefined;
return callWithoutTotp(methodName, args, callback)();
};
Meteor.callAsync = async function _callAsyncWithTotp(methodName: string, ...args: unknown[]): Promise<unknown> {
try {
return await callAsync(methodName, ...args);
} catch (error: unknown) {
return process2faAsyncReturn({
error,
onCode: (twoFactorCode, twoFactorMethod) => Meteor.callAsync(methodName, ...args, { twoFactorCode, twoFactorMethod }),
emailOrUsername: undefined,
});
}
};
import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client';
import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth';
import { config } from '../lib/config';
new CustomOAuth('apple', config);
......@@ -70,7 +70,7 @@ settings.watchMultiple(
secret,
enabled: settings.get('Accounts_OAuth_Apple'),
loginStyle: 'popup',
clientId,
clientId: clientId as string,
buttonColor: '#000',
buttonLabelColor: '#FFF',
},
......
import { Random } from '@rocket.chat/random';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings/client';
const openCenteredPopup = (url: string, width: number, height: number) => {
const screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft;
const screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop;
const outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth;
const outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : document.body.clientHeight - 22;
// XXX what is the 22?
// Use `outerWidth - width` and `outerHeight - height` for help in
// positioning the popup centered relative to the current window
const left = screenX + (outerWidth - width) / 2;
const top = screenY + (outerHeight - height) / 2;
const features = `width=${width},height=${height},left=${left},top=${top},scrollbars=yes`;
const newwindow = window.open(url, 'Login', features);
newwindow?.focus();
return newwindow;
};
(Meteor as any).loginWithCas = (_?: unknown, callback?: () => void) => {
const credentialToken = Random.id();
const loginUrl = settings.get('CAS_login_url');
const popupWidth = settings.get('CAS_popup_width') || 800;
const popupHeight = settings.get('CAS_popup_height') || 600;
if (!loginUrl) {
return;
}
const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
// check if the provided CAS URL already has some parameters
const delim = loginUrl.split('?').length > 1 ? '&' : '?';
const popupUrl = `${loginUrl}${delim}service=${appUrl}/_cas/${credentialToken}`;
const popup = openCenteredPopup(popupUrl, popupWidth, popupHeight);
const checkPopupOpen = setInterval(() => {
let popupClosed;
try {
// Fix for #328 - added a second test criteria (popup.closed === undefined)
// to humour this Android quirk:
// http://code.google.com/p/android/issues/detail?id=21061
popupClosed = popup?.closed || popup?.closed === undefined;
} catch (e) {
// For some unknown reason, IE9 (and others?) sometimes (when
// the popup closes too quickly?) throws "SCRIPT16386: No such
// interface supported" when trying to read 'popup.closed'. Try
// again in 100ms.
return;
}
if (popupClosed) {
clearInterval(checkPopupOpen);
// check auth on server.
Accounts.callLoginMethod({
methodArguments: [{ cas: { credentialToken } }],
userCallback: callback,
});
}
}, 100);
};
import './cas_client';
import './loginHelper';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
Meteor.loginWithCrowd = function (...args) {
// Pull username and password
const username = args.shift();
const password = args.shift();
const callback = args.shift();
const loginRequest = {
crowd: true,
username,
crowdPassword: password,
};
Accounts.callLoginMethod({
methodArguments: [loginRequest],
userCallback(error) {
if (callback) {
if (error) {
return callback(error);
}
return callback();
}
},
});
};
import type { OauthConfig } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { capitalize } from '@rocket.chat/string-helpers';
import { Accounts } from 'meteor/accounts-base';
......@@ -6,6 +7,9 @@ import { Meteor } from 'meteor/meteor';
import { OAuth } from 'meteor/oauth';
import { ServiceConfiguration } from 'meteor/service-configuration';
import type { IOAuthProvider } from '../../../client/definitions/IOAuthProvider';
import { overrideLoginMethod, type LoginCallback } from '../../../client/lib/2fa/overrideLoginMethod';
import { createOAuthTotpLoginMethod } from '../../../client/meteorOverrides/login/oauth';
import { isURL } from '../../../lib/utils/isURL';
// Request custom OAuth credentials for the user
......@@ -14,8 +18,16 @@ import { isURL } from '../../../lib/utils/isURL';
// completion. Takes one argument, credentialToken on success, or Error on
// error.
export class CustomOAuth {
constructor(name, options) {
export class CustomOAuth implements IOAuthProvider {
public serverURL: string;
public authorizePath: string;
public scope: string;
public responseType: string;
constructor(public readonly name: string, options: OauthConfig) {
this.name = name;
if (!Match.test(this.name, String)) {
throw new Meteor.Error('CustomOAuth: Name is required and must be String');
......@@ -28,7 +40,7 @@ export class CustomOAuth {
this.configureLogin();
}
configure(options) {
configure(options: OauthConfig) {
if (!Match.test(options, Object)) {
throw new Meteor.Error('CustomOAuth: Options is required and must be Object');
}
......@@ -56,31 +68,28 @@ export class CustomOAuth {
}
configureLogin() {
const loginWithService = `loginWith${capitalize(String(this.name || ''))}`;
const loginWithService = `loginWith${capitalize(String(this.name || ''))}` as const;
Meteor[loginWithService] = async (options, callback) => {
// support a callback without options
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
const loginWithOAuthTokenAndTOTP = createOAuthTotpLoginMethod(this);
const loginWithOAuthToken = async (options?: Meteor.LoginWithExternalServiceOptions, callback?: LoginCallback) => {
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
await this.requestCredential(options, credentialRequestCompleteCallback);
};
}
async requestCredential(options, credentialRequestCompleteCallback) {
// support both (options, callback) and (callback).
if (!credentialRequestCompleteCallback && typeof options === 'function') {
credentialRequestCompleteCallback = options;
options = {};
}
(Meteor as any)[loginWithService] = (options: Meteor.LoginWithExternalServiceOptions, callback: LoginCallback) => {
overrideLoginMethod(loginWithOAuthToken, [options], callback, loginWithOAuthTokenAndTOTP);
};
}
async requestCredential(
options: Meteor.LoginWithExternalServiceOptions = {},
credentialRequestCompleteCallback: (credentialTokenOrError?: string | Error) => void,
) {
const config = await ServiceConfiguration.configurations.findOneAsync({ service: this.name });
if (!config) {
if (credentialRequestCompleteCallback) {
credentialRequestCompleteCallback(new ServiceConfiguration.ConfigError());
credentialRequestCompleteCallback(new Accounts.ConfigError());
}
return;
}
......
......@@ -106,7 +106,7 @@ export class CustomOAuth {
async getAccessToken(query) {
const config = await ServiceConfiguration.configurations.findOneAsync({ service: this.name });
if (!config) {
throw new ServiceConfiguration.ConfigError();
throw new Accounts.ConfigError();
}
let response = undefined;
......
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client';
import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth';
import { settings } from '../../settings/client';
const config = {
......
......@@ -2,7 +2,7 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client';
import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth';
import { settings } from '../../settings/client';
// Drupal Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/drupal
......
......@@ -2,7 +2,7 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client';
import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth';
import { settings } from '../../settings/client';
// GitHub Enterprise Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/github_enterprise
......
......@@ -2,7 +2,7 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { CustomOAuth } from '../../custom-oauth/client/custom_oauth_client';
import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth';
import { settings } from '../../settings/client';
const config: OauthConfig = {
......
......@@ -36,7 +36,7 @@ Accounts.registerLoginHandler(async (options) => {
// Make sure we're configured
if (!(await ServiceConfiguration.configurations.findOneAsync({ service: options.serviceName }))) {
throw new ServiceConfiguration.ConfigError();
throw new Accounts.ConfigError();
}
if (!_.contains(Accounts.oauth.serviceNames(), service.serviceName)) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment