authui-container/common/config-builder.ts (411 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 * as validators from './validator'; import {GcipConfig, TenantUiConfig, ExtendedTenantUiConfig, UiConfig} from './config'; // TODO: Temporary URLs for now. Replace with production ones when ready. // This is the icon for each tenant button in the tenant selection screen. export const TENANT_ICON_URL = 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/anonymous.png'; // List of required fields. const REQUIRED_FIELDS = [ '*.authDomain', '*.displayMode', '*.tenants.*.displayName', '*.tenants.*.iconUrl', '*.tenants.*.buttonColor', '*.tenants.*.signInOptions[]', ]; /** UiConfig validation tree. */ const VALIDATION_TREE: validators.ValidationTree = { '*': { nodes: { authDomain: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, displayMode: { validator: (value: any, key: string) => { if (value !== 'optionFirst' && value !== 'identifierFirst') { throw new Error(`"${key}" should be either "optionFirst" or "identifierFirst".`); } }, }, selectTenantUiTitle: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, selectTenantUiLogo: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, styleUrl: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, tosUrl: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, privacyPolicyUrl: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, tenants: { nodes: { '*': { nodes: { fullLabel: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, displayName: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, iconUrl: { validator: (value: any, key: string) => { if (!validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, logoUrl: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, buttonColor: { validator: (value: any, key: string) => { if (!validators.isValidColorString(value)) { throw new Error(`"${key}" should be a valid color string of format #xxxxxx.`); } }, }, tosUrl: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, privacyPolicyUrl: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, immediateFederatedRedirect: { validator: (value: any, key: string) => { if (!validators.isBoolean(value)) { throw new Error(`"${key}" should be a valid boolean.`); } }, }, signInFlow: { validator: (value: any, key: string) => { if (value !== 'popup' && value !== 'redirect') { throw new Error(`"${key}" should be either "popup" or "redirect".`); } }, }, adminRestrictedOperation: { nodes: { status: { validator: (value: any, key: string) => { if (!validators.isBoolean(value)) { throw new Error(`"${key}" should be a boolean.`); } }, }, adminEmail: { validator: (value: any, key: string) => { if (value && !validators.isEmail(value)) { throw new Error(`"${key}" should be a valid email.`); } }, }, helpLink: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, }, }, 'signInOptions[]': { // signInOptions can be a list of string too. validator: (value: any, key: string) => { if (!validators.isProviderId(value)) { throw new Error(`"${key}" should be a valid providerId string or provider object.`); } }, nodes: { provider: { validator: (value: any, key: string) => { if (!validators.isProviderId(value)) { throw new Error(`"${key}" should be a valid providerId string.`); } }, }, providerName: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, fullLabel: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, hd: { validator: (value: any, key: string) => { // Regexp is not an allowed JSON field. Limit to domains. if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid domain string.`); } }, }, buttonColor: { validator: (value: any, key: string) => { if (!validators.isValidColorString(value)) { throw new Error(`"${key}" should be a valid color string of format #xxxxxx.`); } }, }, iconUrl: { validator: (value: any, key: string) => { if (!validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, 'scopes[]': { validator: (value: any, key: string) => { // Google OAuth scopes are URLs. if (!validators.isSafeString(value) && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid array of OAuth scopes.`); } }, }, customParameters: { nodes: { '*': { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, }, }, loginHintKey: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, requireDisplayName: { validator: (value: any, key: string) => { if (!validators.isBoolean(value)) { throw new Error(`"${key}" should be a valid boolean.`); } }, }, recaptchaParameters: { nodes: { type: { validator: (value: any, key: string) => { if (value !== 'image' && value !== 'audio') { throw new Error(`"${key}" should be either "image" or "audio".`); } }, }, size: { validator: (value: any, key: string) => { if (value !== 'invisible' && value !== 'compact' && value !== 'normal') { throw new Error(`"${key}" should be one of ["invisible", "compact", "normal"].`); } }, }, badge: { validator: (value: any, key: string) => { if (value !== 'bottomright' && value !== 'bottomleft' && value !== 'inline') { throw new Error(`"${key}" should be one of ["bottomright", "bottomleft", "inline"].`); } }, }, }, }, defaultCountry: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, defaultNationalNumber: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, loginHint: { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, 'whitelistedCountries[]': { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, 'blacklistedCountries[]': { validator: (value: any, key: string) => { if (!validators.isSafeString(value)) { throw new Error(`"${key}" should be a valid string.`); } }, }, disableSignUp: { nodes: { status: { validator: (value: any, key: string) => { if (!validators.isBoolean(value)) { throw new Error(`"${key}" should be a boolean.`); } }, }, adminEmail: { validator: (value: any, key: string) => { if (value && !validators.isEmail(value)) { throw new Error(`"${key}" should be a valid email.`); } }, }, helpLink: { validator: (value: any, key: string) => { if (value && !validators.isHttpsURL(value)) { throw new Error(`"${key}" should be a valid HTTPS URL.`); } }, }, }, }, }, }, }, }, }, }, }, }, }; /** Utility for building the default UI config object. */ export class DefaultUiConfigBuilder { private static uiConfigValidator: validators.JsonObjectValidator = new validators.JsonObjectValidator(VALIDATION_TREE, REQUIRED_FIELDS); /** * Validates the provided UiConfig object. * @param config The input configuration to validate. */ public static validateConfig(config: UiConfig) { DefaultUiConfigBuilder.uiConfigValidator.validate(config); } /** * Instantiates a default UI config builder instance. * @param projectId The project ID to use. * @param gcipConfig The GCIP web config. * @param tenantUiConfigMap The map of tenant IDs to TenantUiConfig object. */ constructor( private readonly projectId: string, private readonly hostName: string, private readonly gcipConfig: GcipConfig, private readonly tenantUiConfigMap: {[key: string]: TenantUiConfig}) {} /** * @return The generated UiConfig object if available, null otherwise. */ build(): UiConfig | null { const tenantConfigs: {[key: string]: ExtendedTenantUiConfig} = {}; let charCode = 'A'.charCodeAt(0); const optionsMap = this.tenantUiConfigMap; const tenantIds: string[] = []; let totalSignInOptions: number = 0; for (const tenantId in optionsMap) { if (optionsMap.hasOwnProperty(tenantId)) { tenantIds.push(tenantId); } } tenantIds.forEach((tenantId) => { let key; let displayName; let fullLabel; if (tenantId.charAt(0) === '_') { key = '_'; displayName = (optionsMap[key] && optionsMap[key].displayName) || 'My Company'; fullLabel = optionsMap[key] && optionsMap[key].fullLabel; } else { key = tenantId; displayName = (optionsMap[key] && optionsMap[key].displayName) || `Company ${String.fromCharCode(charCode)}`; fullLabel = optionsMap[key] && optionsMap[key].fullLabel; charCode++; } totalSignInOptions += (optionsMap[key] && optionsMap[key].signInOptions && optionsMap[key].signInOptions.length) || 0; const adminRestrictedOperation = optionsMap[key] && optionsMap[key].adminRestrictedOperation; tenantConfigs[key] = { displayName, iconUrl: TENANT_ICON_URL, logoUrl: '', buttonColor: '#007bff', // By default, use immediate federated redirect. // This is safe since if more than one provider is used, FirebaseUI will ignore this. immediateFederatedRedirect: true, signInFlow: 'redirect', signInOptions: (optionsMap[key] && optionsMap[key].signInOptions) || [], tosUrl: '', privacyPolicyUrl: '', adminRestrictedOperation, }; if (!adminRestrictedOperation) { delete tenantConfigs[key].adminRestrictedOperation; } if (fullLabel) { tenantConfigs[key].fullLabel = fullLabel; } }); // IAP or IdPs not yet configured. if (totalSignInOptions === 0) {; return null; } let authDomain = this.gcipConfig.authDomain; // override authDomain to be the current IAP URL by default, // so that requests to "/__/auth" are sent on the same domain as the main UI. // These requests will be proxied to the original authDomain in auth-server.ts. if (validators.isNonEmptyString(this.hostName)) { authDomain = this.hostName; } return { [this.gcipConfig.apiKey]: { authDomain, displayMode: 'optionFirst', selectTenantUiTitle: this.projectId, selectTenantUiLogo: '', styleUrl: '', tenants: tenantConfigs, tosUrl: '', privacyPolicyUrl: '', }, }; } }