| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type {AuthCompletedCredentials, AuthParams} from 'chrome://chrome-signin/gaia_auth_host/authenticator.js'; |
| import {Authenticator} from 'chrome://chrome-signin/gaia_auth_host/authenticator.js'; |
| import {PostMessageApiServer} from 'chrome://resources/ash/common/post_message_api/post_message_api_server.js'; |
| |
| import type {EduCoexistenceBrowserProxy} from './edu_coexistence_browser_proxy.js'; |
| import {EduCoexistenceBrowserProxyImpl} from './edu_coexistence_browser_proxy.js'; |
| |
| const MILLISECONDS_PER_SECOND = 1000; |
| |
| export interface EduCoexistenceParams { |
| hl: string; |
| url: string; |
| clientId: string; |
| sourceUi: string; |
| clientVersion: string; |
| eduCoexistenceAccessToken: string; |
| eduCoexistenceId: string; |
| platformVersion: string; |
| releaseChannel: string; |
| deviceId: string; |
| email?: string; |
| readOnlyEmail?: string; |
| signinTime: number; |
| } |
| |
| function constructEduCoexistenceUrl(params: EduCoexistenceParams): URL { |
| const url = new URL(params.url); |
| url.searchParams.set('hl', params.hl); |
| url.searchParams.set('source_ui', params.sourceUi); |
| url.searchParams.set('client_id', params.clientId); |
| url.searchParams.set('client_version', params.clientVersion); |
| url.searchParams.set('edu_coexistence_id', params.eduCoexistenceId); |
| url.searchParams.set('platform_version', params.platformVersion); |
| url.searchParams.set('release_channel', params.releaseChannel); |
| url.searchParams.set('device_id', params.deviceId); |
| if (params.email) { |
| url.searchParams.set('email', params.email); |
| if (params.readOnlyEmail) { |
| url.searchParams.set('read_only_email', params.readOnlyEmail); |
| } |
| } |
| return url; |
| } |
| |
| /** |
| * Class that orchestrates the EDU Coexistence signin flow. |
| */ |
| export class EduCoexistenceController extends PostMessageApiServer { |
| authenticator: Authenticator; |
| private ui: Element; |
| private isOobe: boolean; |
| private flowUrl: URL; |
| private originUrlPrefix: string; |
| private webview: chrome.webviewTag.WebView; |
| private authCompletedReceived: boolean; |
| private browserProxy: EduCoexistenceBrowserProxy; |
| private eduCoexistenceAccessToken: string; |
| private signinTime: number; |
| private isDomLoaded: boolean; |
| private guestFlowState: number|null; |
| private userInfo: any; |
| |
| constructor(ui: Element, webview: Element, params: EduCoexistenceParams) { |
| const flowUrl = constructEduCoexistenceUrl(params); |
| const protocol = flowUrl.hostname === 'localhost' ? 'http://' : 'https://'; |
| const originUrlPrefix = protocol + flowUrl.host; |
| super(webview, originUrlPrefix, originUrlPrefix); |
| |
| this.ui = ui; |
| this.isOobe = params.sourceUi === 'oobe'; |
| this.flowUrl = flowUrl; |
| this.originUrlPrefix = originUrlPrefix; |
| this.webview = webview as chrome.webviewTag.WebView; |
| this.userInfo = null; |
| this.authCompletedReceived = false; |
| this.browserProxy = EduCoexistenceBrowserProxyImpl.getInstance(); |
| this.eduCoexistenceAccessToken = params.eduCoexistenceAccessToken; |
| this.signinTime = params.signinTime; |
| |
| this.webview.request.onBeforeSendHeaders.addListener( |
| (details) => { |
| if (this.originMatchesFilter(details.url)) { |
| details.requestHeaders.push({ |
| name: 'Authorization', |
| value: 'Bearer ' + this.eduCoexistenceAccessToken, |
| }); |
| } |
| |
| return {requestHeaders: details.requestHeaders}; |
| }, |
| |
| {urls: ['<all_urls>']}, ['blocking', 'requestHeaders']); |
| |
| /** |
| * The state of the guest content, saved as requested by |
| * the guest content to ensure that its state outlives content |
| * reload events, which destroy the state of the guest content. |
| * The value itself is opaque encoded binary data. |
| */ |
| this.guestFlowState = null; |
| this.authenticator = new Authenticator(this.webview); |
| |
| this.isDomLoaded = document.readyState !== 'loading'; |
| if (this.isDomLoaded) { |
| this.initializeAfterDomLoaded(); |
| } else { |
| document.addEventListener( |
| 'DOMContentLoaded', this.initializeAfterDomLoaded.bind(this)); |
| } |
| } |
| |
| override onInitializationError(origin: string) { |
| this.reportError( |
| ['Error initializing communication channel with origin:' + origin]); |
| } |
| |
| getIsOobe(): boolean { |
| return this.isOobe; |
| } |
| |
| /** |
| * Returns the hostname of the origin of the flow's URL (the one it was |
| * initialized with, not its current URL). |
| */ |
| getFlowOriginHostname(): string { |
| return this.flowUrl.hostname; |
| } |
| |
| private initializeAfterDomLoaded() { |
| this.isDomLoaded = true; |
| // Register methods with PostMessageAPI. |
| this.registerMethod('consentValid', this.consentValid.bind(this)); |
| this.registerMethod('consentLogged', this.consentLogged.bind(this)); |
| this.registerMethod('requestClose', this.requestClose.bind(this)); |
| this.registerMethod('reportError', this.reportError.bind(this)); |
| this.registerMethod( |
| 'saveGuestFlowState', this.saveGuestFlowState.bind(this)); |
| this.registerMethod( |
| 'fetchGuestFlowState', this.fetchGuestFlowState.bind(this)); |
| this.registerMethod( |
| 'getEduAccountEmail', this.getEduAccountEmail.bind(this)); |
| this.registerMethod( |
| 'getTimeDeltaSinceSigninSeconds', |
| this.getTimeDeltaSinceSigninSeconds.bind(this)); |
| |
| // Add listeners for Authenticator. |
| this.addAuthenticatorListeners(); |
| } |
| |
| /** |
| * Loads the flow into the controller. |
| */ |
| loadAuthenticator(data: AuthParams) { |
| // We use the Authenticator to set the web flow URL instead |
| // of setting it ourselves, so that the content isn't loaded twice. |
| // This is why this class doesn't directly set webview.src_ (except in |
| // onAuthCompleted below to handle the corner case of loading |
| // accounts.google.com for running against webserver running on localhost). |
| // The EDU Coexistence web flow will be responsible for constructing |
| // and forwarding to the accounts.google.com URL that Authenticator |
| // interacts with. |
| data.frameUrl = this.flowUrl; |
| this.authenticator.load(data.authMode, data); |
| } |
| |
| /** |
| * Resets the internal state of the controller. |
| */ |
| reset() { |
| this.userInfo = null; |
| this.authCompletedReceived = false; |
| } |
| |
| private addAuthenticatorListeners() { |
| this.authenticator.addEventListener('ready', () => this.onAuthReady()); |
| this.authenticator.addEventListener( |
| 'getAccounts', () => this.onGetAccounts()); |
| this.authenticator.addEventListener( |
| 'getDeviceId', () => this.onGetDeviceId()); |
| this.authenticator.addEventListener( |
| 'authCompleted', |
| e => this.onAuthCompleted(e as CustomEvent<AuthCompletedCredentials>)); |
| } |
| |
| private onAuthReady() { |
| this.browserProxy.authenticatorReady(); |
| } |
| |
| private onGetAccounts() { |
| this.browserProxy.getAccounts().then(result => { |
| this.authenticator.getAccountsResponse(result); |
| }); |
| } |
| |
| private onGetDeviceId() { |
| this.browserProxy.getDeviceId().then(deviceId => { |
| this.authenticator.getDeviceIdResponse(deviceId); |
| }); |
| } |
| |
| private onAuthCompleted(e: CustomEvent<AuthCompletedCredentials>) { |
| this.authCompletedReceived = true; |
| this.userInfo = e.detail; |
| this.browserProxy.completeLogin(e.detail); |
| |
| // The EDU Signin page doesn't forward to the next page on success, so we |
| // have to manually update the src to continue to the last page of the flow. |
| const finishUrl = this.flowUrl; |
| finishUrl.pathname = '/supervision/coexistence/finish'; |
| this.webview.src = finishUrl.toString(); |
| } |
| |
| /** Informs API that the parent consent is now valid. */ |
| private consentValid() { |
| this.browserProxy.consentValid(); |
| } |
| |
| private consentLogged(eduCoexistenceToSVersion: string[]): Promise<boolean> { |
| // The first argument of eduCoexistenceToSVersion contains the ToS version. |
| return this.browserProxy.consentLogged( |
| this.userInfo.email, eduCoexistenceToSVersion[0]); |
| } |
| |
| /** Attempts to close the widget hosting the flow. */ |
| private requestClose() { |
| this.browserProxy.dialogClose(); |
| } |
| |
| private saveGuestFlowState(guestFlowState: number[]) { |
| // The first argument of guestFlowState contains the guest flow state. |
| this.guestFlowState = guestFlowState[0]; |
| } |
| |
| /** |
| * Returns the guest flow state previously saved using saveGuestFlowState(). |
| */ |
| private fetchGuestFlowState(): {'state': number|null} { |
| return {'state': this.guestFlowState}; |
| } |
| |
| private getEduAccountEmail(): string { |
| console.assert(this.userInfo); |
| return this.userInfo.email; |
| } |
| |
| /** |
| * Notifies the API that there was an unrecoverable error during the flow. |
| * Takes an array that contains the error message at index 0. |
| */ |
| private reportError(error: string[]) { |
| // Notify the app to switch to error screen. |
| this.ui.dispatchEvent(new CustomEvent('go-error')); |
| |
| // Send the error strings to C++ handler so they are logged. |
| this.browserProxy.onError(error); |
| } |
| |
| /** |
| * Made public for testing purposes. |
| * Returns the number of seconds that have elapsed since the user's initial |
| * signin. |
| */ |
| getTimeDeltaSinceSigninSeconds(): number { |
| return (Date.now() - this.signinTime) / MILLISECONDS_PER_SECOND; |
| } |
| } |