1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-24 12:06:15 +01:00

two-factor support in browser extensions

This commit is contained in:
Kyle Spearrin 2017-06-26 15:37:15 -04:00
parent 981fd22ce5
commit 7815af24e5
11 changed files with 748 additions and 19 deletions

6
src/images/loading.svg Normal file
View 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>

View File

@ -40,7 +40,8 @@
$state.go('twoFactor', {
animation: 'in-slide-left',
email: model.email,
masterPassword: model.masterPassword
masterPassword: model.masterPassword,
providers: response.twoFactorProviders
});
}
else {

View File

@ -2,23 +2,30 @@
.module('bit.accounts')
.controller('accountsLoginTwoFactorController', function ($scope, $state, authService, toastr, utilsService,
$analytics, i18nService, $stateParams) {
$analytics, i18nService, $stateParams, $filter, constantsService, $timeout, $window, cryptoService) {
$scope.i18n = i18nService;
$scope.model = {};
utilsService.initListSectionItemListeners($(document), angular);
$('#code').focus();
var constants = constantsService;
var email = $stateParams.email;
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.login = function (model) {
if (!model.code) {
$scope.login = function (token) {
if (!token) {
toastr.error(i18nService.verificationCodeRequired, i18nService.errorsOccurred);
return;
}
$scope.loginPromise = authService.logIn(email, masterPassword, 0, model.code);
$scope.loginPromise = authService.logIn(email, masterPassword, $scope.providerType, token);
$scope.loginPromise.then(function () {
$analytics.eventTrack('Logged In From Two-step');
$state.go('tabs.vault', { animation: 'in-slide-left', syncOnLoad: true });
@ -29,4 +36,94 @@
$analytics.eventTrack('Selected Lost 2FA App');
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);
}
});

View File

@ -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="left">
<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">
<i class="fa fa-lock fa-lg fa-fw"></i>
<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 class="list-section-footer">
@ -29,3 +30,63 @@
</p>
</div>
</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>

View File

@ -66,7 +66,7 @@
controller: 'accountsLoginTwoFactorController',
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
data: { authorize: false },
params: { animation: null, email: null, masterPassword: null }
params: { animation: null, email: null, masterPassword: null, providers: null, provider: null }
})
.state('register', {
url: '/register',

View File

@ -17,6 +17,8 @@
<script src="../lib/papaparse/papaparse.js"></script>
<script src="../lib/clipboard/clipboard.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-animate/angular-animate.js"></script>

View File

@ -493,3 +493,16 @@
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
View 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
View 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);
}));
}

View File

@ -1,21 +1,21 @@
function ApiService(tokenService, appIdService, utilsService, logoutCallback) {
// Desktop
// Desktop
//this.baseUrl = 'http://localhost:4000';
//this.identityBaseUrl = 'http://localhost:33656';
// Desktop HTTPS
// Desktop HTTPS
//this.baseUrl = 'https://localhost:44377';
//this.identityBaseUrl = 'https://localhost:44392';
// Desktop external
// Desktop external
//this.baseUrl = 'http://192.168.1.6:4000';
//this.identityBaseUrl = 'http://192.168.1.6:33656';
// Preview
// Preview
//this.baseUrl = 'https://preview-api.bitwarden.com';
//this.identityBaseUrl = 'https://preview-identity.bitwarden.com';
// Production
// Production
this.baseUrl = 'https://api.bitwarden.com';
this.identityBaseUrl = 'https://identity.bitwarden.com';
@ -43,9 +43,9 @@ function initApiService() {
success(new IdentityTokenResponse(response));
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.responseJSON && jqXHR.responseJSON.TwoFactorProviders &&
jqXHR.responseJSON.TwoFactorProviders.length) {
successWithTwoFactor();
if (jqXHR.responseJSON && jqXHR.responseJSON.TwoFactorProviders2 &&
Object.keys(jqXHR.responseJSON.TwoFactorProviders2).length) {
successWithTwoFactor(jqXHR.responseJSON.TwoFactorProviders2);
}
else {
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
ApiService.prototype.getAccountRevisionDate = function (success, error) {

View File

@ -13,6 +13,58 @@ function ConstantsService() {
Rsa2048_OaepSha1_B64: 4,
Rsa2048_OaepSha256_HmacSha256_B64: 5,
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
}
]
};
};