Skip to content
This repository has been archived by the owner on Dec 27, 2023. It is now read-only.

Commit

Permalink
feature(Tinebase): H|TOTP MFA UI
Browse files Browse the repository at this point in the history
  • Loading branch information
corneliusweiss committed Aug 4, 2021
1 parent f7efb5f commit 3a796b7
Show file tree
Hide file tree
Showing 11 changed files with 427 additions and 294 deletions.
6 changes: 3 additions & 3 deletions tine20/Tinebase/Model/MFA/HOTPUserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class Tinebase_Model_MFA_HOTPUserConfig extends Tinebase_Auth_MFA_AbstractUserCo
protected static $_modelConfiguration = [
self::APP_NAME => Tinebase_Config::APP_NAME,
self::MODEL_NAME => self::MODEL_NAME_PART,
self::RECORD_NAME => 'HOTP',
self::TITLE_PROPERTY => 'HOTP',
self::RECORD_NAME => 'Counter based OTP (HOTP)', // _('Counter based OTP')
self::TITLE_PROPERTY => 'Counter based OTP (HOPT) is configured', // _('Counter based OTP (HOPT) is configured')

self::FIELDS => [
self::FLD_ACCOUNT_ID => [
Expand All @@ -51,7 +51,7 @@ class Tinebase_Model_MFA_HOTPUserConfig extends Tinebase_Auth_MFA_AbstractUserCo
],
self::FLD_SECRET => [
self::TYPE => self::TYPE_STRING,
self::LABEL => 'H/T OTP secret', // _('H/T OTP secret')
self::LABEL => 'Secret Key', // _('Secret Key')
],
self::FLD_CC_ID => [
self::TYPE => self::TYPE_STRING,
Expand Down
6 changes: 3 additions & 3 deletions tine20/Tinebase/Model/MFA/TOTPUserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class Tinebase_Model_MFA_TOTPUserConfig extends Tinebase_Auth_MFA_AbstractUserCo
protected static $_modelConfiguration = [
self::APP_NAME => Tinebase_Config::APP_NAME,
self::MODEL_NAME => self::MODEL_NAME_PART,
self::RECORD_NAME => 'TOTP',
self::TITLE_PROPERTY => 'TOTP',
self::RECORD_NAME => 'Time based OTP (TOTP)', // _('Time based OTP')
self::TITLE_PROPERTY => 'Time based OTP (TOPT) is configured', // _('Time based OTP (TOPT) is configured')

self::FIELDS => [
self::FLD_ACCOUNT_ID => [
Expand All @@ -50,7 +50,7 @@ class Tinebase_Model_MFA_TOTPUserConfig extends Tinebase_Auth_MFA_AbstractUserCo
],
self::FLD_SECRET => [
self::TYPE => self::TYPE_STRING,
self::LABEL => 'H/T OTP secret', // _('H/T OTP secret')
self::LABEL => 'Secret Key', // _('Secret Key')
],
self::FLD_CC_ID => [
self::TYPE => self::TYPE_STRING,
Expand Down
5 changes: 3 additions & 2 deletions tine20/Tinebase/js/AreaLocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import PinProvider from 'MFA/Providers/Pin'
import TokenProvider from 'MFA/Providers/Token'
import SmsProvider from 'MFA/Providers/Sms'
import YubicoOTPProvider from 'MFA/Providers/YubicoOTP'
import HTOTPProvider from 'MFA/Providers/HTOTP'
import HTOTPAuthenticatorProvider from 'MFA/Providers/HTOTPAuthenticator'

let providerMap = {
Tinebase_Model_MFA_HTOTPUserConfig: HTOTPProvider,
Tinebase_Model_MFA_TOTPUserConfig: HTOTPAuthenticatorProvider,
Tinebase_Model_MFA_HOTPUserConfig: HTOTPAuthenticatorProvider,
Tinebase_Model_MFA_SmsUserConfig: SmsProvider,
Tinebase_Model_MFA_UserPassword: UserPasswordProvider,
Tinebase_Model_MFA_PinUserConfig: PinProvider,
Expand Down
1 change: 1 addition & 0 deletions tine20/Tinebase/js/BL/BLConfigPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Tine.Tinebase.BL.BLConfigPanel = Ext.extend(Tine.widgets.grid.QuickaddGridPanel,
// @TODO: move to fieldManager?
this.BLElementConfigClassNames = _.get(this.recordClass.getField(this.classNameField), 'fieldDefinition.config.availableModels', [])
this.BLElementPicker = new Ext.form.ComboBox({
listWidth: 200,
store: _.reduce(this.BLElementConfigClassNames, function(arr, classname) {
var recordClass = Tine.Tinebase.data.RecordMgr.get(classname);
if (recordClass) {
Expand Down
106 changes: 106 additions & 0 deletions tine20/Tinebase/js/MFA/HTOTPSecretField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
Ext.ns('Tine.Tinebase');

const HTOTOPSecretField = Ext.extend(Ext.form.FieldSet, {
/**
* @cfg {String} type h|totp
*/
type: 'totp',

initComponent: function() {
this.title = i18n._('Secret Key');
this.explainText = new Ext.form.Label({
text: i18n._("Please note: After saving you can not view this autogenerated secret key again!")
});
this.secretField = new Ext.form.TextField({
//@TODO clipboard plugin!
name: 'secret',
anchor: '100%',
hideLabel: true,
setValue: this.setValue.createDelegate(this)
// getValue: this.getValue.createDelegate(this)
});
this.qrField = new Ext.BoxComponent({
width: 150,
height: 150,
html: '<img style="width: 100%; height: 100%"/>'
});
this.items = [
this.explainText,
this.secretField,
this.qrField
];

this.supr().initComponent.call(this);
},

onRender: function() {
this.supr().onRender.apply(this, arguments);
this.editDialog = this.findParentBy(function (c) {
return c instanceof Tine.widgets.dialog.EditDialog
});
},
setValue: function(value, record) {
const supr = Ext.form.TextField.prototype.setValue.createDelegate(this.secretField);

if (! record.get('secret')) {
supr(i18n._('Generating secret key ...'));
this.secretField.setDisabled(true);
import(/* webpackChunkName: "Tinebase/js/base32-encode" */ 'base32-encode').then((module) => {
const bytes = new Uint8Array(35);
window.crypto.getRandomValues(bytes);
supr(module.default(bytes, 'RFC3548'));
this.secretField.setDisabled(false);
this.onValueChange();
});
} else {
supr(value);
this.onValueChange();
}

},

onValueChange: async function() {
const QRCode = await import(/* webpackChunkName: "Tinebase/js/qrcode" */ 'qrcode');
const otpURI = this.getOTPAuthURI();
const imgURL = await QRCode.toDataURL(otpURI);

this.qrField.el.child('img').dom.src = imgURL;
},

getOTPAuthURI: function() {
const secret = this.secretField.getValue();
const type = this.type.toLowerCase();
const account = encodeURIComponent(JSON.parse(this.editDialog.record.json.account_id).accountLoginName);
const issuer = encodeURIComponent(window.location.hostname);

let uri = `otpauth://${type}/${issuer}:${account}?secret=${secret}&issuer=${issuer}`;

// uri += "&algorithm=" + this.editDialog.record.get('algorithm');
// uri += "&digits=" + this.editDialog.record.get('digits');
// uri += "&period=" + this.editDialog.record.get('period');

if (type == "hotp")
uri += "&counter=" + (this.editDialog.record.get('counter') || 0);

// uri += "&lock=" + ???; // freeOTP only?
uri += "&image=" + encodeURIComponent(Tine.Tinebase.common.getUrl() + Tine.Tinebase.registry.get('installLogo')); // freeOTP only?;

return uri;
}


});

Ext.reg('mfa-htotp-secretfield', HTOTOPSecretField)

Tine.widgets.form.FieldManager.register('Tinebase', 'MFA_TOTPUserConfig', 'secret', {
type: 'totp',
xtype: 'mfa-htotp-secretfield',
height: 300,
}, Tine.widgets.form.FieldManager.CATEGORY_EDITDIALOG);

Tine.widgets.form.FieldManager.register('Tinebase', 'MFA_HOTPUserConfig', 'secret', {
type: 'hotp',
xtype: 'mfa-htotp-secretfield',
height: 300,
}, Tine.widgets.form.FieldManager.CATEGORY_EDITDIALOG);
21 changes: 0 additions & 21 deletions tine20/Tinebase/js/MFA/Providers/HTOTP.js

This file was deleted.

21 changes: 21 additions & 0 deletions tine20/Tinebase/js/MFA/Providers/HTOTPAuthenticator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Tine 2.0
*
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @author Cornelius Weiss <c.weiss@metaways.de>
* @copyright Copyright (c) 2018-2021 Metaways Infosystems GmbH (http://www.metaways.de)
*/

import Generic from './Generic'

class Sms extends Generic {
constructor (config) {
super(config)
this.isOTP = true
this.windowTitle = i18n._('Authenticator code required')
this.questionText = formatMessage('This area is locked. To unlock it you need to provide the code from your authenticator app {mfaDevice.device_name}.', this)
this.passwordFieldLabel = formatMessage('Authenticator code from {mfaDevice.device_name}', this)
}
}

export default Sms
11 changes: 9 additions & 2 deletions tine20/Tinebase/js/MFA/UserConfigPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* @copyright Copyright (c) 2021 Metaways Infosystems GmbH (http://www.metaways.de)
*/

import './HTOTPSecretField';

class UserConfigPanel extends Tine.Tinebase.BL.BLConfigPanel {
/**
*
Expand All @@ -26,6 +28,7 @@ class UserConfigPanel extends Tine.Tinebase.BL.BLConfigPanel {

// load dynamic list of possible mfa devices for user
return new Promise((async (resolve) => {
const me = this;
const mfaDevices = await Tine.Admin.getPossibleMFAs(this.account);
const arr = _.map(mfaDevices, (record) => {
// we use mfa_config_id as id here as config_class is not unique!
Expand All @@ -38,7 +41,11 @@ class UserConfigPanel extends Tine.Tinebase.BL.BLConfigPanel {
return {
id: Tine.Tinebase.data.Record.generateUID(),
config_class: _.find(mfaDevices, {mfa_config_id: this.store.getAt(this.selectedIndex).data.field1}).config_class,
mfa_config_id: this.getValue()
mfa_config_id: this.getValue(),
config: {
// this is a hack to transport the accountData to the UserConfigs UI (needed e.g. for h|totp
account_id: JSON.stringify(me.editDialog.record.data)
}
};
};
}));
Expand All @@ -60,7 +67,7 @@ const deviceTypeRenderer = (config_class, metadata, record) => {
const providerName = recordClass.getRecordName();
const mfaConfigId = _.get(record, 'data.mfa_config_id', _.get(record, 'mfa_config_id', providerName));

return mfaConfigId + (mfaConfigId !== providerName ? ` (${providerName})` : '');
return mfaConfigId + (mfaConfigId !== providerName ? ` (${i18n._hidden(providerName)})` : '');
}

Tine.widgets.grid.RendererManager.register('Tinebase', 'MFA_UserConfig', 'config_class', deviceTypeRenderer);
Expand Down

0 comments on commit 3a796b7

Please sign in to comment.