Regression: Fix Custom OAuth 2FA (#19691)

pull/19706/head
pierre-lehnen-rc 5 years ago committed by GitHub
parent f7f2940ad5
commit 43cb21cd46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 80
      app/2fa/client/TOTPOAuth.js
  2. 45
      app/2fa/client/lib/2fa.js
  3. 54
      app/2fa/server/loginHandler.js

@ -6,11 +6,17 @@ import { Twitter } from 'meteor/twitter-oauth';
import { MeteorDeveloperAccounts } from 'meteor/meteor-developer-oauth';
import { Linkedin } from 'meteor/pauli:linkedin-oauth';
import { OAuth } from 'meteor/oauth';
import s from 'underscore.string';
import { Utils2fa } from './lib/2fa';
import { process2faReturn } from './callWithTwoFactorRequired';
import { CustomOAuth } from '../../custom-oauth';
Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback, totpCode) {
const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken) || null;
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,
@ -18,6 +24,9 @@ Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback, to
},
};
lastCredentialToken = credentialToken;
lastCredentialSecret = credentialSecret;
if (totpCode && typeof totpCode === 'string') {
methodArgument.totp = {
code: totpCode,
@ -41,32 +50,89 @@ Accounts.oauth.credentialRequestCompleteHandler = function(callback, totpCode) {
};
};
const loginWithFacebookAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Facebook);
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) {
Utils2fa.overrideLoginMethod(loginWithFacebook, [options], cb, loginWithFacebookAndTOTP);
};
const loginWithGithubAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Github);
const loginWithGithubAndTOTP = createOAuthTotpLoginMethod(() => Github);
const { loginWithGithub } = Meteor;
Meteor.loginWithGithub = function(options, cb) {
Utils2fa.overrideLoginMethod(loginWithGithub, [options], cb, loginWithGithubAndTOTP);
};
const loginWithMeteorDeveloperAccountAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => MeteorDeveloperAccounts);
const loginWithMeteorDeveloperAccountAndTOTP = createOAuthTotpLoginMethod(() => MeteorDeveloperAccounts);
const { loginWithMeteorDeveloperAccount } = Meteor;
Meteor.loginWithMeteorDeveloperAccount = function(options, cb) {
Utils2fa.overrideLoginMethod(loginWithMeteorDeveloperAccount, [options], cb, loginWithMeteorDeveloperAccountAndTOTP);
};
const loginWithTwitterAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Twitter);
const loginWithTwitterAndTOTP = createOAuthTotpLoginMethod(() => Twitter);
const { loginWithTwitter } = Meteor;
Meteor.loginWithTwitter = function(options, cb) {
Utils2fa.overrideLoginMethod(loginWithTwitter, [options], cb, loginWithTwitterAndTOTP);
};
const loginWithLinkedinAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Linkedin);
const loginWithLinkedinAndTOTP = createOAuthTotpLoginMethod(() => Linkedin);
const { loginWithLinkedin } = Meteor;
Meteor.loginWithLinkedin = function(options, cb) {
Utils2fa.overrideLoginMethod(loginWithLinkedin, [options], cb, loginWithLinkedinAndTOTP);
};
Accounts.onPageLoadLogin((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;
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${ s.capitalize(this.name) }`;
oldConfigureLogin.apply(this, args);
const oldMethod = Meteor[loginWithService];
Meteor[loginWithService] = function(options, cb) {
Utils2fa.overrideLoginMethod(oldMethod, [options], cb, loginWithOAuthTokenAndTOTP);
};
};

@ -1,9 +1,7 @@
import { Meteor } from 'meteor/meteor';
import toastr from 'toastr';
import s from 'underscore.string';
import { Accounts } from 'meteor/accounts-base';
import { CustomOAuth } from '../../../custom-oauth';
import { t } from '../../../utils/client';
import { process2faReturn } from '../callWithTwoFactorRequired';
@ -36,8 +34,9 @@ export class Utils2fa {
originalCallback: cb,
onCode: (code) => {
loginMethodTOTP && loginMethodTOTP.apply(this, loginArgs.concat([code, (error) => {
console.log('failed');
console.log(error);
if (error) {
console.log(error);
}
if (error && error.error === 'totp-invalid') {
toastr.error(t('Invalid_two_factor_code'));
cb();
@ -49,42 +48,4 @@ export class Utils2fa {
});
}]));
}
static createOAuthTotpLoginMethod(credentialProvider) {
return function(options, code, callback) {
// support a callback without options
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
const provider = credentialProvider();
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback, code);
provider.requestCredential(options, credentialRequestCompleteCallback);
};
}
}
const oldConfigureLogin = CustomOAuth.prototype.configureLogin;
CustomOAuth.prototype.configureLogin = function(...args) {
const loginWithService = `loginWith${ s.capitalize(this.name) }`;
oldConfigureLogin.apply(this, args);
const oldMethod = Meteor[loginWithService];
const newMethod = (options, code, callback) => {
// support a callback without options
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback, code);
this.requestCredential(options, credentialRequestCompleteCallback);
};
Meteor[loginWithService] = function(options, cb) {
Utils2fa.overrideLoginMethod(oldMethod, [options], cb, newMethod);
};
};

@ -1,4 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { OAuth } from 'meteor/oauth';
import { check } from 'meteor/check';
import { callbacks } from '../../callbacks';
import { checkCodeForUser } from './code/index';
@ -22,3 +25,54 @@ callbacks.add('onValidateLogin', (login) => {
return login;
}, callbacks.priority.MEDIUM, '2fa');
const recreateError = (errorDoc) => {
let error;
if (errorDoc.meteorError) {
error = new Meteor.Error();
delete errorDoc.meteorError;
} else {
error = new Error();
}
Object.getOwnPropertyNames(errorDoc).forEach((key) => {
error[key] = errorDoc[key];
});
return error;
};
OAuth._retrievePendingCredential = function(key, ...args) {
const credentialSecret = args.length > 0 && args[0] !== undefined ? args[0] : null;
check(key, String);
const pendingCredential = OAuth._pendingCredentials.findOne({
key,
credentialSecret,
});
if (!pendingCredential) {
return;
}
if (pendingCredential.credential.error) {
OAuth._pendingCredentials.remove({
_id: pendingCredential._id,
});
return recreateError(pendingCredential.credential.error);
}
// Work-around to make the credentials reusable for 2FA
const future = new Date();
future.setMinutes(future.getMinutes() + 2);
OAuth._pendingCredentials.update({
_id: pendingCredential._id,
}, {
$set: {
createdAt: future,
},
});
return OAuth.openSecret(pendingCredential.credential);
};

Loading…
Cancel
Save