authui-container/server/api/gcip-handler.ts (300 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 {AuthenticatedRequestHandler} from './authenticated-request-handler'; import {AccessTokenManager} from './token-manager'; import {ApplicationData} from './metadata-server'; /** Get GCIP config endpoint. */ const GET_GCIP_CONFIG_URL = 'https://identitytoolkit.googleapis.com/admin/v2/projects/{projectId}/config'; /** Get tenant config endpoint. */ const GET_TENANT_CONFIG_URL = 'https://identitytoolkit.googleapis.com/v2/projects/{projectId}/tenants/{tenantId}'; /** Get default Idps config endpoint. */ const GET_DEFAULT_IDPS_URL = 'https://identitytoolkit.googleapis.com/v2/{resourceId}/defaultSupportedIdpConfigs?pageSize={pageSize}'; /** Get SAML Idps config endpoint. */ const GET_SAML_IDPS_URL = 'https://identitytoolkit.googleapis.com/v2/{resourceId}/inboundSamlConfigs?pageSize={pageSize}'; /** Get OIDC Idps config endpoint. */ const GET_OIDC_IDPS_URL = 'https://identitytoolkit.googleapis.com/v2/{resourceId}/oauthIdpConfigs?pageSize={pageSize}'; /** Single page size for listing IdPs. */ const PAGE_SIZE = 100; /** Default error message to show when GCIP config cannot be retrieved. */ export const DEFAULT_ERROR_GET_GCIP_CONFIG = 'Unable to retrieve Identity Platform config.'; /** Default error message to show when tenant config cannot be retrieved. */ export const DEFAULT_ERROR_GET_TENANT_CONFIG = 'Unable to retrieve tenant config.'; /** Default error message to show when default IdPs config cannot be retrieved. */ export const DEFAULT_ERROR_GET_DEFAULT_IDPS_CONFIG = 'Unable to retrieve default IdPs config.'; /** Default error message to show when SAML IdPs cannot be retrieved. */ export const DEFAULT_ERROR_GET_SAML_IDPS_CONFIG = 'Unable to retrieve SAML IdPs config.'; /** Default error message to show when OIDC IdPs cannot be retrieved. */ export const DEFAULT_ERROR_GET_OIDC_IDPS_CONFIG = 'Unable to retrieve OIDC IdPs config.'; /** Network request timeout duration. */ const TIMEOUT_DURATION = 10000; // List of client GCIP interfaces. export interface GcipConfig { apiKey: string; authDomain: string; } export interface SignInOption { provider: string; providerName?: string; } export interface TenantUiConfig { displayName?: string; signInOptions: SignInOption[]; } // List of backend response interfaces. export interface Config { client?: { apiKey?: string; firebaseSubdomain?: string; }; signIn?: { email?: { enabled?: boolean; [key: string]: any; }; phoneNumber?: { enabled?: boolean; [key: string]: any; }; } [key: string]: any; } export interface Tenant { name?: string; displayName?: string; allowPasswordSignup?: boolean; enableEmailLinkSignin?: boolean; [key: string]: any; } export interface ListDefaultSupportedIdpConfigsResponse { defaultSupportedIdpConfigs?: { name?: string; enabled?: boolean; [key: string]: any; }; nextPageToken?: string; } export interface ListInboundSamlConfigsResponse { inboundSamlConfigs?: { name?: string; enabled?: boolean; displayName?: string; [key: string]: any; }; nextPageToken?: string; } export interface ListOAuthIdpConfigsResponse { oauthIdpConfigs?: { name?: string; enabled?: boolean; displayName?: string; [key: string]: any; }; nextPageToken?: string; } /** * Utility used to make GCIP API calls. This is currently used to build the GCIP config * for web UI rendering. It is also used to retrieve the list of enabled IdPs for a * GCIP tenant in a format that is compatible with FirebaseUI. */ export class GcipHandler { private getGcipConfigHandler: AuthenticatedRequestHandler; private getDefaultIdpsHandler: AuthenticatedRequestHandler; private getSamlIdpsHandler: AuthenticatedRequestHandler; private getOidcIdpsHandler: AuthenticatedRequestHandler; private getTenantBasicConfigHandler: AuthenticatedRequestHandler; /** * Instantiates an GCIP handler. * @param app The application data. * @param accessTokenManager The access token manager. */ constructor( private readonly app: ApplicationData, private readonly accessTokenManager: AccessTokenManager) { this.getGcipConfigHandler = new AuthenticatedRequestHandler({ method: 'GET', url: GET_GCIP_CONFIG_URL, timeout: TIMEOUT_DURATION, }, this.accessTokenManager, app.log.bind(app)); this.getDefaultIdpsHandler = new AuthenticatedRequestHandler({ method: 'GET', url: GET_DEFAULT_IDPS_URL, timeout: TIMEOUT_DURATION, }, this.accessTokenManager, app.log.bind(app)); this.getSamlIdpsHandler = new AuthenticatedRequestHandler({ method: 'GET', url: GET_SAML_IDPS_URL, timeout: TIMEOUT_DURATION, }, this.accessTokenManager, app.log.bind(app)); this.getOidcIdpsHandler = new AuthenticatedRequestHandler({ method: 'GET', url: GET_OIDC_IDPS_URL, timeout: TIMEOUT_DURATION, }, this.accessTokenManager, app.log.bind(app)); this.getTenantBasicConfigHandler = new AuthenticatedRequestHandler({ method: 'GET', url: GET_TENANT_CONFIG_URL, timeout: TIMEOUT_DURATION, }, this.accessTokenManager, app.log.bind(app)); } /** @return A promise that resolves with the GCIP web config. */ getGcipConfig(): Promise<GcipConfig> { return this.app.getProjectId() .then((projectId) => { return this.getGcipConfigHandler.send({ urlParams: { projectId, }, }, DEFAULT_ERROR_GET_GCIP_CONFIG); }) .then((httpResponse) => { const config: Config = typeof httpResponse.body === 'object' ? httpResponse.body : JSON.parse(httpResponse.body); if (!config.client || !config.client.apiKey || !config.client.firebaseSubdomain) { throw new Error(DEFAULT_ERROR_GET_GCIP_CONFIG); } return { apiKey: config.client.apiKey, authDomain: `${config.client.firebaseSubdomain}.firebaseapp.com`, }; }); } /** * Retrieves the TenantUiConfig corresponding to the tenant ID provided. * @param tenantId The corresponding tenant ID whose TenantUiConfig is to be returned. * @return A promise that resolves with the corresponding TenantUiConfig. */ getTenantUiConfig(tenantId: string): Promise<TenantUiConfig> { let tenantConfig: TenantUiConfig; return this.getEnabledPasswordAndPhoneIdps(tenantId) .then((config) => { tenantConfig = config; return this.getEnabledDefaultIdps(tenantId); }) .then((defaultIdps) => { tenantConfig.signInOptions = tenantConfig.signInOptions.concat(defaultIdps); return this.getEnabledSamlIdps(tenantId); }) .then((samlIdps) => { tenantConfig.signInOptions = tenantConfig.signInOptions.concat(samlIdps); return this.getEnabledOidcIdps(tenantId); }) .then((oidcIdps) => { tenantConfig.signInOptions = tenantConfig.signInOptions.concat(oidcIdps); return tenantConfig; }); } /** * Retrieves the password/phone TenantUiConfig corresponding to the tenant ID provided. * @param tenantId The corresponding tenant ID whose TenantUiConfig is to be returned. * @return A promise that resolves with the corresponding TenantUiConfig. */ private getEnabledPasswordAndPhoneIdps(tenantId: string): Promise<TenantUiConfig> { if (tenantId.charAt(0) === '_') { let retrievedProjectId: string; return this.app.getProjectId() .then((projectId) => { retrievedProjectId = projectId; return this.getGcipConfigHandler.send({ urlParams: { projectId, }, }, DEFAULT_ERROR_GET_GCIP_CONFIG); }) .then((httpResponse) => { const config: Config = typeof httpResponse.body === 'object' ? httpResponse.body : JSON.parse(httpResponse.body); const signInOptions: SignInOption[] = []; if (config && config.signIn && config.signIn.email && config.signIn.email.enabled) { signInOptions.push({provider: 'password'}); } if (config && config.signIn && config.signIn.phoneNumber && config.signIn.phoneNumber.enabled) { signInOptions.push({provider: 'phone'}); } return { displayName: retrievedProjectId, signInOptions, }; }); } else { return this.app.getProjectId() .then((projectId) => { return this.getTenantBasicConfigHandler.send({ urlParams: { projectId, tenantId, }, }, DEFAULT_ERROR_GET_TENANT_CONFIG); }) .then((httpResponse) => { const config: Tenant = typeof httpResponse.body === 'object' ? httpResponse.body : JSON.parse(httpResponse.body); const signInOptions: SignInOption[] = []; if (config && config.allowPasswordSignup) { signInOptions.push({provider: 'password'}); } return { displayName: (config && config.displayName) || undefined, signInOptions, }; }); } } /** * Retrieves the default IdP provider IDs corresponding to the tenant ID provided. * @param tenantId The corresponding tenant ID whose default IdP provider IDs are to be returned. * @return A promise that resolves with the corresponding default IdP provider IDs. */ private getEnabledDefaultIdps(tenantId: string): Promise<SignInOption[]> { return this.app.getProjectId() .then((projectId) => { return this.getDefaultIdpsHandler.send({ urlParams: { resourceId: (tenantId.charAt(0) === '_' ? `projects/${projectId}` : `projects/${projectId}/tenants/${tenantId}`), pageSize: PAGE_SIZE.toString(), }, }, DEFAULT_ERROR_GET_DEFAULT_IDPS_CONFIG); }) .then((httpResponse) => { const config: ListDefaultSupportedIdpConfigsResponse = typeof httpResponse.body === 'object' ? httpResponse.body : JSON.parse(httpResponse.body); const delimiter = 'defaultSupportedIdpConfigs/'; const signInOptions: SignInOption[] = []; (config.defaultSupportedIdpConfigs || []).forEach((defaultSupportedIdp: any) => { const name = defaultSupportedIdp && defaultSupportedIdp.name; if (defaultSupportedIdp && defaultSupportedIdp.enabled) { signInOptions.push({ provider: name.substring(name.indexOf(delimiter) + delimiter.length), }); } }); return signInOptions; }); } /** * Retrieves the SAML sign-in options corresponding to the tenant ID provided. * @param tenantId The corresponding tenant ID whose SAML sign-in options are to be returned. * @return A promise that resolves with the corresponding SAML sign-in options. */ private getEnabledSamlIdps(tenantId: string): Promise<SignInOption[]> { return this.app.getProjectId() .then((projectId) => { return this.getSamlIdpsHandler.send({ urlParams: { resourceId: (tenantId.charAt(0) === '_' ? `projects/${projectId}` : `projects/${projectId}/tenants/${tenantId}`), pageSize: PAGE_SIZE.toString(), }, }, DEFAULT_ERROR_GET_SAML_IDPS_CONFIG); }) .then((httpResponse) => { const config: ListInboundSamlConfigsResponse = typeof httpResponse.body === 'object' ? httpResponse.body : JSON.parse(httpResponse.body); const delimiter = 'inboundSamlConfigs/'; const signInOptions: SignInOption[] = []; (config.inboundSamlConfigs || []).forEach((inboundSamlConfig: any) => { if (inboundSamlConfig && inboundSamlConfig.enabled) { const name = inboundSamlConfig.name; signInOptions.push({ provider: name.substring(name.indexOf(delimiter) + delimiter.length), providerName: inboundSamlConfig.displayName, }); } }); return signInOptions; }); } /** * Retrieves the OIDC sign-in options corresponding to the tenant ID provided. * @param tenantId The corresponding tenant ID whose OIDC sign-in options are to be returned. * @return A promise that resolves with the corresponding OIDC sign-in options. */ private getEnabledOidcIdps(tenantId: string): Promise<SignInOption[]> { return this.app.getProjectId() .then((projectId) => { return this.getOidcIdpsHandler.send({ urlParams: { resourceId: (tenantId.charAt(0) === '_' ? `projects/${projectId}` : `projects/${projectId}/tenants/${tenantId}`), pageSize: PAGE_SIZE.toString(), }, }, DEFAULT_ERROR_GET_OIDC_IDPS_CONFIG); }) .then((httpResponse) => { const config: ListOAuthIdpConfigsResponse = typeof httpResponse.body === 'object' ? httpResponse.body : JSON.parse(httpResponse.body); const delimiter = 'oauthIdpConfigs/'; const signInOptions: SignInOption[] = []; (config.oauthIdpConfigs || []).forEach((oauthIdpConfig: any) => { if (oauthIdpConfig && oauthIdpConfig.enabled) { const name = oauthIdpConfig.name; signInOptions.push({ provider: name.substring(name.indexOf(delimiter) + delimiter.length), providerName: oauthIdpConfig.displayName, }); } }); return signInOptions; }); } }