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

Commit

Permalink
feature(Tinebase) webauthn mfa ui
Browse files Browse the repository at this point in the history
  • Loading branch information
corneliusweiss authored and paulmhh committed Aug 12, 2021
1 parent 6a82c7a commit e6cc6bc
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 53 deletions.
28 changes: 20 additions & 8 deletions tine20/Tinebase/js/MFA/HTOTPSecretField.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const HTOTOPSecretField = Ext.extend(Ext.form.FieldSet, {
this.qrField = new Ext.BoxComponent({
width: 150,
height: 150,
html: '<img style="width: 100%; height: 100%"/>'
html: '<canvas style="width: 100%; height: 100%"/><img src="favicon/180" style="display: none;">'
});
this.items = [
this.explainText,
Expand All @@ -55,12 +55,10 @@ const HTOTOPSecretField = Ext.extend(Ext.form.FieldSet, {
if (!value && !record.id) {
supr(i18n._('Generating secret key ...'));
this.secretField.setDisabled(true);
// @TODO: use rfc lib and remove bas32-encode
// const rfc4648 = await import(/* webpackChunkName: "Tinebase/js/rfc4648" */ 'rfc4648');
import(/* webpackChunkName: "Tinebase/js/base32-encode" */ 'base32-encode').then((module) => {
import(/* webpackChunkName: "Tinebase/js/rfc4648" */ 'rfc4648').then((module) => {
const bytes = new Uint8Array(35);
window.crypto.getRandomValues(bytes);
supr(module.default(bytes, 'RFC3548'));
supr(module.base32.stringify(bytes));
this.secretField.setDisabled(false);
this.onValueChange();
});
Expand All @@ -76,6 +74,10 @@ const HTOTOPSecretField = Ext.extend(Ext.form.FieldSet, {
const type = this.type.toLowerCase();
const account = encodeURIComponent(this.editDialog.blConfigPanel.account.get('accountLoginName'));
const issuer = encodeURIComponent(window.location.hostname);
const canvas = this.qrField.el.child('canvas').dom;
const favicon = this.qrField.el.child('img').dom;
const context = canvas.getContext("2d");


let uri = `otpauth://${type}/${issuer}:${account}?secret=${secret}&issuer=${issuer}`;
// uri += "&algorithm=" + this.editDialog.record.get('algorithm');
Expand All @@ -84,11 +86,21 @@ const HTOTOPSecretField = Ext.extend(Ext.form.FieldSet, {
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?;
uri += "&image=" + encodeURIComponent(Tine.Tinebase.common.getUrl() + Tine.Tinebase.registry.get('favicon/180')); // freeOTP only?;

const QRCode = await import(/* webpackChunkName: "Tinebase/js/qrcode" */ 'qrcode');
const imgURL = await QRCode.toDataURL(uri);
this.qrField.el.child('img').dom.src = imgURL;
await QRCode.toCanvas(canvas, uri, {width: 200, errorCorrectionLevel: 'H'});

context.beginPath();
context.arc(100, 100, 25, 0, 2 * Math.PI, false);
context.fillStyle = 'white';
context.fill();
context.drawImage(favicon, 80, 80, 40, 40);
favicon.addEventListener('load', e => {
context.drawImage(favicon, 80, 80, 40, 40);
});

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

const typeString = this.editDialog.record.constructor.getRecordName();
this.editDialog.window.setTitle(`${typeString} ${i18n._('for')} ${account} : ${issuer}`);
Expand Down
44 changes: 15 additions & 29 deletions tine20/Tinebase/js/MFA/Providers/WebAuthn.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class WebAuthn extends Abstract {
const rfc4648 = await import(/* webpackChunkName: "Tinebase/js/rfc4648" */ 'rfc4648');
const publicKeyOptions = await Tine.Tinebase.getWebAuthnAuthenticateOptionsForMFA(this.username, this.mfaDevice.id);
const accountid = publicKeyOptions.extensions.userHandle;
console.log('accountid', accountid)
publicKeyOptions.challenge = rfc4648.base64url.parse(publicKeyOptions.challenge, { loose: true });
for (let allowCredential of publicKeyOptions.allowCredentials) {
allowCredential.id = rfc4648.base64url.parse(allowCredential.id, { loose: true });
Expand All @@ -47,35 +46,22 @@ class WebAuthn extends Abstract {
const unlockMethod = this.unlockMethod || Tine.Tinebase_AreaLock.unlock
await unlockMethod(this.areaName, this.mfaDevice.id, JSON.stringify(publicKeyData));
} catch (e) {
//@TODO!!!
debugger
console.error(e);
Ext.MessageBox.show({
icon: Ext.MessageBox.WARNING,
buttons: Ext.MessageBox.OKCANCEL,
title: i18n._('Error'),
msg: i18n._("FIDO2 WebAuthn authentication failed. Try again?"),
fn: (btn) => {
if (btn === 'ok') {
Ext.MessageBox.hide();
return this.unlock();
} else {
throw new Error('USERABORT');
}
}
});
}



// let me = this
// return new Promise((resolve, reject) => {
// let pwDlg = new Tine.Tinebase.widgets.dialog.PasswordDialog({
// windowTitle: me.windowTitle,
// questionText: me.questionText,
// passwordFieldLabel: me.passwordFieldLabel,
// allowEmptyPassword: false,
// hasPwGen: false,
// locked: !me.isOTP
// })
// pwDlg.openWindow()
// pwDlg.on('apply', async (password) => {
// try {
// const unlockMethod = this.unlockMethod || Tine.Tinebase_AreaLock.unlock
// resolve(await unlockMethod(me.areaName, me.mfaDevice.id, password))
// } catch (e) {
// reject(e)
// }
// })
// pwDlg.on('cancel', () => {
// reject(new Error('USERABORT'))
// })
// })
}
}
export default WebAuthn
19 changes: 17 additions & 2 deletions tine20/Tinebase/js/MFA/WebAuthnPublicKeyDataField.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,24 @@ const WebAuthnPublicKeyDataField = Ext.extend(Ext.form.FieldSet, {
}

this.publicKeyDataField.setValue(JSON.stringify(publicKeyData));
this.editDialog.record.set('id', Tine.Tinebase.data.Record.generateUID());
this.editDialog.onSaveAndClose();
} catch (e) {
//@TODO!!!
debugger
console.error(e);
Ext.MessageBox.show({
icon: Ext.MessageBox.WARNING,
buttons: Ext.MessageBox.OKCANCEL,
title: i18n._('Error'),
msg: i18n._("FIDO2 WebAuthn registration failed. Try again?"),
fn: (btn) => {
if (btn === 'ok') {
Ext.MessageBox.hide();
return this.register();
} else {
this.editDialog.window.close(true);
}
}
});
}

return publicKeyOptions;
Expand Down
17 changes: 4 additions & 13 deletions tine20/Tinebase/js/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion tine20/Tinebase/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"ace-builds": "^1.4.11",
"async": "^3.2.0",
"axios": "^0.21.1",
"base32-encode": "^2.0.0",
"bootstrap": "^4.5.0",
"bootstrap-vue": "^2.14.0",
"ciena-dagre": "^1.0.10",
Expand Down

0 comments on commit e6cc6bc

Please sign in to comment.