UNPKG

@okta/okta-signin-widget

Version:
221 lines (198 loc) 7.69 kB
import { $, View } from '@okta/courage'; import { BaseFormWithPolling } from '../internals'; import Logger from 'util/Logger'; import { AUTHENTICATOR_CANCEL_ACTION, AUTHENTICATION_CANCEL_REASONS, CHALLENGE_TIMEOUT, } from '../utils/Constants'; import BrowserFeatures from 'util/BrowserFeatures'; import { doChallenge, cancelPollingWithParams } from '../utils/ChallengeViewUtil'; const request = (opts) => { const ajaxOptions = Object.assign({ method: 'GET', contentType: 'application/json', }, opts); return $.ajax(ajaxOptions); }; const Body = BaseFormWithPolling.extend({ noButtonBar: true, className: 'ion-form device-challenge-poll', events: { 'click #launch-ov': function(e) { e.preventDefault(); this.doCustomURI(); } }, pollingCancelAction: AUTHENTICATOR_CANCEL_ACTION, initialize() { BaseFormWithPolling.prototype.initialize.apply(this, arguments); this.listenTo(this.model, 'error', this.onPollingFail); this.doChallenge(); this.startPolling(); }, doChallenge() { doChallenge(this); }, onPollingFail() { this.$('.spinner').hide(); this.stopPolling(); }, remove() { BaseFormWithPolling.prototype.remove.apply(this, arguments); this.stopProbing(); this.stopPolling(); }, getDeviceChallengePayload() { throw new Error('getDeviceChallengePayload needs to be implemented'); }, doLoopback(deviceChallenge) { let authenticatorDomainUrl = deviceChallenge.domain !== undefined ? deviceChallenge.domain : ''; let ports = deviceChallenge.ports !== undefined ? deviceChallenge.ports : []; let challengeRequest = deviceChallenge.challengeRequest !== undefined ? deviceChallenge.challengeRequest : ''; let probeTimeoutMillis = deviceChallenge.probeTimeoutMillis !== undefined ? deviceChallenge.probeTimeoutMillis : 100; let currentPort; let foundPort = false; let ovFailed = false; let countFailedPorts = 0; const getAuthenticatorUrl = (path) => { return `${authenticatorDomainUrl}:${currentPort}/${path}`; }; const checkPort = () => { return request({ url: getAuthenticatorUrl('probe'), /* OKTA-278573 in loopback server, SSL handshake sometimes takes more than 100ms and thus needs additional timeout however, increasing timeout is a temporary solution since user will need to wait much longer in worst case. TODO: Android timeout is temporarily set to 3000ms and needs optimization post-Beta. OKTA-365427 introduces probeTimeoutMillis; but we should also consider probeTimeoutMillisHTTPS for customizing timeouts in the more costly Android and other (keyless) HTTPS scenarios. */ timeout: BrowserFeatures.isAndroid() ? 3000 : probeTimeoutMillis }); }; const onPortFound = () => { return request({ url: getAuthenticatorUrl('challenge'), method: 'POST', data: JSON.stringify({ challengeRequest }), timeout: CHALLENGE_TIMEOUT // authenticator should respond within 5 min (300000ms) for challenge request }); }; const onFailure = () => { Logger.error(`Something unexpected happened while we were checking port ${currentPort}.`); }; const doProbing = () => { return checkPort() .done(() => { return onPortFound() .done(() => { foundPort = true; if (deviceChallenge.enhancedPollingEnabled !== false) { // this way we can gurantee that // 1. the polling is triggered right away (1ms interval) // 2. Only one polling queue // 3. follwoing polling will continue with refresh interval from previous polling response // NOTE: technically, there could still be concurrency issue where when we called stopPolling, // there is already a polling triggered and hasn't completed yet // but the possibility would be much smaller than previous concurrency issue // this is a best effort change this.stopPolling(); this.startPolling(1); return; } // once the OV challenge succeeds, // triggers another polling right away without waiting for the next ongoing polling to be triggered // to make the authentication flow goes faster return this.trigger('save', this.model); }) .fail((xhr) => { countFailedPorts++; // Windows and MacOS return status code 503 when // there are multiple profiles on the device and // the wrong OS profile responds to the challenge request if (xhr.status !== 503) { // when challenge responds with other errors, // - stop the remaining probing ovFailed = true; // - cancel polling right away cancelPollingWithParams( this.options.appState, this.pollingCancelAction, AUTHENTICATION_CANCEL_REASONS.OV_ERROR, xhr.status ); } else if (countFailedPorts === ports.length) { // when challenge is responded by the wrong OS profile and // all the ports are exhausted, // cancel the polling like the probing has failed cancelPollingWithParams( this.options.appState, this.pollingCancelAction, AUTHENTICATION_CANCEL_REASONS.LOOPBACK_FAILURE, null ); } }); }) .fail(onFailure); }; let probeChain = Promise.resolve(); ports.forEach(port => { probeChain = probeChain .then(() => { if (!(foundPort || ovFailed)) { currentPort = port; return doProbing(); } }) .catch(() => { countFailedPorts++; Logger.error(`Authenticator is not listening on port ${currentPort}.`); if (countFailedPorts === ports.length) { Logger.error('No available ports. Loopback server failed and polling is cancelled.'); // When no port is found, cancel the polling as well // This is to avoid concurrency issue where /poll/cancel takes long time to complete // and SIW will receive 400 error if the polling continues this.stopPolling(); cancelPollingWithParams( this.options.appState, this.pollingCancelAction, AUTHENTICATION_CANCEL_REASONS.LOOPBACK_FAILURE, null ); } }); }); }, doCustomURI() { this.ulDom && this.ulDom.remove(); const IframeView = createInvisibleIFrame('custom-uri-container', this.customURI); this.ulDom = this.add(IframeView).last(); }, doChromeDTC(deviceChallenge) { this.ulDom && this.ulDom.remove(); const IframeView = createInvisibleIFrame('chrome-dtc-container', deviceChallenge.href); this.ulDom = this.add(IframeView).last(); }, stopProbing() { this.checkPortXhr && this.checkPortXhr.abort(); this.probingXhr && this.probingXhr.abort(); }, }); function createInvisibleIFrame(iFrameId, iFrameSrc) { const iFrameView = View.extend({ tagName: 'iframe', id: iFrameId, attributes: { src: iFrameSrc, }, initialize() { this.el.style.display = 'none'; } }); return iFrameView; } export default Body;