From fd15c094069bbdb08529d55b2a7e9101ad88e872 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 1 Feb 2018 22:59:04 -0500 Subject: [PATCH] 2fa support --- package.json | 2 +- src/app/accounts/login.component.html | 2 +- src/app/accounts/login.component.ts | 5 +- src/app/accounts/two-factor.component.html | 51 ++- src/app/accounts/two-factor.component.ts | 122 +++++- src/app/app.module.ts | 1 + src/app/main.ts | 1 + src/app/services/services.module.ts | 5 +- src/locales/en/messages.json | 65 +++ src/main.ts | 2 +- src/scripts/duo.js | 418 +++++++++++++++++++ src/scss/misc.scss | 11 + src/scss/pages.scss | 18 +- src/services/desktopPlatformUtils.service.ts | 6 + 14 files changed, 681 insertions(+), 28 deletions(-) create mode 100644 src/scripts/duo.js diff --git a/package.json b/package.json index 17c24fccc9..1ff4e8548a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "ts-loader": "^3.0.5", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", - "typescript": "^2.5.3", + "typescript": "^2.6.2", "webpack": "^3.8.1", "webpack-merge": "^4.1.0", "webpack-node-externals": "^1.6.0" diff --git a/src/app/accounts/login.component.html b/src/app/accounts/login.component.html index d5ac698f55..682f6eed06 100644 --- a/src/app/accounts/login.component.html +++ b/src/app/accounts/login.component.html @@ -1,7 +1,7 @@
bitwarden -

{{'loginOrCreateNewAccount' | i18n}}

+

{{'loginOrCreateNewAccount' | i18n}}

diff --git a/src/app/accounts/login.component.ts b/src/app/accounts/login.component.ts index c144476e18..f2336097b2 100644 --- a/src/app/accounts/login.component.ts +++ b/src/app/accounts/login.component.ts @@ -9,6 +9,8 @@ import { Router } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; import { ToasterService } from 'angular2-toaster'; +import { AuthResult } from 'jslib/models/domain/authResult'; + import { AuthService } from 'jslib/abstractions/auth.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; @@ -19,7 +21,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service'; export class LoginComponent { email: string = ''; masterPassword: string = ''; - formPromise: Promise; + formPromise: Promise; constructor(private authService: AuthService, private router: Router, private analytics: Angulartics2, private toasterService: ToasterService, private i18nService: I18nService) { } @@ -47,7 +49,6 @@ export class LoginComponent { if (response.twoFactor) { this.analytics.eventTrack.next({ action: 'Logged In To Two-step' }); this.router.navigate(['2fa']); - // TODO: pass 2fa info } else { this.analytics.eventTrack.next({ action: 'Logged In' }); this.router.navigate(['vault']); diff --git a/src/app/accounts/two-factor.component.html b/src/app/accounts/two-factor.component.html index 0583c1b477..b70909afc8 100644 --- a/src/app/accounts/two-factor.component.html +++ b/src/app/accounts/two-factor.component.html @@ -1,24 +1,65 @@ - +

{{title}}

-
+

{{'enterVerificationCodeApp' | i18n}}

+

{{'enterVerificationCodeEmail' | i18n}}

+
- - + + +
+
+
+ +

{{'insertYubiKey' | i18n}}

+ +
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+
+
+

{{'noTwoStepProviders' | i18n}}

+

{{'noTwoStepProviders2' | i18n}}

{{'cancel' | i18n}}
+
diff --git a/src/app/accounts/two-factor.component.ts b/src/app/accounts/two-factor.component.ts index e7cc670ee3..4782cdce54 100644 --- a/src/app/accounts/two-factor.component.ts +++ b/src/app/accounts/two-factor.component.ts @@ -2,6 +2,7 @@ import * as template from './two-factor.component.html'; import { Component, + OnInit, } from '@angular/core'; import { Router } from '@angular/router'; @@ -9,28 +10,99 @@ import { Router } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; import { ToasterService } from 'angular2-toaster'; -import { RegisterRequest } from 'jslib/models/request/registerRequest'; +import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; + +import { TwoFactorEmailRequest } from 'jslib/models/request/twoFactorEmailRequest'; import { ApiService } from 'jslib/abstractions/api.service'; import { AuthService } from 'jslib/abstractions/auth.service'; -import { CryptoService } from 'jslib/abstractions/crypto.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; + +import { TwoFactorProviders } from 'jslib/services/auth.service'; @Component({ selector: 'app-two-factor', template: template, }) -export class TwoFactorComponent { +export class TwoFactorComponent implements OnInit { token: string = ''; remember: boolean = false; - providerType: number; - email: string; - masterPassword: string; + u2fReady: boolean = false; + providers = TwoFactorProviders; + providerType = TwoFactorProviderType; + selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; + u2fSupported: boolean = false; + u2f: any = null; + title: string = ''; + useVerificationCode: boolean = false; + twoFactorEmail: string = null; formPromise: Promise; + emailPromise: Promise; constructor(private authService: AuthService, private router: Router, private analytics: Angulartics2, - private toasterService: ToasterService, private i18nService: I18nService, - private cryptoService: CryptoService, private apiService: ApiService) { } + private toasterService: ToasterService, private i18nService: I18nService, private apiService: ApiService, + private platformUtilsService: PlatformUtilsService) { + this.u2fSupported = this.platformUtilsService.supportsU2f(window); + } + + async ngOnInit() { + if (this.authService.email == null || this.authService.masterPasswordHash == null || + this.authService.twoFactorProviders == null) { + this.router.navigate(['login']); + return; + } + + this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.u2fSupported); + await this.init(); + } + + async init() { + this.useVerificationCode = this.selectedProviderType === TwoFactorProviderType.Email || + this.selectedProviderType === TwoFactorProviderType.Authenticator || + this.selectedProviderType === TwoFactorProviderType.Yubikey; + + if (this.selectedProviderType == null) { + this.title = this.i18nService.t('loginUnavailable'); + return; + } + + this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; + const params = this.authService.twoFactorProviders.get(this.selectedProviderType); + switch (this.selectedProviderType) { + case TwoFactorProviderType.U2f: + if (!this.u2fSupported) { + break; + } + + const challenges = JSON.parse(params['Challenges']); + // TODO: init u2f + break; + case TwoFactorProviderType.Duo: + setTimeout(() => { + (window as any).Duo.init({ + host: params['Host'], + sig_request: params['Signature'], + submit_callback: async (f: HTMLFormElement) => { + const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; + if (sig != null) { + this.token = sig.value; + await this.submit(); + } + } + }); + }); + break; + case TwoFactorProviderType.Email: + this.twoFactorEmail = params['Email']; + if (this.authService.twoFactorProviders.size > 1) { + await this.sendEmail(false); + } + break; + default: + break; + } + } async submit() { if (this.token == null || this.token === '') { @@ -39,17 +111,41 @@ export class TwoFactorComponent { return; } - // TODO: stop U2f - // TODO: normalize token + if (this.selectedProviderType === TwoFactorProviderType.U2f) { + // TODO: stop U2f + } else if (this.selectedProviderType === TwoFactorProviderType.Email || + this.selectedProviderType === TwoFactorProviderType.Authenticator) { + this.token = this.token.replace(' ', '').trim(); + } try { - this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.providerType, - this.token, this.remember); + this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); await this.formPromise; this.analytics.eventTrack.next({ action: 'Logged In From Two-step' }); this.router.navigate(['vault']); } catch { - // TODO: start U2F + if (this.selectedProviderType === TwoFactorProviderType.U2f) { + // TODO: start U2F again + } } } + + async sendEmail(doToast: boolean) { + if (this.selectedProviderType !== TwoFactorProviderType.Email) { + return; + } + + try { + const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash); + this.emailPromise = this.apiService.postTwoFactorEmail(request); + await this.emailPromise; + if (doToast) { + // TODO toast + } + } catch { } + } + + anotherMethod() { + // TODO + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b9032a0f24..bec35815a2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -73,6 +73,7 @@ import { ViewComponent } from './vault/view.component'; TwoFactorComponent, VaultComponent, ViewComponent, + WebviewDirective, ], entryComponents: [ AttachmentsComponent, diff --git a/src/app/main.ts b/src/app/main.ts index bc3ef6b900..0de443196d 100644 --- a/src/app/main.ts +++ b/src/app/main.ts @@ -2,6 +2,7 @@ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; require('../scss/styles.scss'); +require('../scripts/duo.js'); import { AppModule } from './app.module'; diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index b591d5e113..35853594b9 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -92,8 +92,8 @@ const syncService = new SyncService(userService, apiService, settingsService, const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService); const totpService = new TotpService(storageService); const containerService = new ContainerService(cryptoService, platformUtilsService); -const authService: AuthServiceAbstraction = new AuthService(cryptoService, apiService, - userService, tokenService, appIdService, platformUtilsService, constantsService, +const authService = new AuthService(cryptoService, apiService, + userService, tokenService, appIdService, i18nService, platformUtilsService, constantsService, messagingService); const analytics = new Analytics(window, null, platformUtilsService, storageService, appIdService); @@ -105,6 +105,7 @@ environmentService.setUrlsFromStorage().then(() => { function initFactory(i18n: I18nService, platformUtilsService: DesktopPlatformUtilsService): Function { return async () => { await i18n.init(); + await authService.init(); const htmlEl = window.document.documentElement; htmlEl.classList.add('os_' + platformUtilsService.getDeviceString()); htmlEl.classList.add('locale_' + i18n.translationLocale); diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 6c2635c462..738f4cc9e1 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -445,5 +445,70 @@ }, "noItemsInList": { "message": "There are no items to list." + }, + "verificationCode": { + "message": "Verification Code" + }, + "verificationCodeRequired": { + "message": "Verification code is required." + }, + "continue": { + "message": "Continue" + }, + "enterVerificationCodeApp": { + "message": "Enter the 6 digit verification code from your authenticator app." + }, + "enterVerificationCodeEmail": { + "message": "Enter the 6 digit verification code that was emailed to" + }, + "rememberMe": { + "message": "Remember me" + }, + "sendVerificationCodeEmailAgain": { + "message": "Send verification code email again" + }, + "useAnotherTwoStepMethod": { + "message": "Use another two-step login method" + }, + "insertYubiKey": { + "message": "Insert your YubiKey into your computer's USB port, then touch its button." + }, + "insertU2f": { + "message": "Insert your security key into your computer's USB port. If it has a button, touch it." + }, + "recoveryCodeDesc": { + "message": "Lost access to all of your two-factor providers? Use your recovery code to disable all two-factor providers from your account." + }, + "recoveryCodeTitle": { + "message": "Recovery Code" + }, + "authenticatorAppTitle": { + "message": "Authenticator App" + }, + "authenticatorAppDesc": { + "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", + "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + }, + "yubiKeyTitle": { + "message": "YubiKey OTP Security Key" + }, + "yubiKeyDesc": { + "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." + }, + "duoDesc": { + "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." + }, + "u2fDesc": { + "message": "Use any FIDO U2F enabled security key to access your account." + }, + "u2fTitle": { + "message": "FIDO U2F Security Key" + }, + "emailTitle": { + "message": "Email" + }, + "emailDesc": { + "message": "Verification codes will be emailed to you." } } diff --git a/src/main.ts b/src/main.ts index 5df545547a..a56af68f38 100644 --- a/src/main.ts +++ b/src/main.ts @@ -45,7 +45,7 @@ function createWindow() { // Create the browser window. win = new BrowserWindow({ width: primaryScreenSize.width < 950 ? primaryScreenSize.width : 950, - height: primaryScreenSize.height < 700 ? primaryScreenSize.height : 700, + height: primaryScreenSize.height < 600 ? primaryScreenSize.height : 600, minWidth: 680, minHeight: 500, title: app.getName(), diff --git a/src/scripts/duo.js b/src/scripts/duo.js new file mode 100644 index 0000000000..8b712dcf25 --- /dev/null +++ b/src/scripts/duo.js @@ -0,0 +1,418 @@ +/** + * Duo Web SDK v2 + * Copyright 2017, Duo Security + */ + +var Duo; +(function (root, factory) { + // Browser globals (root is window) + var d = factory(); + // If the Javascript was loaded via a script tag, attempt to autoload + // the frame. + d._onReady(d.init); + // Attach Duo to the `window` object + root.Duo = Duo = d; +}(window, function () { + var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/; + var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/; + var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/; + var VALID_OPEN_WINDOW_DOMAINS = [ + 'duo.com', + 'duosecurity.com', + 'duomobile.s3-us-west-1.amazonaws.com' + ]; + + var iframeId = 'duo_iframe', + postAction = '', + postArgument = 'sig_response', + host, + sigRequest, + duoSig, + appSig, + iframe, + submitCallback; + + function throwError(message, url) { + throw new Error( + 'Duo Web SDK error: ' + message + + (url ? ('\n' + 'See ' + url + ' for more information') : '') + ); + } + + function hyphenize(str) { + return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase(); + } + + // cross-browser data attributes + function getDataAttribute(element, name) { + if ('dataset' in element) { + return element.dataset[name]; + } else { + return element.getAttribute('data-' + hyphenize(name)); + } + } + + // cross-browser event binding/unbinding + function on(context, event, fallbackEvent, callback) { + if ('addEventListener' in window) { + context.addEventListener(event, callback, false); + } else { + context.attachEvent(fallbackEvent, callback); + } + } + + function off(context, event, fallbackEvent, callback) { + if ('removeEventListener' in window) { + context.removeEventListener(event, callback, false); + } else { + context.detachEvent(fallbackEvent, callback); + } + } + + function onReady(callback) { + on(document, 'DOMContentLoaded', 'onreadystatechange', callback); + } + + function offReady(callback) { + off(document, 'DOMContentLoaded', 'onreadystatechange', callback); + } + + function onMessage(callback) { + on(window, 'message', 'onmessage', callback); + } + + function offMessage(callback) { + off(window, 'message', 'onmessage', callback); + } + + /** + * Parse the sig_request parameter, throwing errors if the token contains + * a server error or if the token is invalid. + * + * @param {String} sig Request token + */ + function parseSigRequest(sig) { + if (!sig) { + // nothing to do + return; + } + + // see if the token contains an error, throwing it if it does + if (sig.indexOf('ERR|') === 0) { + throwError(sig.split('|')[1]); + } + + // validate the token + if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) { + throwError( + 'Duo was given a bad token. This might indicate a configuration ' + + 'problem with one of Duo\'s client libraries.', + 'https://www.duosecurity.com/docs/duoweb#first-steps' + ); + } + + var sigParts = sig.split(':'); + + // hang on to the token, and the parsed duo and app sigs + sigRequest = sig; + duoSig = sigParts[0]; + appSig = sigParts[1]; + + return { + sigRequest: sig, + duoSig: sigParts[0], + appSig: sigParts[1] + }; + } + + /** + * This function is set up to run when the DOM is ready, if the iframe was + * not available during `init`. + */ + function onDOMReady() { + iframe = document.getElementById(iframeId); + + if (!iframe) { + throw new Error( + 'This page does not contain an iframe for Duo to use.' + + 'Add an element like ' + + 'to this page. ' + + 'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' + + 'for more information.' + ); + } + + // we've got an iframe, away we go! + ready(); + + // always clean up after yourself + offReady(onDOMReady); + } + + /** + * Validate that a MessageEvent came from the Duo service, and that it + * is a properly formatted payload. + * + * The Google Chrome sign-in page injects some JS into pages that also + * make use of postMessage, so we need to do additional validation above + * and beyond the origin. + * + * @param {MessageEvent} event Message received via postMessage + */ + function isDuoMessage(event) { + return Boolean( + event.origin === ('https://' + host) && + typeof event.data === 'string' && + ( + event.data.match(DUO_MESSAGE_FORMAT) || + event.data.match(DUO_ERROR_FORMAT) || + event.data.match(DUO_OPEN_WINDOW_FORMAT) + ) + ); + } + + /** + * Validate the request token and prepare for the iframe to become ready. + * + * All options below can be passed into an options hash to `Duo.init`, or + * specified on the iframe using `data-` attributes. + * + * Options specified using the options hash will take precedence over + * `data-` attributes. + * + * Example using options hash: + * ```javascript + * Duo.init({ + * iframe: "some_other_id", + * host: "api-main.duo.test", + * sig_request: "...", + * post_action: "/auth", + * post_argument: "resp" + * }); + * ``` + * + * Example using `data-` attributes: + * ``` + * + * ``` + * + * @param {Object} options + * @param {String} options.iframe The iframe, or id of an iframe to set up + * @param {String} options.host Hostname + * @param {String} options.sig_request Request token + * @param {String} [options.post_action=''] URL to POST back to after successful auth + * @param {String} [options.post_argument='sig_response'] Parameter name to use for response token + * @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute + * the callback function with reference to the "duo_form" form object + * submit_callback can be used to prevent the webpage from reloading. + */ + function init(options) { + if (options) { + if (options.host) { + host = options.host; + } + + if (options.sig_request) { + parseSigRequest(options.sig_request); + } + + if (options.post_action) { + postAction = options.post_action; + } + + if (options.post_argument) { + postArgument = options.post_argument; + } + + if (options.iframe) { + if (options.iframe.tagName) { + iframe = options.iframe; + } else if (typeof options.iframe === 'string') { + iframeId = options.iframe; + } + } + + if (typeof options.submit_callback === 'function') { + submitCallback = options.submit_callback; + } + } + + // if we were given an iframe, no need to wait for the rest of the DOM + if (false && iframe) { + ready(); + } else { + // try to find the iframe in the DOM + iframe = document.getElementById(iframeId); + + // iframe is in the DOM, away we go! + if (iframe) { + ready(); + } else { + // wait until the DOM is ready, then try again + onReady(onDOMReady); + } + } + + // always clean up after yourself! + offReady(init); + } + + /** + * This function is called when a message was received from another domain + * using the `postMessage` API. Check that the event came from the Duo + * service domain, and that the message is a properly formatted payload, + * then perform the post back to the primary service. + * + * @param event Event object (contains origin and data) + */ + function onReceivedMessage(event) { + if (isDuoMessage(event)) { + if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) { + var url = event.data.substring("DUO_OPEN_WINDOW|".length); + if (isValidUrlToOpen(url)) { + // Open the URL that comes after the DUO_WINDOW_OPEN token. + window.open(url, "_self"); + } + } + else { + // the event came from duo, do the post back + doPostBack(event.data); + + // always clean up after yourself! + offMessage(onReceivedMessage); + } + } + } + + /** + * Validate that this passed in URL is one that we will actually allow to + * be opened. + * @param url String URL that the message poster wants to open + * @returns {boolean} true if we allow this url to be opened in the window + */ + function isValidUrlToOpen(url) { + if (!url) { + return false; + } + + var parser = document.createElement('a'); + parser.href = url; + + if (parser.protocol === "duotrustedendpoints:") { + return true; + } else if (parser.protocol !== "https:") { + return false; + } + + for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) { + if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) || + parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) { + return true; + } + } + return false; + } + + /** + * Point the iframe at Duo, then wait for it to postMessage back to us. + */ + function ready() { + if (!host) { + host = getDataAttribute(iframe, 'host'); + + if (!host) { + throwError( + 'No API hostname is given for Duo to use. Be sure to pass ' + + 'a `host` parameter to Duo.init, or through the `data-host` ' + + 'attribute on the iframe element.', + 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe' + ); + } + } + + if (!duoSig || !appSig) { + parseSigRequest(getDataAttribute(iframe, 'sigRequest')); + + if (!duoSig || !appSig) { + throwError( + 'No valid signed request is given. Be sure to give the ' + + '`sig_request` parameter to Duo.init, or use the ' + + '`data-sig-request` attribute on the iframe element.', + 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe' + ); + } + } + + // if postAction/Argument are defaults, see if they are specified + // as data attributes on the iframe + if (postAction === '') { + postAction = getDataAttribute(iframe, 'postAction') || postAction; + } + + if (postArgument === 'sig_response') { + postArgument = getDataAttribute(iframe, 'postArgument') || postArgument; + } + + // point the iframe at Duo + iframe.src = [ + 'https://', host, '/frame/web/v1/auth?tx=', duoSig, + '&parent=', encodeURIComponent(document.location.href), + '&v=2.6' + ].join(''); + + // listen for the 'message' event + onMessage(onReceivedMessage); + } + + /** + * We received a postMessage from Duo. POST back to the primary service + * with the response token, and any additional user-supplied parameters + * given in form#duo_form. + */ + function doPostBack(response) { + // create a hidden input to contain the response token + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = postArgument; + input.value = response + ':' + appSig; + + // user may supply their own form with additional inputs + var form = document.getElementById('duo_form'); + + // if the form doesn't exist, create one + if (!form) { + form = document.createElement('form'); + + // insert the new form after the iframe + iframe.parentElement.insertBefore(form, iframe.nextSibling); + } + + // make sure we are actually posting to the right place + form.method = 'POST'; + form.action = postAction; + + // add the response token input to the form + form.appendChild(input); + + // away we go! + if (typeof submitCallback === "function") { + submitCallback.call(null, form); + } else { + form.submit(); + } + } + + return { + init: init, + _onReady: onReady, + _parseSigRequest: parseSigRequest, + _isDuoMessage: isDuoMessage, + _doPostBack: doPostBack + }; +})); diff --git a/src/scss/misc.scss b/src/scss/misc.scss index 50345ad044..1d900075cd 100644 --- a/src/scss/misc.scss +++ b/src/scss/misc.scss @@ -104,3 +104,14 @@ } } +#duo-frame { + background: url('../images/loading.svg') 0 0 no-repeat; + width: 100%; + height: 300px; + + iframe { + width: 100%; + height: 100%; + border: none; + } +} diff --git a/src/scss/pages.scss b/src/scss/pages.scss index 3913c528ff..40ad264317 100644 --- a/src/scss/pages.scss +++ b/src/scss/pages.scss @@ -23,19 +23,31 @@ } } -#register-page, #hint-page { +#register-page, #hint-page, #two-factor-page { margin-top: 20px; .content { margin: 0 auto; } + + img { + margin-bottom: 10px; + max-width: 100%; + height: auto; + display: block; + border-radius: $border-radius; + } + + p { + text-align: center + } } -#login-page, #register-page, #hint-page { +#login-page, #register-page, #hint-page, #two-factor-page { .content { max-width: 300px; - p, h1 { + p.lead, h1 { font-size: $font-size-large; text-align: center; margin-bottom: 20px; diff --git a/src/services/desktopPlatformUtils.service.ts b/src/services/desktopPlatformUtils.service.ts index 494895300d..355ab1bc28 100644 --- a/src/services/desktopPlatformUtils.service.ts +++ b/src/services/desktopPlatformUtils.service.ts @@ -117,4 +117,10 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService { getApplicationVersion(): string { return (window as any).require('electron').remote.app.getVersion(); } + + supportsU2f(win: Window): boolean { + // Not supported in Electron at this time. + // ref: https://github.com/electron/electron/issues/3226 + return false; + } }