]>
git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/window/TfaWindow.js
2 Ext
.define('Proxmox.window.TfaLoginWindow', {
3 extend
: 'Ext.window.Window',
4 mixins
: ['Proxmox.Mixin.CBind'],
6 title
: gettext("Second login factor required"),
16 defaultButton
: 'tfaButton',
20 confirmText
: gettext('Confirm Second Factor'),
22 availableChallenge
: {},
29 xclass
: 'Ext.app.ViewController',
31 init: function(view
) {
33 let vm
= me
.getViewModel();
36 throw "no userid given";
39 throw "no ticket given";
41 const challenge
= view
.challenge
;
43 throw "no challenge given";
46 let lastTabId
= me
.getLastTabUsed();
47 let initialTab
= -1, i
= 0;
48 for (const k
of ['webauthn', 'totp', 'recovery', 'u2f', 'yubico']) {
49 const available
= !!challenge
[k
];
50 vm
.set(`availableChallenge.${k}`, available
);
53 if (i
=== lastTabId
) {
55 } else if (initialTab
< 0) {
61 view
.down('tabpanel').setActiveTab(initialTab
);
63 if (challenge
.recovery
) {
64 me
.lookup('availableRecovery').update(Ext
.String
.htmlEncode(
65 gettext('Available recovery keys: ') + view
.challenge
.recovery
.join(', '),
67 me
.lookup('availableRecovery').setVisible(true);
68 if (view
.challenge
.recovery
.length
<= 3) {
69 me
.lookup('recoveryLow').setVisible(true);
73 if (challenge
.webauthn
&& initialTab
=== 0) {
74 let _promise
= me
.loginWebauthn();
75 } else if (challenge
.u2f
&& initialTab
=== 3) {
76 let _promise
= me
.loginU2F();
81 tabchange: function(tabPanel
, newCard
, oldCard
) {
82 // for now every TFA method has at max one field, so keep it simple..
83 let oldField
= oldCard
.down('field');
85 oldField
.setDisabled(true);
87 let newField
= newCard
.down('field');
89 newField
.setDisabled(false);
94 let confirmText
= newCard
.confirmText
|| gettext('Confirm Second Factor');
95 this.getViewModel().set('confirmText', confirmText
);
97 this.saveLastTabUsed(tabPanel
, newCard
);
101 validitychange: function(field
, valid
) {
102 // triggers only for enabled fields and we disable the one from the
103 // non-visible tab, so we can just directly use the valid param
104 this.getViewModel().set('canConfirm', valid
);
106 afterrender
: field
=> field
.focus(), // ensure focus after initial render
110 saveLastTabUsed: function(tabPanel
, card
) {
111 let id
= tabPanel
.items
.indexOf(card
);
112 window
.localStorage
.setItem('Proxmox.TFALogin.lastTab', JSON
.stringify({ id
}));
115 getLastTabUsed: function() {
116 let data
= window
.localStorage
.getItem('Proxmox.TFALogin.lastTab');
117 if (typeof data
=== 'string') {
118 let last
= JSON
.parse(data
);
124 onClose: function() {
126 let view
= me
.getView();
128 if (!view
.cancelled
) {
136 this.getView().close();
139 loginTotp: function() {
142 let code
= me
.lookup('totp').getValue();
143 let _promise
= me
.finishChallenge(`totp:${code}`);
146 loginYubico: function() {
149 let code
= me
.lookup('yubico').getValue();
150 let _promise
= me
.finishChallenge(`yubico:${code}`);
153 loginWebauthn
: async
function() {
155 let view
= me
.getView();
157 me
.lookup('webAuthnWaiting').setVisible(true);
158 me
.lookup('webAuthnError').setVisible(false);
160 let challenge
= view
.challenge
.webauthn
;
162 if (typeof challenge
.string
!== 'string') {
163 // Byte array fixup, keep challenge string:
164 challenge
.string
= challenge
.publicKey
.challenge
;
165 challenge
.publicKey
.challenge
= Proxmox
.Utils
.base64url_to_bytes(challenge
.string
);
166 for (const cred
of challenge
.publicKey
.allowCredentials
) {
167 cred
.id
= Proxmox
.Utils
.base64url_to_bytes(cred
.id
);
171 let controller
= new AbortController();
172 challenge
.signal
= controller
.signal
;
177 hwrsp
= await navigator
.credentials
.get(challenge
);
179 // we do NOT want to fail login because of canceling the challenge actively,
180 // in some browser that's the only way to switch over to another method as the
181 // disallow user input during the time the challenge is active
182 // checking for error.code === DOMException.ABORT_ERR only works in firefox -.-
183 this.getViewModel().set('canConfirm', true);
184 // FIXME: better handling, show some message, ...?
185 me
.lookup('webAuthnError').setData({
186 error
: Ext
.htmlEncode(error
.toString()),
188 me
.lookup('webAuthnError').setVisible(true);
191 let waitingMessage
= me
.lookup('webAuthnWaiting');
192 if (waitingMessage
) {
193 waitingMessage
.setVisible(false);
200 challenge
: challenge
.string
,
201 rawId
: Proxmox
.Utils
.bytes_to_base64url(hwrsp
.rawId
),
203 authenticatorData
: Proxmox
.Utils
.bytes_to_base64url(
204 hwrsp
.response
.authenticatorData
,
206 clientDataJSON
: Proxmox
.Utils
.bytes_to_base64url(hwrsp
.response
.clientDataJSON
),
207 signature
: Proxmox
.Utils
.bytes_to_base64url(hwrsp
.response
.signature
),
211 await me
.finishChallenge("webauthn:" + JSON
.stringify(response
));
214 loginU2F
: async
function() {
216 let view
= me
.getView();
218 me
.lookup('u2fWaiting').setVisible(true);
219 me
.lookup('u2fError').setVisible(false);
223 hwrsp
= await
new Promise((resolve
, reject
) => {
225 let data
= view
.challenge
.u2f
;
226 let chlg
= data
.challenge
;
227 u2f
.sign(chlg
.appId
, chlg
.challenge
, data
.keys
, resolve
);
232 if (hwrsp
.errorCode
) {
233 throw Proxmox
.Utils
.render_u2f_error(hwrsp
.errorCode
);
235 delete hwrsp
.errorCode
;
237 this.getViewModel().set('canConfirm', true);
238 me
.lookup('u2fError').setData({
239 error
: Ext
.htmlEncode(error
.toString()),
241 me
.lookup('u2fError').setVisible(true);
244 let waitingMessage
= me
.lookup('u2fWaiting');
245 if (waitingMessage
) {
246 waitingMessage
.setVisible(false);
250 await me
.finishChallenge("u2f:" + JSON
.stringify(hwrsp
));
253 loginRecovery: function() {
256 let key
= me
.lookup('recoveryKey').getValue();
257 let _promise
= me
.finishChallenge(`recovery:${key}`);
260 loginTFA: function() {
262 // avoid triggering more than once during challenge
263 me
.getViewModel().set('canConfirm', false);
264 let view
= me
.getView();
265 let tfaPanel
= view
.down('tabpanel').getActiveTab();
266 me
[tfaPanel
.handler
]();
269 finishChallenge: function(password
) {
271 let view
= me
.getView();
272 view
.cancelled
= false;
275 username
: view
.userid
,
276 'tfa-challenge': view
.ticket
,
280 let resolve
= view
.onResolve
;
281 let reject
= view
.onReject
;
284 return Proxmox
.Async
.api2({
285 url
: '/api2/extjs/access/ticket',
307 iconCls
: 'fa fa-fw fa-shield',
308 confirmText
: gettext('Start WebAuthn challenge'),
309 handler
: 'loginWebauthn',
311 disabled
: '{!availableChallenge.webauthn}',
316 html
: gettext('Please insert your authentication device and press its button'),
320 html
: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
321 reference
: 'webAuthnWaiting',
329 tpl
: '<i class="fa fa-warning warning"></i> {error}',
330 reference
: 'webAuthnError',
337 title
: gettext('TOTP App'),
338 iconCls
: 'fa fa-fw fa-clock-o',
339 handler
: 'loginTotp',
341 disabled
: '{!availableChallenge.totp}',
346 fieldLabel
: gettext('Please enter your TOTP verification code'),
352 regex
: /^[0-9]{2,16}$/,
353 regexText
: gettext('TOTP codes usually consist of six decimal digits'),
359 title
: gettext('Recovery Key'),
360 iconCls
: 'fa fa-fw fa-file-text-o',
361 handler
: 'loginRecovery',
363 disabled
: '{!availableChallenge.recovery}',
368 reference
: 'availableRecovery',
373 fieldLabel
: gettext('Please enter one of your single-use recovery keys'),
377 reference
: 'recoveryKey',
379 regex
: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
380 regexText
: gettext('Does not look like a valid recovery key'),
384 reference
: 'recoveryLow',
386 html
: '<i class="fa fa-exclamation-triangle warning"></i>'
388 gettext('Less than {0} recovery keys available. Please generate a new set after login!'),
397 iconCls
: 'fa fa-fw fa-shield',
398 confirmText
: gettext('Start U2F challenge'),
401 disabled
: '{!availableChallenge.u2f}',
405 hidden
: '{!availableChallenge.u2f}',
411 html
: gettext('Please insert your authentication device and press its button'),
415 html
: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
416 reference
: 'u2fWaiting',
424 tpl
: '<i class="fa fa-warning warning"></i> {error}',
425 reference
: 'u2fError',
432 title
: gettext('Yubico OTP'),
433 iconCls
: 'fa fa-fw fa-yahoo',
434 handler
: 'loginYubico',
436 disabled
: '{!availableChallenge.yubico}',
440 hidden
: '{!availableChallenge.yubico}',
446 fieldLabel
: gettext('Please enter your Yubico OTP code'),
452 regex
: /^[a-z0-9]{30,60}$/, // *should* be 44 but not sure if that's "fixed"
453 regexText
: gettext('TOTP codes consist of six decimal digits'),
463 reference
: 'tfaButton',
466 text
: '{confirmText}',
467 disabled
: '{!canConfirm}',