feat(totp): initial totp implementation (#5962), r=@vladikoff
This commit is contained in:
Родитель
bc6e567c9b
Коммит
8a3b610c93
|
@ -0,0 +1,26 @@
|
|||
<svg width="122" height="138" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient x1="-170.039%" y1="49.981%" x2="257.195%" y2="49.981%" id="b">
|
||||
<stop stop-color="#00C8D7" offset="0%"/>
|
||||
<stop stop-color="#0A84FF" offset="100%"/>
|
||||
</linearGradient>
|
||||
<rect id="a" x="18" width="76" height="130" rx="4"/>
|
||||
<path d="M45-8h22a1 1 0 0 1 1 1v110a1 1 0 0 1-1 1H45a1 1 0 0 1-1-1V-7a1 1 0 0 1 1-1z" id="c"/>
|
||||
</defs>
|
||||
<g transform="translate(5 4)" fill="none" fill-rule="evenodd">
|
||||
<use fill="#FFF" xlink:href="#a"/>
|
||||
<rect stroke="url(#b)" stroke-width="4" x="16" y="-2" width="80" height="134" rx="4"/>
|
||||
<rect fill="#CCEDF0" x="26" y="10" width="60" height="100" rx="1"/>
|
||||
<g transform="rotate(-90 56 48)">
|
||||
<use fill="#FFF" xlink:href="#c"/>
|
||||
<path stroke="#FFF" stroke-width="5"
|
||||
d="M45-10.5h22A3.5 3.5 0 0 1 70.5-7v110a3.5 3.5 0 0 1-3.5 3.5H45a3.5 3.5 0 0 1-3.5-3.5V-7a3.5 3.5 0 0 1 3.5-3.5z"/>
|
||||
<path stroke="url(#b)" stroke-width="3"
|
||||
d="M45-9.5h22A2.5 2.5 0 0 1 69.5-7v110a2.5 2.5 0 0 1-2.5 2.5H45a2.5 2.5 0 0 1-2.5-2.5V-7A2.5 2.5 0 0 1 45-9.5z"/>
|
||||
</g>
|
||||
<path d="M32.986 48.216l-3.446.781 2.342 2.748L30.261 53l-1.78-3.174L26.726 53l-1.621-1.232 2.32-2.77L24 48.215l.608-1.942 3.198 1.444L27.468 44h2.027l-.338 3.742 3.198-1.492.631 1.966zm10.638 0l-3.446.781 2.342 2.748L40.899 53l-1.78-3.174L37.363 53l-1.622-1.232 2.32-2.77-3.423-.782.608-1.942 3.198 1.444L38.106 44h2.027l-.338 3.742 3.198-1.492.63 1.966zm10.637 0l-3.446.781 2.343 2.748L51.536 53l-1.78-3.174L48 53l-1.621-1.232 2.32-2.77-3.424-.782.608-1.942 3.198 1.444L48.743 44h2.027l-.337 3.742 3.198-1.492.63 1.966zm10.638 0l-3.446.781 2.342 2.748L62.174 53l-1.78-3.174L58.638 53l-1.622-1.232 2.32-2.77-3.423-.782.608-1.942 3.198 1.444L59.38 44h2.027l-.338 3.742 3.198-1.492.63 1.966zm10.637 0l-3.445.781 2.342 2.748L72.81 53l-1.779-3.174L69.275 53l-1.621-1.232 2.32-2.77-3.424-.782.608-1.942 3.198 1.444L70.02 44h2.027l-.338 3.742 3.198-1.492.63 1.966zm10.638 0l-3.446.781 2.342 2.748L83.45 53l-1.78-3.174L79.914 53l-1.622-1.232 2.32-2.77-3.423-.782.608-1.942 3.198 1.444L80.656 44h2.027l-.338 3.742 3.198-1.492.63 1.966z"
|
||||
fill="url(#b)"/>
|
||||
<rect fill="#CCEDF0" x="50" y="4" width="12" height="1" rx=".5"/>
|
||||
<circle fill="#CCEDF0" cx="56" cy="120" r="6"/>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 2.4 KiB |
|
@ -206,6 +206,10 @@ define(function (require, exports, module) {
|
|||
errno: 153,
|
||||
message: t('This verification code has expired')
|
||||
},
|
||||
INVALID_TOTP_CODE: {
|
||||
errno: 154,
|
||||
message: t('Invalid two-step authentication code')
|
||||
},
|
||||
// Secondary Email errors end
|
||||
SERVER_BUSY: {
|
||||
errno: 201,
|
||||
|
|
|
@ -962,7 +962,41 @@ define(function (require, exports, module) {
|
|||
* @param {String} code tokenCode
|
||||
* @returns {Promise} resolves when complete
|
||||
*/
|
||||
verifyTokenCode: createClientDelegate('verifyTokenCode')
|
||||
verifyTokenCode: createClientDelegate('verifyTokenCode'),
|
||||
|
||||
/**
|
||||
* Creates a new TOTP token for the current user.
|
||||
*
|
||||
* @param {String} sessionToken SessionToken obtained from signIn
|
||||
* @returns {Promise} resolves when complete
|
||||
*/
|
||||
createTotpToken: createClientDelegate('createTotpToken'),
|
||||
|
||||
/**
|
||||
* Deletes the current user's TOTP token.
|
||||
*
|
||||
* @param {String} sessionToken SessionToken obtained from signIn
|
||||
* @returns {Promise} resolves when complete
|
||||
*/
|
||||
deleteTotpToken: createClientDelegate('deleteTotpToken'),
|
||||
|
||||
/**
|
||||
* Checks to see if the current user has a TOTP token.
|
||||
*
|
||||
* @param {String} sessionToken SessionToken obtained from signIn
|
||||
* @returns {Promise} resolves when complete
|
||||
*/
|
||||
checkTotpTokenExists: createClientDelegate('checkTotpTokenExists'),
|
||||
|
||||
/**
|
||||
* Checks to see if the TOTP code is valid and verifies the TOTP token
|
||||
* if it is.
|
||||
*
|
||||
* @param {String} sessionToken SessionToken obtained from signIn
|
||||
* @param {String} code TOTP code
|
||||
* @returns {Promise} resolves when complete
|
||||
*/
|
||||
verifyTotpCode: createClientDelegate('verifyTotpCode')
|
||||
};
|
||||
|
||||
module.exports = FxaClientWrapper;
|
||||
|
|
|
@ -39,6 +39,7 @@ define(function (require, exports, module) {
|
|||
const SettingsView = require('../views/settings');
|
||||
const SignInBouncedView = require('../views/sign_in_bounced');
|
||||
const SignInTokenCodeView = require('../views/sign_in_token_code');
|
||||
const SignInTotpCodeView = require('../views/sign_in_totp_code');
|
||||
const SignInPasswordView = require('../views/sign_in_password');
|
||||
const SignInReportedView = require('../views/sign_in_reported');
|
||||
const SignInUnblockView = require('../views/sign_in_unblock');
|
||||
|
@ -49,6 +50,7 @@ define(function (require, exports, module) {
|
|||
const SmsSentView = require('../views/sms_sent');
|
||||
const Storage = require('./storage');
|
||||
const TosView = require('../views/tos');
|
||||
const TwoStepAuthenticationView = require('../views/settings/two_step_authentication');
|
||||
const VerificationReasons = require('./verification-reasons');
|
||||
const WhyConnectAnotherDeviceView = require('../views/why_connect_another_device');
|
||||
|
||||
|
@ -107,12 +109,14 @@ define(function (require, exports, module) {
|
|||
'settings/delete_account(/)': createChildViewHandler(DeleteAccountView, SettingsView),
|
||||
'settings/display_name(/)': createChildViewHandler(DisplayNameView, SettingsView),
|
||||
'settings/emails(/)': createChildViewHandler(EmailsView, SettingsView),
|
||||
'settings/two_step_authentication(/)': createChildViewHandler(TwoStepAuthenticationView, SettingsView),
|
||||
'signin(/)': 'onSignIn',
|
||||
'signin_bounced(/)': createViewHandler(SignInBouncedView),
|
||||
'signin_confirmed(/)': createViewHandler(ReadyView, { type: VerificationReasons.SIGN_IN }),
|
||||
'signin_permissions(/)': createViewHandler(PermissionsView, { type: VerificationReasons.SIGN_IN }),
|
||||
'signin_reported(/)': createViewHandler(SignInReportedView),
|
||||
'signin_token_code(/)': createViewHandler(SignInTokenCodeView),
|
||||
'signin_totp_code(/)': createViewHandler(SignInTotpCodeView),
|
||||
'signin_unblock(/)': createViewHandler(SignInUnblockView),
|
||||
'signin_verified(/)': createViewHandler(ReadyView, { type: VerificationReasons.SIGN_IN }),
|
||||
'signup(/)': 'onSignUp',
|
||||
|
|
|
@ -15,6 +15,7 @@ define(function (require, exports, module) {
|
|||
EMAIL: 'email',
|
||||
EMAIL_2FA: 'email-2fa',
|
||||
EMAIL_CAPTCHA: 'email-captcha',
|
||||
TOTP_2FA: 'totp-2fa'
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1228,7 +1228,61 @@ define(function (require, exports, module) {
|
|||
this.get('sessionToken'),
|
||||
email
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new TOTP token for a user.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createTotpToken () {
|
||||
return this._fxaClient.createTotpToken(
|
||||
this.get('sessionToken'),
|
||||
{
|
||||
metricsContext: this._metrics.getFlowEventMetadata()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the current TOTP token for a user.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
deleteTotpToken () {
|
||||
return this._fxaClient.deleteTotpToken(
|
||||
this.get('sessionToken')
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies a TOTP code. If code is verified, token will be marked as verified.
|
||||
*
|
||||
* @param {String} code
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
verifyTotpCode (code) {
|
||||
return this._fxaClient.verifyTotpCode(
|
||||
this.get('sessionToken'),
|
||||
code,
|
||||
{
|
||||
metricsContext: this._metrics.getFlowEventMetadata(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check to see if the current user has a verified TOTP token.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
checkTotpTokenExists () {
|
||||
return this._fxaClient.checkTotpTokenExists(
|
||||
this.get('sessionToken')
|
||||
);
|
||||
}
|
||||
|
||||
}, {
|
||||
ALLOWED_KEYS: ALLOWED_KEYS,
|
||||
PERMISSIONS_TO_KEYS: PERMISSIONS_TO_KEYS
|
||||
|
|
|
@ -75,7 +75,7 @@ define(function (require, exports, module) {
|
|||
success: t('Secondary email verified successfully')
|
||||
}),
|
||||
afterCompleteSignIn: new NavigateBehavior('signin_verified'),
|
||||
afterCompleteSignInTokenCode: new NavigateBehavior('settings'),
|
||||
afterCompleteSignInWithCode: new NavigateBehavior('settings'),
|
||||
afterCompleteSignUp: new NavigateBehavior('signup_verified'),
|
||||
afterDeleteAccount: new NullBehavior(),
|
||||
afterForceAuth: new NavigateBehavior('signin_confirmed'),
|
||||
|
@ -239,8 +239,8 @@ define(function (require, exports, module) {
|
|||
.then(() => this.getBehavior('afterCompleteSignIn'));
|
||||
},
|
||||
|
||||
afterCompleteSignInTokenCode () {
|
||||
return Promise.resolve(this.getBehavior('afterCompleteSignInTokenCode'));
|
||||
afterCompleteSignInWithCode () {
|
||||
return Promise.resolve(this.getBehavior('afterCompleteSignInWithCode'));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -81,7 +81,11 @@ define(function (require, exports, module) {
|
|||
*/
|
||||
_notifyRelierOfLogin (account) {
|
||||
return proto._notifyRelierOfLogin.call(this, account);
|
||||
}
|
||||
},
|
||||
|
||||
afterCompleteSignInWithCode (account) {
|
||||
return this._notifyRelierOfLogin(account);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = FxiOSV1AuthenticationBroker;
|
||||
|
|
|
@ -42,7 +42,7 @@ define(function (require, exports, module) {
|
|||
return channel;
|
||||
},
|
||||
|
||||
afterCompleteSignInTokenCode (account) {
|
||||
afterCompleteSignInWithCode (account) {
|
||||
return this._notifyRelierOfLogin(account)
|
||||
.then(() => proto.afterSignInConfirmationPoll.call(this, account));
|
||||
},
|
||||
|
|
|
@ -212,7 +212,7 @@ define(function (require, exports, module) {
|
|||
.then(() => proto.afterSignInConfirmationPoll.call(this, account));
|
||||
},
|
||||
|
||||
afterCompleteSignInTokenCode (account) {
|
||||
afterCompleteSignInWithCode (account) {
|
||||
return this.finishOAuthSignInFlow(account)
|
||||
.then(() => proto.afterSignIn.call(this, account));
|
||||
},
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<div id="totp-section" class="settings-unit {{#isPanelOpen}}open{{/isPanelOpen}} totp-panel">
|
||||
<div class="settings-unit-stub">
|
||||
<header class="settings-unit-summary">
|
||||
<h2 class="settings-unit-title">{{#t}}Two-step authentication{{/t}}</h2>
|
||||
</header>
|
||||
|
||||
{{^hasToken}}
|
||||
<button class="settings-button primary settings-unit-toggle totp-create" data-href="settings/two_step_authentication">
|
||||
{{#t}}Enable…{{/t}}
|
||||
</button>
|
||||
{{/hasToken}}
|
||||
|
||||
{{#hasToken}}
|
||||
<button class="settings-button secondary settings-unit-toggle" data-href="settings/two_step_authentication">
|
||||
{{#t}}Change…{{/t}}
|
||||
</button>
|
||||
{{/hasToken}}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-unit-details">
|
||||
<form novalidate>
|
||||
<p>
|
||||
{{#t}}Add an extra layer of security to your account by requiring security codes.{{/t}}
|
||||
</p>
|
||||
|
||||
<ul id="totp-status" class="totp-list">
|
||||
<li class="totp-type">
|
||||
<div class="name">Current status</div>
|
||||
<div class="details">
|
||||
{{#hasToken}}
|
||||
<div class="enabled">{{#t}}Enabled{{/t}}</div>
|
||||
{{/hasToken}}
|
||||
{{^hasToken}}
|
||||
<div class="disabled">{{#t}}Disabled{{/t}}</div>
|
||||
{{/hasToken}}
|
||||
</div>
|
||||
<div class="settings-button-group">
|
||||
{{#hasToken}}
|
||||
<button class="settings-button warning totp-delete">{{#t}}Disable{{/t}}</button>
|
||||
{{/hasToken}}
|
||||
{{^hasToken}}
|
||||
<button class="settings-button secondary totp-create">{{#t}}Enable{{/t}}</button>
|
||||
{{/hasToken}}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="totp" class="totp-details hidden">
|
||||
<div class="qr-image-container">
|
||||
<img class="qr-image"/>
|
||||
</div>
|
||||
|
||||
<p class="setup-description">
|
||||
{{#t}}Scan the QR code in your app and confirm it is setup correctly by entering the security code it provides.{{/t}}
|
||||
<a class="show-code-link">{{#t}}Can't scan code?{{/t}}</a>
|
||||
</p>
|
||||
|
||||
<div class="manual-code hidden">
|
||||
<p class="setup-description">{{#t}}Manually enter this secret key into your authentication app:{{/t}}</p>
|
||||
<div class="code"></div>
|
||||
</div>
|
||||
|
||||
<div class="input-row">
|
||||
<label class="label-helper"></label>
|
||||
<input type="text" class="text totp-code tooltip-below" placeholder="{{#t}}Security code{{/t}}" value="{{ code }}" required autofocus autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
{{#hasToken}}
|
||||
<button type="submit" class="settings-button primary totp-refresh">{{#t}}Refresh{{/t}}</button>
|
||||
{{/hasToken}}
|
||||
|
||||
{{^hasToken}}
|
||||
<button type="submit" class="settings-button primary totp-refresh">{{#t}}Refresh{{/t}}</button>
|
||||
<button type="submit" class="settings-button primary hidden totp-confirm-code">{{#t}}Confirm{{/t}}</button>
|
||||
{{/hasToken}}
|
||||
|
||||
<button class="settings-button cancel secondary enabled totp-cancel">{{#t}}Cancel{{/t}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,33 @@
|
|||
<div id="main-content" class="card">
|
||||
<header>
|
||||
<h1 id="fxa-totp-code-header">{{#t}}Enter security code{{/t}}
|
||||
{{#serviceName}}
|
||||
<span class="service">{{#t}}Continue to %(serviceName)s{{/t}}</span>
|
||||
{{/serviceName}}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div class="error"></div>
|
||||
<div class="success"></div>
|
||||
|
||||
<div class="graphic graphic-two-factor-auth"></div>
|
||||
|
||||
<p class="verification-totp-message">
|
||||
{{#t}}Open your authentication app and enter the security code it provides.{{/t}}
|
||||
</p>
|
||||
|
||||
<form novalidate>
|
||||
<div class="input-row token-code-row">
|
||||
<input type="text" class="tooltip-below totp-code" placeholder="{{#t}}Enter 6-digit code{{/t}}" required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button type="submit" class="use-logged-in">{{#t}}Verify{{/t}}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="links"></div>
|
||||
</section>
|
||||
</div>
|
|
@ -33,6 +33,10 @@ define(function (require, exports, module) {
|
|||
this.navigate('settings/clients');
|
||||
},
|
||||
|
||||
_returnToTwoFactorAuthentication () {
|
||||
this.navigate('settings/two_step_authentication');
|
||||
},
|
||||
|
||||
_returnToSettings () {
|
||||
this.navigate('settings');
|
||||
},
|
||||
|
|
|
@ -149,6 +149,11 @@ define(function (require, exports, module) {
|
|||
return this.navigate('signin_token_code', {account});
|
||||
}
|
||||
|
||||
if (verificationReason === VerificationReasons.SIGN_IN &&
|
||||
verificationMethod === VerificationMethods.TOTP_2FA) {
|
||||
return this.navigate('signin_totp_code', {account});
|
||||
}
|
||||
|
||||
return this.navigate('confirm', {account});
|
||||
}
|
||||
|
||||
|
|
|
@ -31,10 +31,13 @@ define(function (require, exports, module) {
|
|||
const Template = require('templates/settings.mustache');
|
||||
const UserAgentMixin = require('../lib/user-agent-mixin');
|
||||
|
||||
const TwoStepAuthenticationView = require('./settings/two_step_authentication');
|
||||
|
||||
var PANEL_VIEWS = [
|
||||
AvatarView,
|
||||
DisplayNameView,
|
||||
EmailsView,
|
||||
TwoStepAuthenticationView,
|
||||
ClientsView,
|
||||
ClientDisconnectView,
|
||||
CommunicationPreferencesView,
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
const AvatarMixin = require('../mixins/avatar-mixin');
|
||||
const AuthErrors = require('lib/auth-errors');
|
||||
const BaseView = require('../base');
|
||||
const Cocktail = require('cocktail');
|
||||
const FloatingPlaceholderMixin = require('../mixins/floating-placeholder-mixin');
|
||||
const FormView = require('../form');
|
||||
const SettingsPanelMixin = require('../mixins/settings-panel-mixin');
|
||||
const SearchParamMixin = require('../../lib/search-param-mixin');
|
||||
const Template = require('templates/settings/two_step_authentication.mustache');
|
||||
const preventDefaultThen = require('../base').preventDefaultThen;
|
||||
const showProgressIndicator = require('../decorators/progress_indicator');
|
||||
|
||||
var t = BaseView.t;
|
||||
|
||||
const CODE_INPUT_SELECTOR = 'input.totp-code';
|
||||
const CODE_REFRESH_SELECTOR = 'button.settings-button.totp-refresh';
|
||||
const CODE_REFRESH_DELAY_MS = 350;
|
||||
|
||||
const View = FormView.extend({
|
||||
template: Template,
|
||||
className: 'two-step-authentication',
|
||||
viewName: 'settings.two-step-authentication',
|
||||
|
||||
events: {
|
||||
'click .show-code-link': preventDefaultThen('_showCode'),
|
||||
'click .totp-cancel': preventDefaultThen('cancel'),
|
||||
'click .totp-confirm-code': preventDefaultThen('confirmCode'),
|
||||
'click .totp-create': preventDefaultThen('createToken'),
|
||||
'click .totp-delete': preventDefaultThen('deleteToken'),
|
||||
'click .totp-refresh': preventDefaultThen('refresh'),
|
||||
},
|
||||
|
||||
_checkTokenExists() {
|
||||
const account = this.getSignedInAccount();
|
||||
return account.checkTotpTokenExists()
|
||||
.then((result) => {
|
||||
this._hasToken = result.exists;
|
||||
});
|
||||
},
|
||||
|
||||
_sanitizeCode(code) {
|
||||
// Remove spaces and `-`
|
||||
return code.replace(/[- ]*/g, '');
|
||||
},
|
||||
|
||||
_showQrCode() {
|
||||
this.$('#totp').removeClass('hidden');
|
||||
},
|
||||
|
||||
_hideStatus() {
|
||||
this.$('.totp-list').addClass('hidden');
|
||||
this.$('.totp-refresh').addClass('hidden');
|
||||
this.$('.totp-confirm-code').removeClass('hidden');
|
||||
},
|
||||
|
||||
_showCode() {
|
||||
this.$('.manual-code').removeClass('hidden');
|
||||
this.$('.show-code-link').addClass('hidden');
|
||||
},
|
||||
|
||||
_getFormattedCode(code) {
|
||||
// Insert spaces every 4 characters
|
||||
return code.replace(/(\w{4})/g, '$1 ');
|
||||
},
|
||||
|
||||
_isPanelEnabled() {
|
||||
if (this.getSearchParam('showTwoStepAuthentication')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
beforeRender() {
|
||||
// This panel is currently behind a feature flag. Only show it
|
||||
// when the query param `showTwoStepAuthentication` is specified.
|
||||
if (! this._isPanelEnabled()) {
|
||||
return this.remove();
|
||||
} else {
|
||||
return this._checkTokenExists();
|
||||
}
|
||||
},
|
||||
|
||||
initialize() {
|
||||
this._hasToken = false;
|
||||
this._statusVisible = true;
|
||||
},
|
||||
|
||||
setInitialContext(context) {
|
||||
context.set({
|
||||
hasToken: this._hasToken,
|
||||
isPanelOpen: this.isPanelOpen(),
|
||||
statusVisible: this._statusVisible
|
||||
});
|
||||
},
|
||||
|
||||
cancel() {
|
||||
return this.render()
|
||||
.then(() => this.navigate('/settings'));
|
||||
},
|
||||
|
||||
createToken() {
|
||||
const account = this.getSignedInAccount();
|
||||
return account.createTotpToken()
|
||||
.then(result => {
|
||||
this.$('.qr-image').attr('src', result.qrCodeUrl);
|
||||
this.$('.code').html(this._getFormattedCode(result.secret));
|
||||
this._showQrCode();
|
||||
this._hideStatus();
|
||||
});
|
||||
},
|
||||
|
||||
deleteToken() {
|
||||
const account = this.getSignedInAccount();
|
||||
return account.deleteTotpToken()
|
||||
.then(() => {
|
||||
this.displaySuccess(t('Two-step authentication removed'), {
|
||||
closePanel: true
|
||||
});
|
||||
return this.render();
|
||||
})
|
||||
.then(() => this.navigate('/settings'));
|
||||
},
|
||||
|
||||
confirmCode() {
|
||||
const account = this.getSignedInAccount();
|
||||
const code = this._sanitizeCode(this.getElementValue('input.totp-code'));
|
||||
return account.verifyTotpCode(code)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
this.displaySuccess(t('Two-step authentication enabled'), {});
|
||||
this.render();
|
||||
} else {
|
||||
throw AuthErrors.toError('INVALID_TOTP_CODE');
|
||||
}
|
||||
})
|
||||
.catch((err) => this.showValidationError(this.$(CODE_INPUT_SELECTOR), err));
|
||||
|
||||
},
|
||||
|
||||
submit() {
|
||||
return this.confirmCode();
|
||||
},
|
||||
|
||||
refresh: showProgressIndicator(function () {
|
||||
return this.render();
|
||||
}, CODE_REFRESH_SELECTOR, CODE_REFRESH_DELAY_MS),
|
||||
|
||||
});
|
||||
|
||||
Cocktail.mixin(
|
||||
View,
|
||||
AvatarMixin,
|
||||
SettingsPanelMixin,
|
||||
FloatingPlaceholderMixin,
|
||||
SearchParamMixin
|
||||
);
|
||||
|
||||
module.exports = View;
|
|
@ -51,7 +51,7 @@ define(function (require, exports, module) {
|
|||
return account.verifyTokenCode(code)
|
||||
.then(() => {
|
||||
this.logViewEvent('success');
|
||||
return this.invokeBrokerMethod('afterCompleteSignInTokenCode', account);
|
||||
return this.invokeBrokerMethod('afterCompleteSignInWithCode', account);
|
||||
}, (err) => {
|
||||
this.displayError(err);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const AuthErrors = require('lib/auth-errors');
|
||||
const Cocktail = require('cocktail');
|
||||
const Constants = require('../lib/constants');
|
||||
const FormView = require('./form');
|
||||
const SignInMixin = require('./mixins/signin-mixin');
|
||||
const ServiceMixin = require('./mixins/service-mixin');
|
||||
const Template = require('templates/sign_in_totp_code.mustache');
|
||||
const VerificationReasonMixin = require('./mixins/verification-reason-mixin');
|
||||
|
||||
const CODE_INPUT_SELECTOR = 'input.totp-code';
|
||||
|
||||
const View = FormView.extend({
|
||||
className: 'sign-in-totp-code',
|
||||
template: Template,
|
||||
|
||||
_sanitizeCode (code) {
|
||||
// Remove spaces and `-`
|
||||
return code.replace(/[- ]*/g, '');
|
||||
},
|
||||
|
||||
beforeRender () {
|
||||
// user cannot confirm if they have not initiated a sign in.
|
||||
const account = this.getSignedInAccount();
|
||||
if (! account || ! account.get('sessionToken')) {
|
||||
this.navigate(this._getAuthPage());
|
||||
}
|
||||
},
|
||||
|
||||
setInitialContext (context) {
|
||||
// This needs to point to correct support link
|
||||
const supportLink = Constants.BLOCKED_SIGNIN_SUPPORT_URL;
|
||||
|
||||
context.set({
|
||||
escapedSupportLink: encodeURI(supportLink),
|
||||
hasSupportLink: !! supportLink
|
||||
});
|
||||
},
|
||||
|
||||
submit () {
|
||||
const account = this.getSignedInAccount();
|
||||
const code = this._sanitizeCode(this.getElementValue('input.totp-code'));
|
||||
return account.verifyTotpCode(code)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
this.logViewEvent('success');
|
||||
return this.invokeBrokerMethod('afterCompleteSignInWithCode', account);
|
||||
} else {
|
||||
throw AuthErrors.toError('INVALID_TOTP_CODE');
|
||||
}
|
||||
})
|
||||
.catch((err) => this.showValidationError(this.$(CODE_INPUT_SELECTOR), err));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the URL of the page for users that
|
||||
* must enter their password.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
_getAuthPage () {
|
||||
const authPage =
|
||||
this.model.get('lastPage') === 'force_auth' ? 'force_auth' : 'signin';
|
||||
|
||||
return this.broker.transformLink(authPage);
|
||||
}
|
||||
});
|
||||
|
||||
Cocktail.mixin(
|
||||
View,
|
||||
SignInMixin,
|
||||
ServiceMixin,
|
||||
VerificationReasonMixin
|
||||
);
|
||||
|
||||
module.exports = View;
|
|
@ -10,6 +10,7 @@
|
|||
@import 'modules/custom-rows';
|
||||
@import 'modules/modal';
|
||||
@import 'modules/settings';
|
||||
@import 'modules/settings-totp';
|
||||
@import 'modules/legal';
|
||||
@import 'modules/spinner';
|
||||
@import 'modules/avatar';
|
||||
|
|
|
@ -78,7 +78,8 @@ ul.links {
|
|||
}
|
||||
|
||||
.verification-email-message,
|
||||
.signed-in-email-message {
|
||||
.signed-in-email-message,
|
||||
.verification-totp-message {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
|
|
@ -66,3 +66,12 @@
|
|||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.graphic-two-factor-auth {
|
||||
background-image: url('/images/graphic_two_factor_auth.svg');
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
height: 138px;
|
||||
margin-top: 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
|
|
@ -40,5 +40,20 @@
|
|||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-success {
|
||||
background-color: $settings-success-background-color;
|
||||
border-color: $settings-success-border-color;
|
||||
border-style: solid;
|
||||
color: $success-text-color;
|
||||
font-size: $base-font;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
|
||||
a {
|
||||
color: $success-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
.totp-panel {
|
||||
.totp-list {
|
||||
padding: 0;
|
||||
|
||||
.totp-type {
|
||||
height: 40px;
|
||||
list-style: none;
|
||||
margin: 10px 0;
|
||||
position: relative;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
background-position: left 2px;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
background-position: right 2px;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@include respond-to('big') {
|
||||
width: calc(95% - 95px);
|
||||
}
|
||||
|
||||
@include respond-to('small') {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
color: $color-grey;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(95% - 95px);
|
||||
|
||||
& .disabled {
|
||||
color: $color-red;
|
||||
}
|
||||
|
||||
& .enabled {
|
||||
color: $color-green;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button-group {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
height: 35px;
|
||||
margin-left: 10px;
|
||||
/*minimum width required for the button to look good without occupying too much space*/
|
||||
/*is also the default computed width on desktop screen*/
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.totp-details {
|
||||
.qr-image-container {
|
||||
margin: auto;
|
||||
padding-bottom: 10px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.setup-description,
|
||||
.code-description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.show-code-link {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.code {
|
||||
color: $color-green-darker;
|
||||
letter-spacing: .1rem;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-row {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.recovery-code {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1498,5 +1498,65 @@ define(function (require, exports, module) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTotpToken', () => {
|
||||
it('delegates to the fxa-js-client', () => {
|
||||
const resp = {
|
||||
qrCodeUrl: 'data:image/png;base64,iVBOR:',
|
||||
secret: 'superdupersecretcode'
|
||||
};
|
||||
sinon.stub(realClient, 'createTotpToken').callsFake(() => Promise.resolve(resp));
|
||||
|
||||
return client.createTotpToken()
|
||||
.then((_resp) => {
|
||||
assert.strictEqual(_resp, resp);
|
||||
assert.isTrue(realClient.createTotpToken.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTotpToken', () => {
|
||||
it('delegates to the fxa-js-client', () => {
|
||||
const resp = {};
|
||||
sinon.stub(realClient, 'deleteTotpToken').callsFake(() => Promise.resolve(resp));
|
||||
|
||||
return client.deleteTotpToken()
|
||||
.then((_resp) => {
|
||||
assert.strictEqual(_resp, resp);
|
||||
assert.isTrue(realClient.deleteTotpToken.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkTotpTokenExists', () => {
|
||||
it('delegates to the fxa-js-client', () => {
|
||||
const resp = {
|
||||
exists: true
|
||||
};
|
||||
sinon.stub(realClient, 'checkTotpTokenExists').callsFake(() => Promise.resolve(resp));
|
||||
|
||||
return client.checkTotpTokenExists()
|
||||
.then((_resp) => {
|
||||
assert.strictEqual(_resp, resp);
|
||||
assert.isTrue(realClient.checkTotpTokenExists.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyTotpCode', () => {
|
||||
it('delegates to the fxa-js-client', () => {
|
||||
const resp = {
|
||||
success: true
|
||||
};
|
||||
sinon.stub(realClient, 'verifyTotpCode').callsFake(() => Promise.resolve(resp));
|
||||
|
||||
return client.verifyTotpCode('code')
|
||||
.then((_resp) => {
|
||||
assert.strictEqual(_resp, resp);
|
||||
assert.isTrue(realClient.verifyTotpCode.calledOnce);
|
||||
assert.isTrue(realClient.verifyTotpCode.calledWith('code'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const $ = require('jquery');
|
||||
const assert = require('chai').assert;
|
||||
const Metrics = require('lib/metrics');
|
||||
const Notifier = require('lib/channels/notifier');
|
||||
const sinon = require('sinon');
|
||||
const TestHelpers = require('../../../lib/helpers');
|
||||
const User = require('models/user');
|
||||
const View = require('views/settings/two_step_authentication');
|
||||
|
||||
describe('views/settings/two_step_authentication', () => {
|
||||
let account;
|
||||
let email;
|
||||
let metrics;
|
||||
let notifier;
|
||||
let featureEnabled;
|
||||
let hasToken;
|
||||
let validCode;
|
||||
const UID = '123';
|
||||
let user;
|
||||
let view;
|
||||
|
||||
function initView() {
|
||||
view = new View({
|
||||
metrics: metrics,
|
||||
notifier: notifier,
|
||||
user: user
|
||||
});
|
||||
|
||||
sinon.stub(view, '_isPanelEnabled').callsFake(() => featureEnabled);
|
||||
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
|
||||
sinon.spy(view, 'remove');
|
||||
|
||||
return view.render()
|
||||
.then(() => $('#container').html(view.$el));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
email = TestHelpers.createEmail();
|
||||
notifier = new Notifier();
|
||||
metrics = new Metrics({notifier});
|
||||
user = new User();
|
||||
account = user.initAccount({
|
||||
email: email,
|
||||
sessionToken: 'abc123',
|
||||
uid: UID,
|
||||
verified: true
|
||||
});
|
||||
|
||||
sinon.stub(account, 'checkTotpTokenExists').callsFake(() => {
|
||||
return Promise.resolve({exists: hasToken});
|
||||
});
|
||||
|
||||
sinon.stub(account, 'verifyTotpCode').callsFake(() => {
|
||||
return Promise.resolve({success: validCode});
|
||||
});
|
||||
|
||||
sinon.stub(account, 'deleteTotpToken').callsFake(() => Promise.resolve({}));
|
||||
|
||||
sinon.stub(account, 'createTotpToken').callsFake(() => {
|
||||
return Promise.resolve({
|
||||
qrCodeUrl: 'data:image/png;base64,iVBOR',
|
||||
secret: 'MZEE 4ODK'
|
||||
});
|
||||
});
|
||||
|
||||
featureEnabled = true;
|
||||
hasToken = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
view.remove();
|
||||
view.destroy();
|
||||
view = null;
|
||||
});
|
||||
|
||||
describe('feature disabled', () => {
|
||||
beforeEach(() => {
|
||||
featureEnabled = false;
|
||||
return initView();
|
||||
});
|
||||
|
||||
it('should remove panel if `showTwoStepAuthentication` query is not specified', () => {
|
||||
assert.equal(view.remove.callCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('feature enabled', () => {
|
||||
beforeEach(() => {
|
||||
featureEnabled = true;
|
||||
return initView();
|
||||
});
|
||||
|
||||
describe('should show token status', () => {
|
||||
beforeEach(() => {
|
||||
hasToken = true;
|
||||
return initView();
|
||||
});
|
||||
|
||||
it('should show token status view if user has token', () => {
|
||||
assert.equal(view.$('#totp-status .enabled').length, 1);
|
||||
assert.equal(view.$('#totp.hidden').length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should create new token', () => {
|
||||
beforeEach(() => {
|
||||
hasToken = false;
|
||||
return initView()
|
||||
.then(() => view.createToken());
|
||||
});
|
||||
|
||||
it('should show QR code', () => {
|
||||
assert.equal(view.$('#totp').is(':visible'), true);
|
||||
assert.equal(view.$('img.qr-image').attr('src'), 'data:image/png;base64,iVBOR');
|
||||
});
|
||||
|
||||
it('should not show status section', () => {
|
||||
assert.equal(view.$('.totp-list.hidden').length, 1);
|
||||
});
|
||||
|
||||
describe('should show code and hide `show code link`', () => {
|
||||
beforeEach(() => {
|
||||
assert.equal(view.$('.manual-code.hidden').length, 1);
|
||||
assert.equal(view.$('.show-code-link:not(hidden)').length, 1);
|
||||
return view.$('.show-code-link').click();
|
||||
});
|
||||
|
||||
it('shows correct links', () => {
|
||||
assert.equal(view.$('.code')[0].innerText, 'MZEE 4ODK');
|
||||
assert.equal(view.$('.manual-code:not(hidden)').length, 1);
|
||||
assert.equal(view.$('.show-code-link.hidden').length, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should display error for invalid code', () => {
|
||||
beforeEach(() => {
|
||||
validCode = false;
|
||||
return initView()
|
||||
.then(() => {
|
||||
sinon.spy(view, 'showValidationError');
|
||||
return view.confirmCode();
|
||||
});
|
||||
});
|
||||
|
||||
it('display error', () => {
|
||||
assert.equal(view.showValidationError.callCount, 1);
|
||||
assert.equal(view.showValidationError.args[0][1].errno, 154, 'invalid code errno');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should validate token code', () => {
|
||||
beforeEach(() => {
|
||||
validCode = true;
|
||||
return initView()
|
||||
.then(() => {
|
||||
sinon.spy(view, 'render');
|
||||
sinon.spy(view, 'displaySuccess');
|
||||
view.confirmCode();
|
||||
});
|
||||
});
|
||||
|
||||
it('confirms code', () => {
|
||||
assert.equal(view.render.callCount, 1);
|
||||
assert.equal(view.displaySuccess.callCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should delete token', () => {
|
||||
beforeEach(() => {
|
||||
hasToken = true;
|
||||
return initView();
|
||||
});
|
||||
|
||||
it('deletes a token and shows success', () => {
|
||||
sinon.spy(view, 'navigate');
|
||||
sinon.spy(view, 'displaySuccess');
|
||||
return view.deleteToken()
|
||||
.then(() => {
|
||||
assert.equal(account.deleteTotpToken.callCount, 1, 'called delete token');
|
||||
assert.equal(view.displaySuccess.callCount, 1, 'displayed success');
|
||||
assert.equal(view.navigate.args[0][0], '/settings', 'navigated to settings');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -146,7 +146,7 @@ define(function (require, exports, module) {
|
|||
|
||||
it('calls correct broker methods', () => {
|
||||
assert.isTrue(account.verifyTokenCode.calledWith(TOKEN_CODE), 'verify with correct code');
|
||||
assert.isTrue(view.invokeBrokerMethod.calledWith('afterCompleteSignInTokenCode', account));
|
||||
assert.isTrue(view.invokeBrokerMethod.calledWith('afterCompleteSignInWithCode', account));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const $ = require('jquery');
|
||||
const _ = require('underscore');
|
||||
const {assert} = require('chai');
|
||||
const Account = require('models/account');
|
||||
const AuthErrors = require('lib/auth-errors');
|
||||
const Backbone = require('backbone');
|
||||
const BaseBroker = require('models/auth_brokers/base');
|
||||
const Constants = require('lib/constants');
|
||||
const {createRandomHexString} = require('../../lib/helpers');
|
||||
const Metrics = require('lib/metrics');
|
||||
const Relier = require('models/reliers/relier');
|
||||
const sinon = require('sinon');
|
||||
const View = require('views/sign_in_totp_code');
|
||||
const WindowMock = require('../../mocks/window');
|
||||
|
||||
const TOTP_CODE = createRandomHexString(Constants.UNBLOCK_CODE_LENGTH);
|
||||
|
||||
describe('views/sign_in_totp_code', () => {
|
||||
let account;
|
||||
let broker;
|
||||
let metrics;
|
||||
let model;
|
||||
let notifier;
|
||||
let relier;
|
||||
let view;
|
||||
let windowMock;
|
||||
|
||||
beforeEach(() => {
|
||||
windowMock = new WindowMock();
|
||||
|
||||
relier = new Relier({
|
||||
window: windowMock
|
||||
});
|
||||
|
||||
broker = new BaseBroker({
|
||||
relier: relier,
|
||||
window: windowMock
|
||||
});
|
||||
|
||||
account = new Account({
|
||||
email: 'a@a.com',
|
||||
sessionToken: 'someToken',
|
||||
uid: 'uid'
|
||||
});
|
||||
|
||||
model = new Backbone.Model({
|
||||
account: account,
|
||||
lastPage: 'signin',
|
||||
password: 'password'
|
||||
});
|
||||
|
||||
notifier = _.extend({}, Backbone.Events);
|
||||
metrics = new Metrics({notifier});
|
||||
|
||||
view = new View({
|
||||
broker,
|
||||
canGoBack: true,
|
||||
metrics,
|
||||
model,
|
||||
notifier,
|
||||
relier,
|
||||
viewName: 'sign-in-totp-code',
|
||||
window: windowMock
|
||||
});
|
||||
|
||||
sinon.stub(view, 'getSignedInAccount').callsFake(() => model.get('account'));
|
||||
|
||||
return view.render()
|
||||
.then(() => $('#container').html(view.$el));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
metrics.destroy();
|
||||
view.remove();
|
||||
view.destroy();
|
||||
view = metrics = null;
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
it('renders the view', () => {
|
||||
assert.lengthOf(view.$('#fxa-totp-code-header'), 1);
|
||||
assert.include(view.$('.verification-totp-message').text(), 'security code');
|
||||
});
|
||||
|
||||
describe('without an account', () => {
|
||||
beforeEach(() => {
|
||||
account = model.get('account').unset('sessionToken');
|
||||
sinon.spy(view, 'navigate');
|
||||
return view.render();
|
||||
});
|
||||
|
||||
it('redirects to the signin page', () => {
|
||||
assert.isTrue(view.navigate.calledWith('signin'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAndSubmit', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(view, 'submit').callsFake(() => Promise.resolve());
|
||||
sinon.spy(view, 'showValidationError');
|
||||
});
|
||||
|
||||
describe('with an empty code', () => {
|
||||
beforeEach(() => {
|
||||
view.$('#totp-code').val('');
|
||||
return view.validateAndSubmit().then(assert.fail, () => {});
|
||||
});
|
||||
|
||||
it('displays a tooltip, does not call submit', () => {
|
||||
assert.isTrue(view.showValidationError.called);
|
||||
assert.isFalse(view.submit.called);
|
||||
});
|
||||
});
|
||||
|
||||
const validCodes = [
|
||||
TOTP_CODE,
|
||||
' ' + TOTP_CODE,
|
||||
TOTP_CODE + ' ',
|
||||
' ' + TOTP_CODE + ' ',
|
||||
'001-001',
|
||||
'111 111'
|
||||
];
|
||||
validCodes.forEach((code) => {
|
||||
describe(`with a valid code: '${code}'`, () => {
|
||||
beforeEach(() => {
|
||||
view.$('.totp-code').val(code);
|
||||
|
||||
return view.validateAndSubmit();
|
||||
});
|
||||
|
||||
it('calls submit', () => {
|
||||
assert.equal(view.submit.callCount, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', () => {
|
||||
describe('success', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(account, 'verifyTotpCode').callsFake(() => Promise.resolve({success: true}));
|
||||
sinon.stub(view, 'invokeBrokerMethod').callsFake(() => Promise.resolve());
|
||||
view.$('.totp-code').val(TOTP_CODE);
|
||||
return view.submit();
|
||||
});
|
||||
|
||||
it('calls correct broker methods', () => {
|
||||
assert.isTrue(account.verifyTotpCode.calledWith(TOTP_CODE), 'verify with correct code');
|
||||
assert.isTrue(view.invokeBrokerMethod.calledWith('afterCompleteSignInWithCode', account));
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid TOTP code', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(account, 'verifyTotpCode').callsFake(() => Promise.resolve({success: false}));
|
||||
sinon.spy(view, 'showValidationError');
|
||||
view.$('.totp-code').val(TOTP_CODE);
|
||||
return view.submit();
|
||||
});
|
||||
|
||||
it('rejects with the error for display', () => {
|
||||
assert.equal(view.showValidationError.args[0][1].errno, 154, 'correct error thrown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(account, 'verifyTotpCode').callsFake(() => Promise.reject(AuthErrors.toError('UNEXPECTED_ERROR')));
|
||||
sinon.spy(view, 'showValidationError');
|
||||
view.$('.totp-code').val(TOTP_CODE);
|
||||
return view.submit();
|
||||
});
|
||||
|
||||
it('rejects with the error for display', () => {
|
||||
assert.equal(view.showValidationError.args[0][1].errno, 999, 'correct error thrown');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -204,12 +204,14 @@ require('./spec/views/settings/communication_preferences');
|
|||
require('./spec/views/settings/delete_account');
|
||||
require('./spec/views/settings/display_name');
|
||||
require('./spec/views/settings/emails');
|
||||
require('./spec/views/settings/two_step_authentication');
|
||||
require('./spec/views/sign_in');
|
||||
require('./spec/views/sign_in_bounced');
|
||||
require('./spec/views/sign_in_password');
|
||||
require('./spec/views/sign_in_reported');
|
||||
require('./spec/views/sign_in_unblock');
|
||||
require('./spec/views/sign_in_token_code');
|
||||
require('./spec/views/sign_in_totp_code');
|
||||
require('./spec/views/sign_up');
|
||||
require('./spec/views/sign_up_password');
|
||||
require('./spec/views/sms_send');
|
||||
|
|
|
@ -39,9 +39,11 @@ module.exports = function () {
|
|||
'settings/delete_account',
|
||||
'settings/display_name',
|
||||
'settings/emails',
|
||||
'settings/two_step_authentication',
|
||||
'signin',
|
||||
'signin_bounced',
|
||||
'signin_token_code',
|
||||
'signin_totp_code',
|
||||
'signin_confirmed',
|
||||
'signin_permissions',
|
||||
'signin_reported',
|
||||
|
|
Загрузка…
Ссылка в новой задаче