// 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.
/**
* @fileoverview An UI component to let user init online re-auth flow on
* the lock screen.
*/
import 'chrome://resources/ash/common/cr.m.js';
import 'chrome://resources/ash/common/event_target.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import './components/buttons/oobe_text_button.js';
import './components/oobe_icons.html.js';
import './components/oobe_illo_icons.html.js';
import './gaia_action_buttons/gaia_action_buttons.js';
import '//resources/ash/common/cr_elements/policy/cr_tooltip_icon.js';
import '//resources/polymer/v3_0/iron-icon/iron-icon.js';
import {AuthCompletedCredentials, AuthCompletedEvent, AuthDomainChangeEvent, Authenticator, AuthFlow, AuthFlowChangeEvent, AuthMode, AuthParams, LoadAbortEvent, SUPPORTED_PARAMS} from '//lock-reauth/gaia_auth_host/authenticator.js';
import {CrInputElement} from '//resources/ash/common/cr_elements/cr_input/cr_input.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {sendWithPromise} from 'chrome://resources/js/cr.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './lock_screen_reauth.html.js';
const clearDataType: chrome.webviewTag.ClearDataTypeSet = {
appcache: true,
cache: true,
cookies: true,
};
interface LockReauthParams {
fallbackGaiaPath: string;
webviewPartitionName: string;
showVerificationNotice: boolean;
}
const LockReauthElementBase = I18nMixin(PolymerElement);
interface LockReauthElement {
$: {
confirmPasswordInput: CrInputElement,
oldPasswordInput: CrInputElement,
passwordInput: CrInputElement,
};
}
class LockReauthElement extends LockReauthElementBase {
static get is() {
return 'lock-reauth';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/**
* User non-canonicalized email for display
*/
email: {
type: String,
value: '',
},
/**
* Auth Domain property of the authenticator. Updated via events.
*/
authDomain: {
type: String,
value: '',
},
/**
* Whether the ‘verify user again’ screen is shown.
*/
isErrorDisplayed: {
type: Boolean,
value: false,
},
/**
* Whether the webview for online sign-in is shown.
*/
isSigninFrameDisplayed: {
type: Boolean,
value: false,
},
/**
* Whether the authenticator is currently showing SAML IdP page.
*/
isSaml: {
type: Boolean,
value: false,
},
/**
* Whether default SAML IdP is shown.
*/
isDefaultSsoProvider: {
type: Boolean,
value: false,
},
/**
* Whether there is a failure to scrape the user's password.
*/
isConfirmPassword: {
type: Boolean,
value: false,
},
/**
* Whether no password is scraped or multiple passwords are scraped.
*/
isManualInput: {
type: Boolean,
value: false,
},
/**
* Whether the user's password has changed.
*/
isPasswordChanged: {
type: Boolean,
value: false,
},
passwordConfirmAttempt: {
type: Number,
value: 0,
},
passwordChangeAttempt: {
type: Number,
value: 0,
},
};
}
email: string;
authDomain: string;
isButtonsEnabled: boolean;
isErrorDisplayed: boolean;
isSigninFrameDisplayed: boolean;
isSaml: boolean;
isDefaultSsoProvider: boolean;
isConfirmPassword: boolean;
isManualInput: boolean;
isPasswordChanged: boolean;
passwordConfirmAttempt: number;
passwordChangeAttempt: number;
/**
* Saved authenticator load params.
*/
private authenticatorParams: null|AuthParams = null;
/**
* The UI component that hosts IdP pages.
*/
authenticator?: Authenticator;
/**
* Webview that view IdP page
*/
private signinFrame?: chrome.webviewTag.WebView;
/**
* Gaia path which can serve as a fallback in reloading scenarios. Expected
* to correspond to editable Gaia username page.
* TODO(b/259181755): this should no longer be needed once we change the
* implementation of the "Enter Google Account info" button to fully reload
* the flow through cpp code.
*/
private fallbackGaiaPath?: string;
override ready() {
super.ready();
this.signinFrame = this.getSigninFrame();
const authenticator = this.authenticator =
new Authenticator(this.signinFrame);
const authenticatorEventListeners: Record<string, (e: any) => void> = {
'authDomainChange': (e: AuthDomainChangeEvent) => {
this.authDomain = e.detail.newValue;
},
'authCompleted': (e: AuthCompletedEvent) =>
void this.onAuthCompletedMessage(e.detail),
'loadAbort': (e: LoadAbortEvent) =>
void this.onLoadAbortMessage(e.detail),
'getDeviceId': (_: Event) => {
sendWithPromise('getDeviceId')
.then(deviceId => authenticator.getDeviceIdResponse(deviceId));
},
'authFlowChange': (e: AuthFlowChangeEvent) => {
this.isSaml = e.detail.newValue === AuthFlow.SAML;
},
};
for (const eventName in authenticatorEventListeners) {
this.authenticator.addEventListener(
eventName, authenticatorEventListeners[eventName].bind(this));
}
chrome.send('initialize');
}
private resetState() {
this.isErrorDisplayed = false;
this.isSaml = false;
this.isSigninFrameDisplayed = false;
this.isConfirmPassword = false;
this.isManualInput = false;
this.isPasswordChanged = false;
this.authDomain = '';
}
/**
* Set the orientation which will be used in styling webui.
* @param isHorizontal whether the orientation is horizontal or
* vertical.
*/
setOrientation(isHorizontal: boolean) {
if (isHorizontal) {
document.documentElement.setAttribute('orientation', 'horizontal');
} else {
document.documentElement.setAttribute('orientation', 'vertical');
}
}
/**
* Set the width which will be used in styling webui.
* @param width the width of the dialog.
*/
setWidth(width: number) {
document.documentElement.style.setProperty(
'--lock-screen-reauth-dialog-width', width + 'px');
}
/**
* Loads the authentication parameters.
* @param data authenticator parameters bag.
*/
loadAuthenticator(data: LockReauthParams&AuthParams) {
assert(
'webviewPartitionName' in data,
'ERROR: missing webview partition name');
assert(this.authenticator, 'ERROR: Authenticator not yet initialized');
this.authenticator.setWebviewPartition(data.webviewPartitionName);
this.fallbackGaiaPath = data.fallbackGaiaPath;
const params: AuthParams = {} as AuthParams;
SUPPORTED_PARAMS.forEach((name: string) => {
if (data.hasOwnProperty(name)) {
params[name] = data[name];
}
});
params.enableGaiaActionButtons = data.enableGaiaActionButtons;
this.authenticatorParams = params;
this.email = data.email;
this.isDefaultSsoProvider = !!data.doSamlRedirect;
this.isSaml = this.isDefaultSsoProvider;
this.doGaiaRedirect();
chrome.send('authenticatorLoaded');
}
/**
* This function is used when the wrong user is verified correctly
* It reset authenticator state and display error message.
*/
resetAuthenticator() {
this.getSigninFrame().clearData({since: 0}, clearDataType, () => {
this.authenticator!.resetStates();
this.isButtonsEnabled = true;
this.isErrorDisplayed = true;
});
}
/**
* Reloads the page.
*/
reloadAuthenticator() {
this.getSigninFrame().clearData({since: 0}, clearDataType, () => {
this.authenticator!.resetStates();
});
}
private getSigninFrame(): chrome.webviewTag.WebView {
// Note: Can't use |this.$|, since it returns cached references to elements
// originally present in DOM, while the signin-frame is dynamically
// recreated (see Authenticator.setWebviewPartition()).
const signinFrame = this.shadowRoot!.getElementById('signin-frame');
assert(signinFrame, 'ERROR: signin-frame not found');
return signinFrame as chrome.webviewTag.WebView;
}
private setFocusToWebview() {
this.signinFrame!.focus();
}
onAuthCompletedMessage(credentials: AuthCompletedCredentials) {
chrome.send('completeAuthentication', [
credentials.gaiaId,
credentials.email,
credentials.password,
credentials.scrapedSAMLPasswords,
credentials.usingSAML,
credentials.services,
credentials.passwordAttributes,
]);
}
/**
* Invoked when onLoadAbort message received.
* @param data Additional information about error event like:
* {number} error_code Error code such as net::ERR_INTERNET_DISCONNECTED.
* {string} src The URL that failed to load.
*/
private onLoadAbortMessage(data: LoadAbortEvent['detail']) {
chrome.send('webviewLoadAborted', [data.error_code]);
}
/**
* Invoked when the user has successfully authenticated via SAML,
* the Chrome Credentials Passing API was not used and the authenticator needs
* the user to confirm the scraped password.
* @param passwordCount The number of passwords that were scraped.
*/
showSamlConfirmPassword(passwordCount: number) {
this.resetState();
/**
* This statement override resetState calls.
* Thus have to be AFTER resetState.
*/
this.isConfirmPassword = true;
this.isManualInput = (passwordCount === 0);
if (this.passwordConfirmAttempt > 0) {
this.$.passwordInput.value = '';
this.$.passwordInput.invalid = true;
}
this.passwordConfirmAttempt++;
}
/**
* Invoked when the user's password doesn't match his old password.
*/
private passwordChanged() {
this.resetState();
this.isPasswordChanged = true;
this.passwordChangeAttempt++;
if (this.passwordChangeAttempt > 1) {
this.$.oldPasswordInput.invalid = true;
}
}
private onVerify() {
assert(
this.authenticatorParams,
'ERROR: authenticator parameters not yet loaded');
this.authenticator!.load(AuthMode.DEFAULT, this.authenticatorParams);
this.resetState();
/**
* These statements override resetStates calls.
* Thus have to be AFTER resetState.
*/
this.isSigninFrameDisplayed = true;
}
private onConfirm() {
if (!this.$.passwordInput.validate()) {
return;
}
if (this.isManualInput) {
// When using manual password entry, both passwords must match.
if (!this.$.confirmPasswordInput.validate()) {
return;
}
if (this.$.confirmPasswordInput.value !== this.$.passwordInput.value) {
this.$.passwordInput.invalid = true;
this.$.confirmPasswordInput.invalid = true;
return;
}
}
chrome.send('onPasswordTyped', [this.$.passwordInput.value]);
}
private onCloseClick() {
chrome.send('dialogClose');
}
private onNext() {
if (!this.$.oldPasswordInput.validate()) {
this.$.oldPasswordInput.focusInput();
return;
}
chrome.send('updateUserPassword', [this.$.oldPasswordInput.value]);
this.$.oldPasswordInput.value = '';
}
private doGaiaRedirect() {
assert(
this.authenticatorParams,
'ERROR: authenticator parameters not yet loaded');
this.authenticator!.load(AuthMode.DEFAULT, this.authenticatorParams);
this.resetState();
/**
* These statements override resetStates calls.
* Thus have to be AFTER resetState.
*/
this.isSigninFrameDisplayed = true;
}
private passwordPlaceholder(_locale: string, isManualInput: boolean) {
return this.i18n(
isManualInput ? 'manualPasswordInputLabel' : 'confirmPasswordLabel');
}
private passwordErrorText(_locale: string, isManualInput: boolean) {
return this.i18n(
isManualInput ? 'manualPasswordMismatch' :
'passwordChangedIncorrectOldPassword');
}
/**
* Invoked when "Enter Google Account info" button is pressed on SAML screen.
*/
private onChangeSigninProviderClicked() {
assert(
this.authenticatorParams,
'ERROR: authenticator parameters not yet loaded');
this.authenticatorParams.doSamlRedirect = false;
this.authenticatorParams.enableGaiaActionButtons = true;
this.isDefaultSsoProvider = false;
this.isSaml = false;
// Replace Gaia path with a fallback path to land on Gaia username page.
assert(
this.fallbackGaiaPath,
'fallback Gaia path needed when trying to switch from SAML to Gaia');
this.authenticatorParams.gaiaPath = this.fallbackGaiaPath;
this.authenticator!.load(AuthMode.DEFAULT, this.authenticatorParams);
}
private policyProvidedTrustedAnchorsUsed() {
return loadTimeData.getBoolean('policyProvidedCaCertsPresent');
}
}
declare global {
interface HTMLElementTagNameMap {
'lock-reauth': LockReauthElement;
}
}
customElements.define(LockReauthElement.is, LockReauthElement);