authui-container/src/sign-in-ui.ts (153 lines of code) (raw):

/* * Copyright 2020 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ import {deepCopy, setStyleSheet} from './utils/index'; import {HttpClient, HttpRequestConfig} from './utils/http-client'; import {getBrowserName, BrowserName} from './utils/browser'; import {UiConfig} from '/../common/config'; // Import FirebaseUI dependencies. import * as firebaseui from 'firebaseui'; // Import GCIP/IAP module. import * as ciap from 'gcip-iap'; // The expected network timeout duraiton in milliseconds. const TIMEOUT_DURATION = 30000; // The /config HTTP request configuration. const GET_CONFIG_PARAMS: HttpRequestConfig = { method: 'GET', url: '/config', timeout: TIMEOUT_DURATION, }; // The current version of the hosted UI. export const HOSTED_UI_VERSION = '__XXX_HOSTED_UI_VERSION_XXX__'; /** Utility for handling sign-in with IAP external identities. */ export class SignInUi { private containerElement: HTMLElement; private titleElement: HTMLElement; private img: HTMLImageElement; private loadingSpinnerElement: HTMLElement | null; private separatorElement: HTMLElement; private ciapAuth: ciap.Authentication; private mainContainer: Element; private httpClient: HttpClient; /** * Instantiates a SignInUi instance for handling IAP external identities authentication. * @param container The container element / identifier where the UI will be rendered. */ constructor(private readonly container: string | HTMLElement) { this.httpClient = new HttpClient(); this.containerElement = typeof container === 'string' ? document.querySelector(container) : container; this.loadingSpinnerElement = document.getElementById('loading-spinner'); const elements = document.getElementsByClassName('main-container'); if (elements.length > 0 && elements[0]) { this.mainContainer = elements[0]; } else { throw new Error(`.main-container element not found`); } if (!this.containerElement) { throw new Error(`Container element ${container} not found`); } this.titleElement = document.getElementById('title'); if (!this.titleElement) { throw new Error(`#title element not found`); } this.separatorElement = document.getElementById('separator'); if (!this.separatorElement) { throw new Error(`#separator element not found`); } this.img = document.getElementById('logo') as HTMLImageElement; if (!this.img) { throw new Error(`#logo element not found`); } } /** @return A promise that resolves after the authenticaiton instance is started. */ render() { return this.getConfig() .then((configs) => { // Remove spinner if available. if (this.loadingSpinnerElement) { this.loadingSpinnerElement.remove(); } this.setCustomStyleSheet(configs); const config = this.generateFirebaseUiHandlerConfig(configs); // This will handle the underlying handshake for sign-in, sign-out, // token refresh, safe redirect to callback URL, etc. const handler = new firebaseui.auth.FirebaseUiHandler( this.container, config); // Log the hosted UI version. this.ciapAuth = new (ciap.Authentication as any)(handler, undefined, HOSTED_UI_VERSION); return this.ciapAuth.start(); }) .catch((error) => { this.handlerError(error); throw error; }); } /** * @return A promise that resolves with the loaded configuration file from /config. */ private getConfig(): Promise<UiConfig> { return this.httpClient.send(GET_CONFIG_PARAMS) .then((httpResponse) => { return httpResponse.data as UiConfig; }) .catch((error) => { const resp = error.response; const errorData = resp.data; throw new Error(errorData.error.message); }); } /** * Sets any custom CSS URL in the loaded configs to the current document. * @param configs The loaded configuration from /config. */ private setCustomStyleSheet(configs) { for (const apiKey in configs) { if (configs.hasOwnProperty(apiKey) && configs[apiKey].styleUrl) { setStyleSheet(document, configs[apiKey].styleUrl); break; } } } /** * Generates the CIAPHandlerConfig from the loaded config. * @param configs The loaded configuration from /config. * @return The generate object containing the associated CIAPHandlerConfig. */ private generateFirebaseUiHandlerConfig( configs): {[key: string]: firebaseui.auth.CIAPHandlerConfig} { // For prototyping purposes, only one API key should be available in the configuration. for (const apiKey in configs) { if (configs.hasOwnProperty(apiKey)) { const config = deepCopy(configs[apiKey]); const selectTenantUiTitle = config.selectTenantUiTitle; const selectTenantUiLogo = config.selectTenantUiLogo; config.callbacks = { selectTenantUiShown: () => { this.mainContainer.classList.remove('blend'); this.titleElement.innerText = selectTenantUiTitle; if (selectTenantUiLogo) { this.img.style.display = 'block'; this.img.src = selectTenantUiLogo; this.separatorElement.style.display = 'block'; } else { this.img.style.display = 'none'; this.separatorElement.style.display = 'none'; } }, selectTenantUiHidden: () => { this.titleElement.innerText = ''; }, signInUiShown: (tenantId) => { this.mainContainer.classList.remove('blend'); const key = tenantId || '_'; this.titleElement.innerText = config && config.tenants && config.tenants[key] && config.tenants[key].displayName; if (config.tenants[key].logoUrl) { this.img.style.display = 'block'; this.img.src = config.tenants[key].logoUrl; this.separatorElement.style.display = 'block'; } else { this.img.style.display = 'none'; this.separatorElement.style.display = 'none'; } }, }; // Do not trigger immediate redirect in Safari without some user // interaction. for (const tenantId in (config.tenants || {})) { if (config.tenants[tenantId].hasOwnProperty('immediateFederatedRedirect')) { config.tenants[tenantId].immediateFederatedRedirect = config.tenants[tenantId].immediateFederatedRedirect && getBrowserName() !== BrowserName.Safari; } } // Remove unsupported FirebaseUI configs. delete config.selectTenantUiLogo; delete config.selectTenantUiTitle; delete config.styleUrl; return { [apiKey]: config, }; } } return null; } /** * Displays the error message to the end user. * @param error The error to handle. */ private handlerError(error: Error) { // Remove spinner if available. if (this.loadingSpinnerElement) { this.loadingSpinnerElement.remove(); } // Show error message: errorData.error.message. this.mainContainer.classList.remove('blend'); this.separatorElement.style.display = 'none'; this.titleElement.innerText = ''; this.img.style.display = 'none'; this.containerElement.innerText = error.message; } }