Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let users confirm their TOTP setup before enforcing the provider #156

Merged
merged 2 commits into from
Apr 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions appinfo/database.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
<type>clob</type>
<notnull>true</notnull>
</field>
<!--
STATE 0 : disabled
STATE 1 : created (but not active)
STATE 2 : enabled
-->
<field>
<name>state</name>
<type>integer</type>
<default>2</default>
<notnull>true</notnull>
</field>

<index>
<name>totp_secrets_user_id</name>
Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<name>Two Factor TOTP Provider</name>
<summary>TOTP two-factor provider</summary>
<description>A Two-Factor-Auth Provider for TOTP (RFC 6238)</description>
<version>1.1.0</version>
<version>1.2.0</version>
<licence>agpl</licence>
<author>Christoph Wurst</author>
<namespace>TwoFactorTOTP</namespace>
Expand Down
7 changes: 7 additions & 0 deletions css/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@
.nav-icon-totp-second-factor-auth {
background-image: url('../img/app-dark.svg?v=1');
}

.totp-loading {
display: inline-block;
vertical-align: sub;
margin-left: -2px;
margin-right: 4px;
}
235 changes: 183 additions & 52 deletions js/settingsview.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,102 @@
/* global Backbone, Handlebars, Promise */
/* global Backbone, Handlebars, Promise, _ */

(function (OC, Backbone, Handlebars, $) {
(function (OC, Backbone, Handlebars, $, _) {
'use strict';

OC.Settings = OC.Settings || {};
OC.Settings.TwoFactorTotp = OC.Settings.TwoFactorTotp || {};

var TEMPLATE = '<div>'
+ ' <input type="checkbox" class="checkbox" id="totp-enabled">'
var STATE_DISABLED = 0;
var STATE_CREATED = 1;
var STATE_ENABLED = 2;

var TEMPLATE = ''
+ '{{#if loading}}'
+ '<span class="icon-loading-small totp-loading"></span>'
+ '<span>' + t('twofactor_totp', 'Enable TOTP') + '</span>'
+ '{{else}}'
+ '<div>'
+ ' <input type="checkbox" class="checkbox" id="totp-enabled" {{#if enabled}}checked{{/if}}>'
+ ' <label for="totp-enabled">' + t('twofactor_totp', 'Enable TOTP') + '</label>'
+ '</div>'
+ '{{/if}}'
+ '{{#if secret}}'
+ '<div>'
+ ' <span>' + t('twofactor_totp', 'This is your new TOTP secret:') + ' {{secret}}</span>'
+ '</div>'
+ '<div>'
+ ' <span>' + t('twofactor_totp', 'Scan this QR code with your TOTP app') + '<span><br>'
+ ' <img src="{{qr}}">'
+ ' </div>'
+ '</div>'
+ '<span>' + t('twofactor_totp', 'Once you have configured your app, enter a test code below to ensure that your app has been configured correctly.') + '<span><br>'
+ '<input id="totp-confirmation" type="tel" minlength="6" maxlength="6" autocomplete="off" autocapitalize="off" placeholder="' + t('twofactor_totp', 'Authentication code') + '">'
+ '<input id="totp-confirmation-submit" type="button" value="' + t('twofactor_totp', 'Verify') + '">'
+ '{{/if}}';

var View = Backbone.View.extend({

/** @type {function} */
template: Handlebars.compile(TEMPLATE),

/** @type {bool} */
_loading: undefined,
_enabled: undefined,

/** @type {string} */
_qr: undefined,

/** @type {string} */
_secret: undefined,

/** @type {int} */
_state: undefined,

events: {
'change #totp-enabled': '_onToggleEnabled'
'change #totp-enabled': '_onToggleEnabled',
'click #totp-confirmation-submit': '_enableTOTP',
'keydown #totp-confirmation': '_onConfirmKeyDown'
},

/**
* @returns {undefined}
*/
initialize: function () {
this._load();
},
render: function (data) {
this.$el.html(this.template(data));

/**
* @param {Object} data
* @returns {undefined}
*/
render: function () {
this.$el.html(this.template({
loading: this._loading,
secret: this._secret,
qr: this._qr,
enabled: this._state === STATE_ENABLED
}));
},

/**
* @returns {undefined}
*/
_load: function () {
this._loading = true;
this.render();

var url = OC.generateUrl('/apps/twofactor_totp/settings/state');
var loading = $.ajax(url, {
Promise.resolve($.ajax(url, {
method: 'GET'
});

var _this = this;
$.when(loading).done(function (data) {
_this._enabled = data.enabled;
_this.$('#totp-enabled').attr('checked', data.enabled);
});
$.when(loading).always(function () {
_this._loading = false;
});
})).then(function (data) {
this._state = data.state ? STATE_ENABLED : STATE_DISABLED;
}.bind(this), console.error.bind(this)).then(function () {
this._loading = false;
this.render();
}.bind(this));
},

/**
* @returns {Promise}
*/
_onToggleEnabled: function () {
if (this._loading) {
// Ignore event
Expand All @@ -58,53 +105,137 @@

var enabled = this.$('#totp-enabled').is(':checked');

if (enabled !== this._enabled) {
var url = OC.generateUrl('/apps/twofactor_totp/settings/enable');
this._loading = true;
this._enabled = enabled;

var _this = this;
this._requirePasswordConfirmation().then(function () {
return Promise.resolve($.ajax(url, {
method: 'POST',
data: {
state: enabled
}
}));
}).then(function(data) {
_this._enabled = data.enabled;
_this._showQr(data);
_this.$('#totp-enabled').attr('checked', data.enabled);
}).catch(console.error.bind(this)).then(function() {
_this._loading = false;
});
if (!!enabled) {
return this._createTOTP();
} else {
return this._disableTOTP();
}
},

/**
* Create a new secret on the server, which will be inactive until the
* user confirms their app is working by providing a OTP once.
*
* @returns {Promise}
*/
_requirePasswordConfirmation: function () {
if (!OC.PasswordConfirmation.requiresPasswordConfirmation()) {
return Promise.resolve();
_createTOTP: function () {
this._loading = true;
// Show loading spinner
this.render();

return this._updateServerState({
state: STATE_CREATED
}).then(function () {
// If the stat could be changed, keep showing the loading
// spinner until the user has finished the registration
this._loading = this._state === STATE_CREATED;
this.render();
}.bind(this), function() {
// Restore on error
this._loading = false;
this.render();
}).catch(console.error.bind(this));
},

/**
* Also enable TOTP if the user presses enter inside the confirmation
* input
*
* @param {Event} e
* @returns {undefined}
*/
_onConfirmKeyDown: function(e) {
if (e.which === 13) {
this._enableTOTP();
}
return new Promise(function (resolve) {
OC.PasswordConfirmation.requirePasswordConfirmation(resolve);
},

/**
* Enable the previously created TOTP secret by sending a OTP
* to the server for confirmation.
*
* @returns {Promise}
*/
_enableTOTP: function () {
var key = this.$('#totp-confirmation').val();

this._loading = true;
// Show loading spinner and disable input elements
this.render();
this.$('input').prop('disabled', true);

return this._updateServerState({
state: STATE_ENABLED,
key: key
}).then(function () {
this.$('input').prop('disabled', false);
if (this._state === STATE_ENABLED) {
// Success
this._loading = false;
this._qr = undefined;
this._secret = undefined;
} else {
OC.Notification.showTemporary(t('twofactor_totp', 'Could not verify your key. Please try again'));
}
this.render();
}.bind(this), console.error.bind(this));
},

/**
* @returns {Promise}
*/
_disableTOTP: function () {
this._loading = true;
// Show loading spinner
this.render();

return this._updateServerState({
state: STATE_DISABLED
}).then(function () {
this._loading = false;
this.render();
}.bind(this), console.error.bind(this));
},

/**
* @param {Object} data
* @param {int} data.state
* @returns {Promise}
*/
_updateServerState: function (data) {
var url = OC.generateUrl('/apps/twofactor_totp/settings/enable');
return this._requirePasswordConfirmation().then(function () {
return $.ajax(url, {
method: 'POST',
data: data
});
}).then(function (data) {
this._state = data.state;
// Optional response: qr, secret
if (!_.isUndefined(data.qr) && !_.isUndefined(data.secret)) {
this._qr = data.qr;
this._secret = data.secret;
}
}.bind(this), function () {
console.error(arguments);
throw new Error('twofactor_totp', 'Error while communicating with the server');
});
},

/**
* @param {object} data
* @returns {undefined}
* @returns {Promise}
*/
_showQr: function (data) {
this.render({
secret: data.secret,
qr: data.qr
_requirePasswordConfirmation: function () {
if (!OC.PasswordConfirmation.requiresPasswordConfirmation()) {
return Promise.resolve();
}
return new Promise(function (resolve) {
OC.PasswordConfirmation.requirePasswordConfirmation(resolve);
});
}

});

OC.Settings.TwoFactorTotp.View = View;

})(OC, Backbone, Handlebars, $);
})(OC, Backbone, Handlebars, $, _);
Loading