]> git.proxmox.com Git - proxmox-backup.git/blobdiff - www/LoginView.js
run cargo fmt
[proxmox-backup.git] / www / LoginView.js
index d03b0ea4a28c3cdb927fd6faefa3d52358c8ea46..33734f6c1b172217a3caa3132e649c8e0fe7a61e 100644 (file)
@@ -2,6 +2,21 @@ Ext.define('PBS.LoginView', {
     extend: 'Ext.container.Container',
     xtype: 'loginview',
 
+    viewModel: {
+       data: {
+           openid: false,
+       },
+       formulas: {
+           button_text: function(get) {
+               if (get("openid") === true) {
+                   return gettext("Login (OpenID redirect)");
+               } else {
+                   return gettext("Login");
+               }
+           },
+       },
+    },
+
     controller: {
        xclass: 'Ext.app.ViewController',
 
@@ -15,10 +30,33 @@ Ext.define('PBS.LoginView', {
                return;
            }
 
-           let params = loginForm.getValues();
+           let creds = loginForm.getValues();
 
-           params.username = params.username + '@' + params.realm;
-           delete params.realm;
+           if (this.getViewModel().data.openid === true) {
+               const redirectURL = location.origin;
+               try {
+                   let resp = await Proxmox.Async.api2({
+                       url: '/api2/extjs/access/openid/auth-url',
+                       params: {
+                           realm: creds.realm,
+                           "redirect-url": redirectURL,
+                       },
+                       method: 'POST',
+                   });
+                   window.location = resp.result.data;
+               } catch (response) {
+                   Proxmox.Utils.authClear();
+                   loginForm.unmask();
+                   Ext.MessageBox.alert(
+                       gettext('Error'),
+                       gettext('OpenID redirect failed, please try again') + `<br>${response.result.message}`,
+                   );
+               }
+               return;
+           }
+
+           creds.username = `${creds.username}@${creds.realm}`;
+           delete creds.realm;
 
            if (loginForm.isVisible()) {
                loginForm.mask(gettext('Please wait...'), 'x-mask-loading');
@@ -34,9 +72,9 @@ Ext.define('PBS.LoginView', {
            sp.set(saveunField.getStateId(), saveunField.getValue());
 
            try {
-               let resp = await PBS.Async.api2({
+               let resp = await Proxmox.Async.api2({
                    url: '/api2/extjs/access/ticket',
-                   params: params,
+                   params: creds,
                    method: 'POST',
                });
 
@@ -67,7 +105,7 @@ Ext.define('PBS.LoginView', {
            ));
 
            let resp = await new Promise((resolve, reject) => {
-               Ext.create('PBS.login.TfaWindow', {
+               Ext.create('Proxmox.window.TfaLoginWindow', {
                    userid,
                    ticket,
                    challenge,
@@ -98,6 +136,14 @@ Ext.define('PBS.LoginView', {
                    window.location.reload();
                },
            },
+           'field[name=realm]': {
+               change: function(f, value) {
+                   let record = f.store.getById(value);
+                   if (record === undefined) return;
+                   let data = record.data;
+                   this.getViewModel().set("openid", data.type === "openid");
+               },
+           },
            'button[reference=loginButton]': {
                click: 'submitForm',
            },
@@ -116,6 +162,43 @@ Ext.define('PBS.LoginView', {
                        var pwField = this.lookupReference('passwordField');
                        pwField.focus();
                    }
+
+                   let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization();
+                   if (auth !== undefined) {
+                       Proxmox.Utils.authClear();
+
+                       let loginForm = this.lookupReference('loginForm');
+                       loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading');
+
+                       // openID checks the original redirection URL we used, so pass that too
+                       const redirectURL = location.origin;
+
+                       Proxmox.Utils.API2Request({
+                           url: '/api2/extjs/access/openid/login',
+                           params: {
+                               state: auth.state,
+                               code: auth.code,
+                               "redirect-url": redirectURL,
+                           },
+                           method: 'POST',
+                           failure: function(response) {
+                               loginForm.unmask();
+                               let error = response.htmlStatus;
+                               Ext.MessageBox.alert(
+                                   gettext('Error'),
+                                   gettext('OpenID login failed, please try again') + `<br>${error}`,
+                                   () => { window.location = redirectURL; },
+                               );
+                           },
+                           success: function(response, options) {
+                               loginForm.unmask();
+                               let creds = response.result.data;
+                               PBS.Utils.updateLoginData(creds);
+                               PBS.app.changeView('mainview');
+                               history.replaceState(null, '', `${redirectURL}#pbsDashboard`);
+                           },
+                       });
+                   }
                },
            },
        },
@@ -191,6 +274,10 @@ Ext.define('PBS.LoginView', {
                            itemId: 'usernameField',
                            reference: 'usernameField',
                            stateId: 'login-username',
+                           bind: {
+                               visible: "{!openid}",
+                               disabled: "{openid}",
+                           },
                        },
                        {
                            xtype: 'textfield',
@@ -199,6 +286,10 @@ Ext.define('PBS.LoginView', {
                            name: 'password',
                            itemId: 'passwordField',
                            reference: 'passwordField',
+                           bind: {
+                               visible: "{!openid}",
+                               disabled: "{openid}",
+                           },
                        },
                        {
                            xtype: 'pmxRealmComboBox',
@@ -207,7 +298,7 @@ Ext.define('PBS.LoginView', {
                        {
                            xtype: 'proxmoxLanguageSelector',
                            fieldLabel: gettext('Language'),
-                           value: Ext.util.Cookies.get('PBSLangCookie') || Proxmox.defaultLang || 'en',
+                           value: Ext.util.Cookies.get('PBSLangCookie') || Proxmox.defaultLang || '__default__',
                            name: 'lang',
                            reference: 'langField',
                            submitValue: false,
@@ -223,9 +314,14 @@ Ext.define('PBS.LoginView', {
                            labelWidth: 250,
                            labelAlign: 'right',
                            submitValue: false,
+                           bind: {
+                               visible: "{!openid}",
+                           },
                        },
                        {
-                           text: gettext('Login'),
+                           bind: {
+                               text: "{button_text}",
+                           },
                            reference: 'loginButton',
                            formBind: true,
                        },
@@ -235,348 +331,3 @@ Ext.define('PBS.LoginView', {
        },
     ],
 });
-
-Ext.define('PBS.login.TfaWindow', {
-    extend: 'Ext.window.Window',
-    mixins: ['Proxmox.Mixin.CBind'],
-
-    title: gettext("Second login factor required"),
-
-    modal: true,
-    resizable: false,
-    width: 512,
-    layout: {
-       type: 'vbox',
-       align: 'stretch',
-    },
-
-    defaultButton: 'tfaButton',
-
-    viewModel: {
-       data: {
-           canConfirm: false,
-           availabelChallenge: {},
-       },
-    },
-
-    cancelled: true,
-
-    controller: {
-       xclass: 'Ext.app.ViewController',
-
-       init: function(view) {
-           let me = this;
-           let vm = me.getViewModel();
-
-           if (!view.userid) {
-               throw "no userid given";
-           }
-           if (!view.ticket) {
-               throw "no ticket given";
-           }
-           const challenge = view.challenge;
-           if (!challenge) {
-               throw "no challenge given";
-           }
-
-           let lastTabId = me.getLastTabUsed();
-           let initialTab = -1, i = 0;
-           for (const k of ['webauthn', 'totp', 'recovery']) {
-               const available = !!challenge[k];
-               vm.set(`availabelChallenge.${k}`, available);
-
-               if (available) {
-                   if (i === lastTabId) {
-                       initialTab = i;
-                   } else if (initialTab < 0) {
-                       initialTab = i;
-                   }
-               }
-               i++;
-           }
-           view.down('tabpanel').setActiveTab(initialTab);
-
-           if (challenge.recovery) {
-               me.lookup('availableRecovery').update(Ext.String.htmlEncode(
-                   gettext('Available recovery keys: ') + view.challenge.recovery.join(', '),
-               ));
-               me.lookup('availableRecovery').setVisible(true);
-               if (view.challenge.recovery.length <= 3) {
-                   me.lookup('recoveryLow').setVisible(true);
-               }
-           }
-
-           if (challenge.webauthn && initialTab === 0) {
-               let _promise = me.loginWebauthn();
-           }
-       },
-       control: {
-           'tabpanel': {
-               tabchange: function(tabPanel, newCard, oldCard) {
-                   // for now every TFA method has at max one field, so keep it simple..
-                   let oldField = oldCard.down('field');
-                   if (oldField) {
-                       oldField.setDisabled(true);
-                   }
-                   let newField = newCard.down('field');
-                   if (newField) {
-                       newField.setDisabled(false);
-                       newField.focus();
-                       newField.validate();
-                   }
-                   this.saveLastTabUsed(tabPanel, newCard);
-               },
-           },
-           'field': {
-               validitychange: function(field, valid) {
-                   // triggers only for enabled fields and we disable the one from the
-                   // non-visible tab, so we can just directly use the valid param
-                   this.getViewModel().set('canConfirm', valid);
-               },
-           },
-       },
-
-       saveLastTabUsed: function(tabPanel, card) {
-           let id = tabPanel.items.indexOf(card);
-           window.localStorage.setItem('PBS.TFALogin.lastTab', JSON.stringify({ id }));
-       },
-
-       getLastTabUsed: function() {
-           let data = window.localStorage.getItem('PBS.TFALogin.lastTab');
-           if (typeof data === 'string') {
-               let last = JSON.parse(data);
-               return last.id;
-           }
-           return null;
-       },
-
-       onClose: function() {
-           let me = this;
-           let view = me.getView();
-
-           if (!view.cancelled) {
-               return;
-           }
-
-           view.onReject();
-       },
-
-       cancel: function() {
-           this.getView().close();
-       },
-
-       loginTotp: function() {
-           let me = this;
-
-           let code = me.lookup('totp').getValue();
-           let _promise = me.finishChallenge(`totp:${code}`);
-       },
-
-       loginWebauthn: async function() {
-           let me = this;
-           let view = me.getView();
-
-           me.lookup('webAuthnWaiting').setVisible(true);
-
-           let challenge = view.challenge.webauthn;
-
-           if (typeof challenge.string !== 'string') {
-               // Byte array fixup, keep challenge string:
-               challenge.string = challenge.publicKey.challenge;
-               challenge.publicKey.challenge = PBS.Utils.base64url_to_bytes(challenge.string);
-               for (const cred of challenge.publicKey.allowCredentials) {
-                   cred.id = PBS.Utils.base64url_to_bytes(cred.id);
-               }
-           }
-
-           let controller = new AbortController();
-           challenge.signal = controller.signal;
-
-           let hwrsp;
-           try {
-               //Promise.race( ...
-               hwrsp = await navigator.credentials.get(challenge);
-           } catch (error) {
-               // we do NOT want to fail login because of canceling the challenge actively,
-               // in some browser that's the only way to switch over to another method as the
-               // disallow user input during the time the challenge is active
-               // checking for error.code === DOMException.ABORT_ERR only works in firefox -.-
-               this.getViewModel().set('canConfirm', true);
-               // FIXME: better handling, show some message, ...?
-               return;
-           } finally {
-               let waitingMessage = me.lookup('webAuthnWaiting');
-               if (waitingMessage) {
-                   waitingMessage.setVisible(false);
-               }
-           }
-
-           let response = {
-               id: hwrsp.id,
-               type: hwrsp.type,
-               challenge: challenge.string,
-               rawId: PBS.Utils.bytes_to_base64url(hwrsp.rawId),
-               response: {
-                   authenticatorData: PBS.Utils.bytes_to_base64url(
-                       hwrsp.response.authenticatorData,
-                   ),
-                   clientDataJSON: PBS.Utils.bytes_to_base64url(hwrsp.response.clientDataJSON),
-                   signature: PBS.Utils.bytes_to_base64url(hwrsp.response.signature),
-               },
-           };
-
-           await me.finishChallenge("webauthn:" + JSON.stringify(response));
-       },
-
-       loginRecovery: function() {
-           let me = this;
-
-           let key = me.lookup('recoveryKey').getValue();
-           let _promise = me.finishChallenge(`recovery:${key}`);
-       },
-
-       loginTFA: function() {
-           let me = this;
-           // avoid triggering more than once during challenge
-           me.getViewModel().set('canConfirm', false);
-           let view = me.getView();
-           let tfaPanel = view.down('tabpanel').getActiveTab();
-           me[tfaPanel.handler]();
-       },
-
-       finishChallenge: function(password) {
-           let me = this;
-           let view = me.getView();
-           view.cancelled = false;
-
-           let params = {
-               username: view.userid,
-               'tfa-challenge': view.ticket,
-               password,
-           };
-
-           let resolve = view.onResolve;
-           let reject = view.onReject;
-           view.close();
-
-           return PBS.Async.api2({
-               url: '/api2/extjs/access/ticket',
-               method: 'POST',
-               params,
-           })
-           .then(resolve)
-           .catch(reject);
-       },
-    },
-
-    listeners: {
-       close: 'onClose',
-    },
-
-    items: [{
-       xtype: 'tabpanel',
-       region: 'center',
-       layout: 'fit',
-       bodyPadding: 10,
-       stateId: 'pbs-tfa-login-panel', // FIXME: do manually - get/setState miss
-       stateful: true,
-       stateEvents: ['tabchange'],
-       items: [
-           {
-               xtype: 'panel',
-               title: 'WebAuthn',
-               iconCls: 'fa fa-fw fa-shield',
-               handler: 'loginWebauthn',
-               bind: {
-                   disabled: '{!availabelChallenge.webauthn}',
-               },
-               items: [
-                   {
-                       xtype: 'box',
-                       html: gettext('Please insert your authentication device and press its button'),
-                   },
-                   {
-                       xtype: 'box',
-                       html: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
-                       reference: 'webAuthnWaiting',
-                       hidden: true,
-                   },
-               ],
-           },
-           {
-               xtype: 'panel',
-               title: gettext('TOTP App'),
-               iconCls: 'fa fa-fw fa-clock-o',
-               handler: 'loginTotp',
-               bind: {
-                   disabled: '{!availabelChallenge.totp}',
-               },
-               items: [
-                   {
-                       xtype: 'textfield',
-                       fieldLabel: gettext('Please enter your TOTP verification code'),
-                       labelWidth: 300,
-                       name: 'totp',
-                       disabled: true,
-                       reference: 'totp',
-                       allowBlank: false,
-                       regex: /^[0-9]{6}$/,
-                       regexText: 'TOTP codes consist of six decimal digits',
-                   },
-               ],
-           },
-           {
-               xtype: 'panel',
-               title: gettext('Recovery Key'),
-               iconCls: 'fa fa-fw fa-file-text-o',
-               handler: 'loginRecovery',
-               bind: {
-                   disabled: '{!availabelChallenge.recovery}',
-               },
-               items: [
-                   {
-                       xtype: 'box',
-                       reference: 'availableRecovery',
-                       hidden: true,
-                   },
-                   {
-                       xtype: 'textfield',
-                       fieldLabel: gettext('Please enter one of your single-use recovery keys'),
-                       labelWidth: 300,
-                       name: 'recoveryKey',
-                       disabled: true,
-                       reference: 'recoveryKey',
-                       allowBlank: false,
-                       regex: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
-                       regexText: 'Does not looks like a valid recovery key',
-                   },
-                   {
-                       xtype: 'box',
-                       reference: 'recoveryInfo',
-                       hidden: true, // FIXME: remove this?
-                       html: gettext('Note that each recovery code can only be used once!'),
-                   },
-                   {
-                       xtype: 'box',
-                       reference: 'recoveryLow',
-                       hidden: true,
-                       html: '<i class="fa fa-exclamation-triangle warning"></i>'
-                           + gettext('Less than {0} recovery keys available. Please generate a new set!'),
-                   },
-               ],
-           },
-       ],
-    }],
-
-    buttons: [
-       {
-           text: gettext('Confirm Second Factor'),
-           handler: 'loginTFA',
-           reference: 'tfaButton',
-           disabled: true,
-           bind: {
-               disabled: '{!canConfirm}',
-           },
-       },
-    ],
-});