diff --git a/common/src/misc/webauthn_iframe.ts b/common/src/misc/webauthn_iframe.ts index 24aac0f8..54dbdc45 100644 --- a/common/src/misc/webauthn_iframe.ts +++ b/common/src/misc/webauthn_iframe.ts @@ -1,30 +1,87 @@ import { I18nService } from '../abstractions/i18n.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { IFrameComponent } from './iframe_component'; -import { Utils } from './utils'; +export class WebAuthnIFrame { + private iframe: HTMLIFrameElement = null; + private connectorLink: HTMLAnchorElement; + private parseFunction = this.parseMessage.bind(this); -export class WebAuthnIFrame extends IFrameComponent { - constructor(win: Window, webVaultUrl: string, private webAuthnNewTab: boolean, + constructor(private win: Window, private webVaultUrl: string, private webAuthnNewTab: boolean, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - successCallback: (message: string) => any, errorCallback: (message: string) => any, - infoCallback: (message: string) => any) { - super(win, webVaultUrl, 'webauthn-connector.html', 'webauthn_iframe', successCallback, errorCallback, infoCallback); + private successCallback: Function, private errorCallback: Function, private infoCallback: Function) { + this.connectorLink = win.document.createElement('a'); } - init(data: any): void { - const params = this.createParams({ data: JSON.stringify(data), btnText: this.i18nService.t('webAuthnAuthenticate') }, 2); + const params = new URLSearchParams({ + data: this.base64Encode(JSON.stringify(data)), + parent: encodeURIComponent(this.win.document.location.href), + btnText: encodeURIComponent(this.i18nService.t('webAuthnAuthenticate')), + v: '1', + }); if (this.webAuthnNewTab) { // Firefox fallback which opens the webauthn page in a new tab params.append('locale', this.i18nService.translationLocale); this.platformUtilsService.launchUri(`${this.webVaultUrl}/webauthn-fallback-connector.html?${params}`); } else { - super.initComponent(params); - if (Utils.isNullOrWhitespace(this.iframe.allow)) { - this.iframe.allow = 'publickey-credentials-get ' + new URL(this.webVaultUrl).origin; - } + this.connectorLink.href = `${this.webVaultUrl}/webauthn-connector.html?${params}`; + this.iframe = this.win.document.getElementById('webauthn_iframe') as HTMLIFrameElement; + this.iframe.allow = 'publickey-credentials-get ' + new URL(this.webVaultUrl).origin; + this.iframe.src = this.connectorLink.href; + + this.win.addEventListener('message', this.parseFunction, false); } } + + stop() { + this.sendMessage('stop'); + } + + start() { + this.sendMessage('start'); + } + + sendMessage(message: any) { + if (!this.iframe || !this.iframe.src || !this.iframe.contentWindow) { + return; + } + + this.iframe.contentWindow.postMessage(message, this.iframe.src); + } + + base64Encode(str: string): string { + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => { + return String.fromCharCode(('0x' + p1) as any); + })); + } + + cleanup() { + this.win.removeEventListener('message', this.parseFunction, false); + } + + private parseMessage(event: MessageEvent) { + if (!this.validMessage(event)) { + return; + } + + const parts: string[] = event.data.split('|'); + if (parts[0] === 'success' && this.successCallback) { + this.successCallback(parts[1]); + } else if (parts[0] === 'error' && this.errorCallback) { + this.errorCallback(parts[1]); + } else if (parts[0] === 'info' && this.infoCallback) { + this.infoCallback(parts[1]); + } + } + + private validMessage(event: MessageEvent) { + if (event.origin == null || event.origin === '' || event.origin !== (this.connectorLink as any).origin || + event.data == null || typeof (event.data) !== 'string') { + return false; + } + + return event.data.indexOf('success|') === 0 || event.data.indexOf('error|') === 0 || + event.data.indexOf('info|') === 0; + } }