[NEW] 2 Factor Authentication when using OAuth and SAML (#11726)
parent
5c8a15258d
commit
378ef37349
@ -0,0 +1,41 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import { Google } from 'meteor/google-oauth'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { Utils2fa } from './lib/2fa'; |
||||
|
||||
const loginWithGoogleAndTOTP = function(options, code, callback) { |
||||
// support a callback without options
|
||||
if (!callback && typeof options === 'function') { |
||||
callback = options; |
||||
options = null; |
||||
} |
||||
|
||||
if (Meteor.isCordova && Google.signIn) { |
||||
// After 20 April 2017, Google OAuth login will no longer work from
|
||||
// a WebView, so Cordova apps must use Google Sign-In instead.
|
||||
// https://github.com/meteor/meteor/issues/8253
|
||||
Google.signIn(options, callback); |
||||
return; |
||||
} // Use Google's domain-specific login page if we want to restrict creation to
|
||||
// a particular email domain. (Don't use it if restrictCreationByEmailDomain
|
||||
// is a function.) Note that all this does is change Google's UI ---
|
||||
// accounts-base/accounts_server.js still checks server-side that the server
|
||||
// has the proper email address after the OAuth conversation.
|
||||
|
||||
|
||||
if (typeof Accounts._options.restrictCreationByEmailDomain === 'string') { |
||||
options = _.extend({}, options || {}); |
||||
options.loginUrlParameters = _.extend({}, options.loginUrlParameters || {}); |
||||
options.loginUrlParameters.hd = Accounts._options.restrictCreationByEmailDomain; |
||||
} |
||||
|
||||
const credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback, code); |
||||
Google.requestCredential(options, credentialRequestCompleteCallback); |
||||
}; |
||||
|
||||
const { loginWithGoogle } = Meteor; |
||||
Meteor.loginWithGoogle = function(options, cb) { |
||||
Utils2fa.overrideLoginMethod(loginWithGoogle, [options], cb, loginWithGoogleAndTOTP); |
||||
}; |
@ -0,0 +1,50 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
|
||||
import { Utils2fa } from './lib/2fa'; |
||||
|
||||
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) { |
||||
Utils2fa.reportError(error, callback); |
||||
} else { |
||||
callback && callback(); |
||||
} |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
const { loginWithLDAP } = Meteor; |
||||
|
||||
Meteor.loginWithLDAP = function(...args) { |
||||
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null; |
||||
|
||||
Utils2fa.overrideLoginMethod(loginWithLDAP, args, callback, Meteor.loginWithLDAPAndTOTP); |
||||
}; |
@ -0,0 +1,72 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import { Facebook } from 'meteor/facebook-oauth'; |
||||
import { Github } from 'meteor/github-oauth'; |
||||
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 { Utils2fa } from './lib/2fa'; |
||||
|
||||
Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback, totpCode) { |
||||
const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken) || null; |
||||
const methodArgument = { |
||||
oauth: { |
||||
credentialToken, |
||||
credentialSecret, |
||||
}, |
||||
}; |
||||
|
||||
if (totpCode && typeof totpCode === 'string') { |
||||
methodArgument.totp = { |
||||
code: totpCode, |
||||
}; |
||||
} |
||||
|
||||
Accounts.callLoginMethod({ |
||||
methodArguments: [methodArgument], |
||||
userCallback: callback && function(err) { |
||||
callback(Utils2fa.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 loginWithFacebookAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Facebook); |
||||
const { loginWithFacebook } = Meteor; |
||||
Meteor.loginWithFacebook = function(options, cb) { |
||||
Utils2fa.overrideLoginMethod(loginWithFacebook, [options], cb, loginWithFacebookAndTOTP); |
||||
}; |
||||
|
||||
const loginWithGithubAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Github); |
||||
const { loginWithGithub } = Meteor; |
||||
Meteor.loginWithGithub = function(options, cb) { |
||||
Utils2fa.overrideLoginMethod(loginWithGithub, [options], cb, loginWithGithubAndTOTP); |
||||
}; |
||||
|
||||
const loginWithMeteorDeveloperAccountAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => MeteorDeveloperAccounts); |
||||
const { loginWithMeteorDeveloperAccount } = Meteor; |
||||
Meteor.loginWithMeteorDeveloperAccount = function(options, cb) { |
||||
Utils2fa.overrideLoginMethod(loginWithMeteorDeveloperAccount, [options], cb, loginWithMeteorDeveloperAccountAndTOTP); |
||||
}; |
||||
|
||||
const loginWithTwitterAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Twitter); |
||||
const { loginWithTwitter } = Meteor; |
||||
Meteor.loginWithTwitter = function(options, cb) { |
||||
Utils2fa.overrideLoginMethod(loginWithTwitter, [options], cb, loginWithTwitterAndTOTP); |
||||
}; |
||||
|
||||
const loginWithLinkedinAndTOTP = Utils2fa.createOAuthTotpLoginMethod(() => Linkedin); |
||||
const { loginWithLinkedin } = Meteor; |
||||
Meteor.loginWithLinkedin = function(options, cb) { |
||||
Utils2fa.overrideLoginMethod(loginWithLinkedin, [options], cb, loginWithLinkedinAndTOTP); |
||||
}; |
@ -0,0 +1,32 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
|
||||
import { Utils2fa } from './lib/2fa'; |
||||
import '../../meteor-accounts-saml/client/saml_client'; |
||||
|
||||
Meteor.loginWithSamlTokenAndTOTP = function(credentialToken, code, callback) { |
||||
Accounts.callLoginMethod({ |
||||
methodArguments: [{ |
||||
totp: { |
||||
login: { |
||||
saml: true, |
||||
credentialToken, |
||||
}, |
||||
code, |
||||
}, |
||||
}], |
||||
userCallback(error) { |
||||
if (error) { |
||||
Utils2fa.reportError(error, callback); |
||||
} else { |
||||
callback && callback(); |
||||
} |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
const { loginWithSamlToken } = Meteor; |
||||
|
||||
Meteor.loginWithSamlToken = function(options, callback) { |
||||
Utils2fa.overrideLoginMethod(loginWithSamlToken, [options], callback, Meteor.loginWithSamlTokenAndTOTP); |
||||
}; |
@ -1,2 +1,6 @@ |
||||
import './callWithTwoFactorRequired'; |
||||
import './TOTPPassword'; |
||||
import './TOTPOAuth'; |
||||
import './TOTPGoogle'; |
||||
import './TOTPSaml'; |
||||
import './TOTPLDAP'; |
||||
|
@ -0,0 +1,89 @@ |
||||
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'; |
||||
|
||||
export class Utils2fa { |
||||
static reportError(error, callback) { |
||||
if (callback) { |
||||
callback(error); |
||||
} else { |
||||
throw error; |
||||
} |
||||
} |
||||
|
||||
static convertError(err) { |
||||
if (err && err instanceof Meteor.Error && err.error === Accounts.LoginCancelledError.numericError) { |
||||
return new Accounts.LoginCancelledError(err.reason); |
||||
} |
||||
|
||||
return err; |
||||
} |
||||
|
||||
static overrideLoginMethod(loginMethod, loginArgs, cb, loginMethodTOTP) { |
||||
loginMethod.apply(this, loginArgs.concat([(error) => { |
||||
if (!error || error.error !== 'totp-required') { |
||||
return cb(error); |
||||
} |
||||
|
||||
process2faReturn({ |
||||
error, |
||||
originalCallback: cb, |
||||
onCode: (code) => { |
||||
loginMethodTOTP && loginMethodTOTP.apply(this, loginArgs.concat([code, (error) => { |
||||
console.log('failed'); |
||||
console.log(error); |
||||
if (error && error.error === 'totp-invalid') { |
||||
toastr.error(t('Invalid_two_factor_code')); |
||||
cb(); |
||||
} else { |
||||
cb(error); |
||||
} |
||||
}])); |
||||
}, |
||||
}); |
||||
}])); |
||||
} |
||||
|
||||
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); |
||||
}; |
||||
}; |
Loading…
Reference in new issue