mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
two-factor support in browser extensions
This commit is contained in:
parent
981fd22ce5
commit
7815af24e5
6
src/images/loading.svg
Normal file
6
src/images/loading.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
|
||||||
|
<text fill="%23333333" x="50%" y="50%" font-family="\'Open Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
|
||||||
|
font-size="18" text-anchor="middle">
|
||||||
|
Loading...
|
||||||
|
</text>
|
||||||
|
</svg>
|
@ -40,7 +40,8 @@
|
|||||||
$state.go('twoFactor', {
|
$state.go('twoFactor', {
|
||||||
animation: 'in-slide-left',
|
animation: 'in-slide-left',
|
||||||
email: model.email,
|
email: model.email,
|
||||||
masterPassword: model.masterPassword
|
masterPassword: model.masterPassword,
|
||||||
|
providers: response.twoFactorProviders
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -2,23 +2,30 @@
|
|||||||
.module('bit.accounts')
|
.module('bit.accounts')
|
||||||
|
|
||||||
.controller('accountsLoginTwoFactorController', function ($scope, $state, authService, toastr, utilsService,
|
.controller('accountsLoginTwoFactorController', function ($scope, $state, authService, toastr, utilsService,
|
||||||
$analytics, i18nService, $stateParams) {
|
$analytics, i18nService, $stateParams, $filter, constantsService, $timeout, $window, cryptoService) {
|
||||||
$scope.i18n = i18nService;
|
$scope.i18n = i18nService;
|
||||||
$scope.model = {};
|
|
||||||
utilsService.initListSectionItemListeners($(document), angular);
|
utilsService.initListSectionItemListeners($(document), angular);
|
||||||
$('#code').focus();
|
|
||||||
|
|
||||||
|
var constants = constantsService;
|
||||||
var email = $stateParams.email;
|
var email = $stateParams.email;
|
||||||
var masterPassword = $stateParams.masterPassword;
|
var masterPassword = $stateParams.masterPassword;
|
||||||
|
var providers = $stateParams.providers;
|
||||||
|
|
||||||
|
$scope.twoFactorEmail = null;
|
||||||
|
$scope.token = null;
|
||||||
|
$scope.constantsProvider = constants.twoFactorProvider;
|
||||||
|
$scope.providerType = $stateParams.provider ? $stateParams.provider : getDefaultProvider(providers);
|
||||||
|
$scope.u2fReady = false;
|
||||||
|
init();
|
||||||
|
|
||||||
$scope.loginPromise = null;
|
$scope.loginPromise = null;
|
||||||
$scope.login = function (model) {
|
$scope.login = function (token) {
|
||||||
if (!model.code) {
|
if (!token) {
|
||||||
toastr.error(i18nService.verificationCodeRequired, i18nService.errorsOccurred);
|
toastr.error(i18nService.verificationCodeRequired, i18nService.errorsOccurred);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.loginPromise = authService.logIn(email, masterPassword, 0, model.code);
|
$scope.loginPromise = authService.logIn(email, masterPassword, $scope.providerType, token);
|
||||||
$scope.loginPromise.then(function () {
|
$scope.loginPromise.then(function () {
|
||||||
$analytics.eventTrack('Logged In From Two-step');
|
$analytics.eventTrack('Logged In From Two-step');
|
||||||
$state.go('tabs.vault', { animation: 'in-slide-left', syncOnLoad: true });
|
$state.go('tabs.vault', { animation: 'in-slide-left', syncOnLoad: true });
|
||||||
@ -29,4 +36,94 @@
|
|||||||
$analytics.eventTrack('Selected Lost 2FA App');
|
$analytics.eventTrack('Selected Lost 2FA App');
|
||||||
chrome.tabs.create({ url: 'https://help.bitwarden.com/article/lost-two-step-device/' });
|
chrome.tabs.create({ url: 'https://help.bitwarden.com/article/lost-two-step-device/' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.sendEmail = function (doToast) {
|
||||||
|
if ($scope.providerType !== constants.twoFactorProvider.email) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = cryptoService.makeKey(masterPassword, email);
|
||||||
|
var hash = cryptoService.hashPassword(masterPassword, key);
|
||||||
|
apiService.postTwoFactorEmail({
|
||||||
|
email: email,
|
||||||
|
masterPasswordHash: hash
|
||||||
|
}, function () {
|
||||||
|
if (doToast) {
|
||||||
|
toastr.success('Verification email sent to ' + $scope.twoFactorEmail + '.');
|
||||||
|
}
|
||||||
|
}, function () {
|
||||||
|
toastr.error('Could not send verification email.');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDefaultProvider(twoFactorProviders) {
|
||||||
|
var keys = Object.keys(twoFactorProviders);
|
||||||
|
var providerType = null;
|
||||||
|
var providerPriority = -1;
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var provider = $filter('filter')(constants.twoFactorProviderInfo, { type: keys[i], active: true });
|
||||||
|
if (provider.length && provider[0].priority > providerPriority) {
|
||||||
|
if (provider[0].type == constants.twoFactorProvider.u2f &&
|
||||||
|
!utilsService.isChrome() && !utilsService.isOpera()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
providerType = provider[0].type;
|
||||||
|
providerPriority = provider[0].priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parseInt(providerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
$timeout(function () {
|
||||||
|
$('#code').focus();
|
||||||
|
|
||||||
|
if ($scope.providerType === constants.twoFactorProvider.duo) {
|
||||||
|
var params = providers[constants.twoFactorProvider.duo];
|
||||||
|
|
||||||
|
$window.Duo.init({
|
||||||
|
host: params.Host,
|
||||||
|
sig_request: params.Signature,
|
||||||
|
submit_callback: function (theForm) {
|
||||||
|
var response = $(theForm).find('input[name="sig_response"]').val();
|
||||||
|
$scope.login(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if ($scope.providerType === constants.twoFactorProvider.u2f) {
|
||||||
|
var params = providers[constants.twoFactorProvider.u2f];
|
||||||
|
var challenges = JSON.parse(params.Challenges);
|
||||||
|
|
||||||
|
var u2f = new U2f(function (data) {
|
||||||
|
$scope.login(data);
|
||||||
|
$scope.$apply();
|
||||||
|
}, function (error) {
|
||||||
|
toastr.error(error, i18nService.errorsOccurred);
|
||||||
|
$scope.$apply();
|
||||||
|
}, function (info) {
|
||||||
|
if (info === 'ready') {
|
||||||
|
$scope.u2fReady = true;
|
||||||
|
}
|
||||||
|
$scope.$apply();
|
||||||
|
});
|
||||||
|
|
||||||
|
u2f.init({
|
||||||
|
appId: challenges[0].appId,
|
||||||
|
challenge: challenges[0].challenge,
|
||||||
|
keys: [{
|
||||||
|
version: challenges[0].version,
|
||||||
|
keyHandle: challenges[0].keyHandle
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if ($scope.providerType === constants.twoFactorProvider.email) {
|
||||||
|
var params = providers[constants.twoFactorProvider.email];
|
||||||
|
$scope.twoFactorEmail = params.Email;
|
||||||
|
if (Object.keys(providers).length > 1) {
|
||||||
|
$scope.sendEmail(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<form name="theForm" ng-submit="login(model)" bit-form="loginPromise">
|
<form name="theForm" ng-submit="login(token)" bit-form="loginPromise"
|
||||||
|
ng-if="providerType === constantsProvider.authenticator || providerType === constantsProvider.email">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<a ui-sref="login({animation: 'out-slide-right'})"><i class="fa fa-chevron-left"></i> {{i18n.login}}</a>
|
<a ui-sref="login({animation: 'out-slide-right'})"><i class="fa fa-chevron-left"></i> {{i18n.login}}</a>
|
||||||
@ -16,7 +17,7 @@
|
|||||||
<div class="list-section-item list-section-item-icon-input">
|
<div class="list-section-item list-section-item-icon-input">
|
||||||
<i class="fa fa-lock fa-lg fa-fw"></i>
|
<i class="fa fa-lock fa-lg fa-fw"></i>
|
||||||
<label for="code" class="sr-only">{{i18n.verificationCode}}</label>
|
<label for="code" class="sr-only">{{i18n.verificationCode}}</label>
|
||||||
<input id="code" type="text" name="Code" placeholder="{{i18n.verificationCode}}" ng-model="model.code">
|
<input id="code" type="text" name="Code" placeholder="{{i18n.verificationCode}}" ng-model="token">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-section-footer">
|
<div class="list-section-footer">
|
||||||
@ -29,3 +30,63 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form name="theForm" bit-form="loginPromise" ng-if="providerType === constantsProvider.duo">
|
||||||
|
<div class="header">
|
||||||
|
<div class="left">
|
||||||
|
<a ui-sref="login({animation: 'out-slide-right'})"><i class="fa fa-chevron-left"></i> {{i18n.login}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="title">Duo</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div id="duoFrameWrapper">
|
||||||
|
<iframe id="duo_iframe"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form name="theForm" ng-submit="login(token)" bit-form="loginPromise" ng-if="providerType === constantsProvider.yubikey">
|
||||||
|
<div class="header">
|
||||||
|
<div class="left">
|
||||||
|
<a ui-sref="login({animation: 'out-slide-right'})"><i class="fa fa-chevron-left"></i> {{i18n.login}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<button type="submit" class="btn btn-link" ng-show="!theForm.$loading">{{i18n.continue}}</button>
|
||||||
|
<i class="fa fa-spinner fa-lg fa-spin" ng-show="theForm.$loading"></i>
|
||||||
|
</div>
|
||||||
|
<div class="title">YubiKey</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="list">
|
||||||
|
<div class="list-section">
|
||||||
|
<div class="list-section-items">
|
||||||
|
<div class="list-section-item list-section-item-icon-input">
|
||||||
|
<i class="fa fa-lock fa-lg fa-fw"></i>
|
||||||
|
<label for="code" class="sr-only">{{i18n.verificationCode}}</label>
|
||||||
|
<input id="code" type="password" name="Code" ng-model="token">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-section-footer">
|
||||||
|
Touch the YubiKey button.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form name="theForm" bit-form="loginPromise" ng-if="providerType === constantsProvider.u2f">
|
||||||
|
<div class="header">
|
||||||
|
<div class="left">
|
||||||
|
<a ui-sref="login({animation: 'out-slide-right'})"><i class="fa fa-chevron-left"></i> {{i18n.login}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<i class="fa fa-spinner fa-lg fa-spin" ng-show="theForm.$loading"></i>
|
||||||
|
</div>
|
||||||
|
<div class="title">FIDO U2F</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div ng-if="!u2fReady">Loading...</div>
|
||||||
|
<div ng-if="u2fReady">Touch button</div>
|
||||||
|
<iframe id="u2f_iframe"></iframe>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
controller: 'accountsLoginTwoFactorController',
|
controller: 'accountsLoginTwoFactorController',
|
||||||
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
|
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
|
||||||
data: { authorize: false },
|
data: { authorize: false },
|
||||||
params: { animation: null, email: null, masterPassword: null }
|
params: { animation: null, email: null, masterPassword: null, providers: null, provider: null }
|
||||||
})
|
})
|
||||||
.state('register', {
|
.state('register', {
|
||||||
url: '/register',
|
url: '/register',
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
<script src="../lib/papaparse/papaparse.js"></script>
|
<script src="../lib/papaparse/papaparse.js"></script>
|
||||||
<script src="../lib/clipboard/clipboard.js"></script>
|
<script src="../lib/clipboard/clipboard.js"></script>
|
||||||
<script src="../scripts/analytics.js"></script>
|
<script src="../scripts/analytics.js"></script>
|
||||||
|
<script src="../scripts/duo.js"></script>
|
||||||
|
<script src="../scripts/u2f.js"></script>
|
||||||
|
|
||||||
<script src="../lib/angular/angular.js"></script>
|
<script src="../lib/angular/angular.js"></script>
|
||||||
<script src="../lib/angular-animate/angular-animate.js"></script>
|
<script src="../lib/angular-animate/angular-animate.js"></script>
|
||||||
|
@ -493,3 +493,16 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#duoFrameWrapper {
|
||||||
|
background: ~"url('../../images/loading.svg') 0 0 no-repeat";
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
430
src/scripts/duo.js
Normal file
430
src/scripts/duo.js
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
/**
|
||||||
|
* Duo Web SDK v2
|
||||||
|
* Copyright 2017, Duo Security
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (root, factory) {
|
||||||
|
/*eslint-disable */
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define([], factory);
|
||||||
|
/*eslint-enable */
|
||||||
|
} else if (typeof module === 'object' && module.exports) {
|
||||||
|
// Node. Does not work with strict CommonJS, but
|
||||||
|
// only CommonJS-like environments that support module.exports,
|
||||||
|
// like Node.
|
||||||
|
module.exports = factory();
|
||||||
|
} else {
|
||||||
|
// Browser globals (root is window)
|
||||||
|
var Duo = factory();
|
||||||
|
// If the Javascript was loaded via a script tag, attempt to autoload
|
||||||
|
// the frame.
|
||||||
|
Duo._onReady(Duo.init);
|
||||||
|
|
||||||
|
// Attach Duo to the `window` object
|
||||||
|
root.Duo = Duo;
|
||||||
|
}
|
||||||
|
}(this, function () {
|
||||||
|
var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;
|
||||||
|
var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/;
|
||||||
|
var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/;
|
||||||
|
var VALID_OPEN_WINDOW_DOMAINS = [
|
||||||
|
'duo.com',
|
||||||
|
'duosecurity.com',
|
||||||
|
'duomobile.s3-us-west-1.amazonaws.com'
|
||||||
|
];
|
||||||
|
|
||||||
|
var iframeId = 'duo_iframe',
|
||||||
|
postAction = '',
|
||||||
|
postArgument = 'sig_response',
|
||||||
|
host,
|
||||||
|
sigRequest,
|
||||||
|
duoSig,
|
||||||
|
appSig,
|
||||||
|
iframe,
|
||||||
|
submitCallback;
|
||||||
|
|
||||||
|
function throwError(message, url) {
|
||||||
|
throw new Error(
|
||||||
|
'Duo Web SDK error: ' + message +
|
||||||
|
(url ? ('\n' + 'See ' + url + ' for more information') : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hyphenize(str) {
|
||||||
|
return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// cross-browser data attributes
|
||||||
|
function getDataAttribute(element, name) {
|
||||||
|
if ('dataset' in element) {
|
||||||
|
return element.dataset[name];
|
||||||
|
} else {
|
||||||
|
return element.getAttribute('data-' + hyphenize(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cross-browser event binding/unbinding
|
||||||
|
function on(context, event, fallbackEvent, callback) {
|
||||||
|
if ('addEventListener' in window) {
|
||||||
|
context.addEventListener(event, callback, false);
|
||||||
|
} else {
|
||||||
|
context.attachEvent(fallbackEvent, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function off(context, event, fallbackEvent, callback) {
|
||||||
|
if ('removeEventListener' in window) {
|
||||||
|
context.removeEventListener(event, callback, false);
|
||||||
|
} else {
|
||||||
|
context.detachEvent(fallbackEvent, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReady(callback) {
|
||||||
|
on(document, 'DOMContentLoaded', 'onreadystatechange', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function offReady(callback) {
|
||||||
|
off(document, 'DOMContentLoaded', 'onreadystatechange', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(callback) {
|
||||||
|
on(window, 'message', 'onmessage', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function offMessage(callback) {
|
||||||
|
off(window, 'message', 'onmessage', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the sig_request parameter, throwing errors if the token contains
|
||||||
|
* a server error or if the token is invalid.
|
||||||
|
*
|
||||||
|
* @param {String} sig Request token
|
||||||
|
*/
|
||||||
|
function parseSigRequest(sig) {
|
||||||
|
if (!sig) {
|
||||||
|
// nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// see if the token contains an error, throwing it if it does
|
||||||
|
if (sig.indexOf('ERR|') === 0) {
|
||||||
|
throwError(sig.split('|')[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the token
|
||||||
|
if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) {
|
||||||
|
throwError(
|
||||||
|
'Duo was given a bad token. This might indicate a configuration ' +
|
||||||
|
'problem with one of Duo\'s client libraries.',
|
||||||
|
'https://www.duosecurity.com/docs/duoweb#first-steps'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sigParts = sig.split(':');
|
||||||
|
|
||||||
|
// hang on to the token, and the parsed duo and app sigs
|
||||||
|
sigRequest = sig;
|
||||||
|
duoSig = sigParts[0];
|
||||||
|
appSig = sigParts[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
sigRequest: sig,
|
||||||
|
duoSig: sigParts[0],
|
||||||
|
appSig: sigParts[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is set up to run when the DOM is ready, if the iframe was
|
||||||
|
* not available during `init`.
|
||||||
|
*/
|
||||||
|
function onDOMReady() {
|
||||||
|
iframe = document.getElementById(iframeId);
|
||||||
|
|
||||||
|
if (!iframe) {
|
||||||
|
throw new Error(
|
||||||
|
'This page does not contain an iframe for Duo to use.' +
|
||||||
|
'Add an element like <iframe id="duo_iframe"></iframe> ' +
|
||||||
|
'to this page. ' +
|
||||||
|
'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' +
|
||||||
|
'for more information.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we've got an iframe, away we go!
|
||||||
|
ready();
|
||||||
|
|
||||||
|
// always clean up after yourself
|
||||||
|
offReady(onDOMReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a MessageEvent came from the Duo service, and that it
|
||||||
|
* is a properly formatted payload.
|
||||||
|
*
|
||||||
|
* The Google Chrome sign-in page injects some JS into pages that also
|
||||||
|
* make use of postMessage, so we need to do additional validation above
|
||||||
|
* and beyond the origin.
|
||||||
|
*
|
||||||
|
* @param {MessageEvent} event Message received via postMessage
|
||||||
|
*/
|
||||||
|
function isDuoMessage(event) {
|
||||||
|
return Boolean(
|
||||||
|
event.origin === ('https://' + host) &&
|
||||||
|
typeof event.data === 'string' &&
|
||||||
|
(
|
||||||
|
event.data.match(DUO_MESSAGE_FORMAT) ||
|
||||||
|
event.data.match(DUO_ERROR_FORMAT) ||
|
||||||
|
event.data.match(DUO_OPEN_WINDOW_FORMAT)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the request token and prepare for the iframe to become ready.
|
||||||
|
*
|
||||||
|
* All options below can be passed into an options hash to `Duo.init`, or
|
||||||
|
* specified on the iframe using `data-` attributes.
|
||||||
|
*
|
||||||
|
* Options specified using the options hash will take precedence over
|
||||||
|
* `data-` attributes.
|
||||||
|
*
|
||||||
|
* Example using options hash:
|
||||||
|
* ```javascript
|
||||||
|
* Duo.init({
|
||||||
|
* iframe: "some_other_id",
|
||||||
|
* host: "api-main.duo.test",
|
||||||
|
* sig_request: "...",
|
||||||
|
* post_action: "/auth",
|
||||||
|
* post_argument: "resp"
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Example using `data-` attributes:
|
||||||
|
* ```
|
||||||
|
* <iframe id="duo_iframe"
|
||||||
|
* data-host="api-main.duo.test"
|
||||||
|
* data-sig-request="..."
|
||||||
|
* data-post-action="/auth"
|
||||||
|
* data-post-argument="resp"
|
||||||
|
* >
|
||||||
|
* </iframe>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {String} options.iframe The iframe, or id of an iframe to set up
|
||||||
|
* @param {String} options.host Hostname
|
||||||
|
* @param {String} options.sig_request Request token
|
||||||
|
* @param {String} [options.post_action=''] URL to POST back to after successful auth
|
||||||
|
* @param {String} [options.post_argument='sig_response'] Parameter name to use for response token
|
||||||
|
* @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute
|
||||||
|
* the callback function with reference to the "duo_form" form object
|
||||||
|
* submit_callback can be used to prevent the webpage from reloading.
|
||||||
|
*/
|
||||||
|
function init(options) {
|
||||||
|
if (options) {
|
||||||
|
if (options.host) {
|
||||||
|
host = options.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.sig_request) {
|
||||||
|
parseSigRequest(options.sig_request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.post_action) {
|
||||||
|
postAction = options.post_action;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.post_argument) {
|
||||||
|
postArgument = options.post_argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.iframe) {
|
||||||
|
if (options.iframe.tagName) {
|
||||||
|
iframe = options.iframe;
|
||||||
|
} else if (typeof options.iframe === 'string') {
|
||||||
|
iframeId = options.iframe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.submit_callback === 'function') {
|
||||||
|
submitCallback = options.submit_callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we were given an iframe, no need to wait for the rest of the DOM
|
||||||
|
if (false && iframe) {
|
||||||
|
ready();
|
||||||
|
} else {
|
||||||
|
// try to find the iframe in the DOM
|
||||||
|
iframe = document.getElementById(iframeId);
|
||||||
|
|
||||||
|
// iframe is in the DOM, away we go!
|
||||||
|
if (iframe) {
|
||||||
|
ready();
|
||||||
|
} else {
|
||||||
|
// wait until the DOM is ready, then try again
|
||||||
|
onReady(onDOMReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// always clean up after yourself!
|
||||||
|
offReady(init);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called when a message was received from another domain
|
||||||
|
* using the `postMessage` API. Check that the event came from the Duo
|
||||||
|
* service domain, and that the message is a properly formatted payload,
|
||||||
|
* then perform the post back to the primary service.
|
||||||
|
*
|
||||||
|
* @param event Event object (contains origin and data)
|
||||||
|
*/
|
||||||
|
function onReceivedMessage(event) {
|
||||||
|
if (isDuoMessage(event)) {
|
||||||
|
if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) {
|
||||||
|
var url = event.data.substring("DUO_OPEN_WINDOW|".length);
|
||||||
|
if (isValidUrlToOpen(url)) {
|
||||||
|
// Open the URL that comes after the DUO_WINDOW_OPEN token.
|
||||||
|
window.open(url, "_self");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// the event came from duo, do the post back
|
||||||
|
doPostBack(event.data);
|
||||||
|
|
||||||
|
// always clean up after yourself!
|
||||||
|
offMessage(onReceivedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that this passed in URL is one that we will actually allow to
|
||||||
|
* be opened.
|
||||||
|
* @param url String URL that the message poster wants to open
|
||||||
|
* @returns {boolean} true if we allow this url to be opened in the window
|
||||||
|
*/
|
||||||
|
function isValidUrlToOpen(url) {
|
||||||
|
if (!url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parser = document.createElement('a');
|
||||||
|
parser.href = url;
|
||||||
|
|
||||||
|
if (parser.protocol === "duotrustedendpoints:") {
|
||||||
|
return true;
|
||||||
|
} else if (parser.protocol !== "https:") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) {
|
||||||
|
if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) ||
|
||||||
|
parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point the iframe at Duo, then wait for it to postMessage back to us.
|
||||||
|
*/
|
||||||
|
function ready() {
|
||||||
|
if (!host) {
|
||||||
|
host = getDataAttribute(iframe, 'host');
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
throwError(
|
||||||
|
'No API hostname is given for Duo to use. Be sure to pass ' +
|
||||||
|
'a `host` parameter to Duo.init, or through the `data-host` ' +
|
||||||
|
'attribute on the iframe element.',
|
||||||
|
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!duoSig || !appSig) {
|
||||||
|
parseSigRequest(getDataAttribute(iframe, 'sigRequest'));
|
||||||
|
|
||||||
|
if (!duoSig || !appSig) {
|
||||||
|
throwError(
|
||||||
|
'No valid signed request is given. Be sure to give the ' +
|
||||||
|
'`sig_request` parameter to Duo.init, or use the ' +
|
||||||
|
'`data-sig-request` attribute on the iframe element.',
|
||||||
|
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if postAction/Argument are defaults, see if they are specified
|
||||||
|
// as data attributes on the iframe
|
||||||
|
if (postAction === '') {
|
||||||
|
postAction = getDataAttribute(iframe, 'postAction') || postAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postArgument === 'sig_response') {
|
||||||
|
postArgument = getDataAttribute(iframe, 'postArgument') || postArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
// point the iframe at Duo
|
||||||
|
iframe.src = [
|
||||||
|
'https://', host, '/frame/web/v1/auth?tx=', duoSig,
|
||||||
|
'&parent=', encodeURIComponent(document.location.href),
|
||||||
|
'&v=2.6'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
// listen for the 'message' event
|
||||||
|
onMessage(onReceivedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We received a postMessage from Duo. POST back to the primary service
|
||||||
|
* with the response token, and any additional user-supplied parameters
|
||||||
|
* given in form#duo_form.
|
||||||
|
*/
|
||||||
|
function doPostBack(response) {
|
||||||
|
// create a hidden input to contain the response token
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = postArgument;
|
||||||
|
input.value = response + ':' + appSig;
|
||||||
|
|
||||||
|
// user may supply their own form with additional inputs
|
||||||
|
var form = document.getElementById('duo_form');
|
||||||
|
|
||||||
|
// if the form doesn't exist, create one
|
||||||
|
if (!form) {
|
||||||
|
form = document.createElement('form');
|
||||||
|
|
||||||
|
// insert the new form after the iframe
|
||||||
|
iframe.parentElement.insertBefore(form, iframe.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we are actually posting to the right place
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = postAction;
|
||||||
|
|
||||||
|
// add the response token input to the form
|
||||||
|
form.appendChild(input);
|
||||||
|
|
||||||
|
// away we go!
|
||||||
|
if (typeof submitCallback === "function") {
|
||||||
|
submitCallback.call(null, form);
|
||||||
|
} else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
_onReady: onReady,
|
||||||
|
_parseSigRequest: parseSigRequest,
|
||||||
|
_isDuoMessage: isDuoMessage,
|
||||||
|
_doPostBack: doPostBack
|
||||||
|
};
|
||||||
|
}));
|
50
src/scripts/u2f.js
Normal file
50
src/scripts/u2f.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
function U2f(successCallback, errorCallback, infoCallback) {
|
||||||
|
this.success = successCallback;
|
||||||
|
this.error = errorCallback;
|
||||||
|
this.info = infoCallback;
|
||||||
|
this.iframe = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
U2f.prototype.init = function (data) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
iframe = document.getElementById('u2f_iframe');
|
||||||
|
iframe.src = 'https://vault.bitwarden.com/u2f-connector.html' +
|
||||||
|
'?data=' + this.base64Encode(JSON.stringify(data)) +
|
||||||
|
'&parent=' + encodeURIComponent(document.location.href) +
|
||||||
|
'&v=1';
|
||||||
|
|
||||||
|
window.addEventListener('message', function (event) {
|
||||||
|
if (!self.validMessage(event)) {
|
||||||
|
self.error('Invalid message.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = event.data.split('|');
|
||||||
|
if (parts[0] === 'success' && self.success) {
|
||||||
|
self.success(parts[1]);
|
||||||
|
}
|
||||||
|
else if (parts[0] === 'error' && self.error) {
|
||||||
|
self.error(parts[1]);
|
||||||
|
}
|
||||||
|
else if (parts[0] === 'info') {
|
||||||
|
if (self.info) {
|
||||||
|
self.info(parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
U2f.prototype.validMessage = function (event) {
|
||||||
|
if (event.origin !== 'https://vault.bitwarden.com') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.data.indexOf('success|') === 0 || event.data.indexOf('error|') === 0 || event.data.indexOf('info|') === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
U2f.prototype.base64Encode = function (str) {
|
||||||
|
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
|
||||||
|
return String.fromCharCode('0x' + p1);
|
||||||
|
}));
|
||||||
|
}
|
@ -43,9 +43,9 @@ function initApiService() {
|
|||||||
success(new IdentityTokenResponse(response));
|
success(new IdentityTokenResponse(response));
|
||||||
},
|
},
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
error: function (jqXHR, textStatus, errorThrown) {
|
||||||
if (jqXHR.responseJSON && jqXHR.responseJSON.TwoFactorProviders &&
|
if (jqXHR.responseJSON && jqXHR.responseJSON.TwoFactorProviders2 &&
|
||||||
jqXHR.responseJSON.TwoFactorProviders.length) {
|
Object.keys(jqXHR.responseJSON.TwoFactorProviders2).length) {
|
||||||
successWithTwoFactor();
|
successWithTwoFactor(jqXHR.responseJSON.TwoFactorProviders2);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
error(new ErrorResponse(jqXHR, true));
|
error(new ErrorResponse(jqXHR, true));
|
||||||
@ -54,6 +54,23 @@ function initApiService() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Two Factor APIs
|
||||||
|
|
||||||
|
ApiService.prototype.postTwoFactorEmail = function (success, error) {
|
||||||
|
var self = this;
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: self.baseUrl + '/two-factor/send-email?' + token,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (response) {
|
||||||
|
success(response);
|
||||||
|
},
|
||||||
|
error: function (jqXHR, textStatus, errorThrown) {
|
||||||
|
handleError(error, jqXHR, false, self);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Account APIs
|
// Account APIs
|
||||||
|
|
||||||
ApiService.prototype.getAccountRevisionDate = function (success, error) {
|
ApiService.prototype.getAccountRevisionDate = function (success, error) {
|
||||||
|
@ -13,6 +13,58 @@ function ConstantsService() {
|
|||||||
Rsa2048_OaepSha1_B64: 4,
|
Rsa2048_OaepSha1_B64: 4,
|
||||||
Rsa2048_OaepSha256_HmacSha256_B64: 5,
|
Rsa2048_OaepSha256_HmacSha256_B64: 5,
|
||||||
Rsa2048_OaepSha1_HmacSha256_B64: 6
|
Rsa2048_OaepSha1_HmacSha256_B64: 6
|
||||||
|
},
|
||||||
|
twoFactorProvider: {
|
||||||
|
u2f: 4,
|
||||||
|
yubikey: 3,
|
||||||
|
duo: 2,
|
||||||
|
authenticator: 0,
|
||||||
|
email: 1,
|
||||||
|
remember: 5
|
||||||
|
},
|
||||||
|
twoFactorProviderInfo: [
|
||||||
|
{
|
||||||
|
type: 0,
|
||||||
|
name: 'Authenticator App',
|
||||||
|
description: 'Use an authenticator app (such as Authy or Google Authenticator) to generate time-based ' +
|
||||||
|
'verification codes.',
|
||||||
|
active: true,
|
||||||
|
free: true,
|
||||||
|
displayOrder: 0,
|
||||||
|
priority: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
name: 'YubiKey OTP Security Key',
|
||||||
|
description: 'Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices.',
|
||||||
|
active: true,
|
||||||
|
displayOrder: 1,
|
||||||
|
priority: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
name: 'Duo',
|
||||||
|
description: 'Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.',
|
||||||
|
active: true,
|
||||||
|
displayOrder: 2,
|
||||||
|
priority: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 4,
|
||||||
|
name: 'FIDO U2F Security Key',
|
||||||
|
description: 'Use any FIDO U2F enabled security key to access your account.',
|
||||||
|
active: true,
|
||||||
|
displayOrder: 3,
|
||||||
|
priority: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
name: 'Email',
|
||||||
|
description: 'Verification codes will be emailed to you.',
|
||||||
|
active: true,
|
||||||
|
displayOrder: 4,
|
||||||
|
priority: 0
|
||||||
}
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user