Crea una página de acceso personalizada

En este artículo, se muestra cómo crear tu propia página de autenticación con identidades externas y IAP. Si creas esta página por tu cuenta, tendrás control total sobre el flujo de autenticación y la experiencia del usuario.

Si no necesitas personalizar completamente tu IU, puedes permitir que IAP aloje una página de acceso o usar FirebaseUI para obtener una experiencia más optimizada.

Descripción general

Para crear tu propia página de autenticación, sigue estos pasos:

  1. Habilita identidades externas. Selecciona la opción Proporcionaré mi propia IU durante la configuración.
  2. Instala la biblioteca gcip-iap.
  3. Implementa la interfaz AuthenticationHandler para configurar la IU. Tu página de autenticación debe controlar las siguientes situaciones:
    • Selección de usuarios
    • Autorización de usuarios
    • Acceso del usuario
    • Manejo de errores
  4. Opcional: Personaliza tu página de autenticación con funciones adicionales, como barras de progreso, páginas de salida y procesamiento de usuarios.
  5. Prueba tu IU.

Instala la biblioteca gcip-iap

Para instalar la biblioteca gcip-iap, ejecuta el siguiente comando:

npm install gcip-iap --save

El módulo de Administración de socios de red gcip-iap simplifica las comunicaciones entre tu aplicación, IAP y Identity Platform. ya que esta te permite personalizar todo el flujo de autenticación sin tener que administrar los intercambios subyacentes entre la IU y el IAP.

Usa las importaciones correctas para tu versión del SDK:

gcip-iap v0.1.4 o versiones anteriores

// Import Firebase/GCIP dependencies. These are installed on npm install.
import * as firebase from 'firebase/app';
import 'firebase/auth';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';

De gcip-iap v1.0.0 a v1.1.0

A partir de la versión 1.0.0, gcip-iap requiere la dependencia de pares firebase v9 o una posterior. Si migras a gcip-iap v1.0.0 o una versión posterior, completa las siguientes acciones:

  • Actualiza la versión de firebase en tu archivo package.json a la v9.6.0 o una posterior.
  • Actualiza las declaraciones de importación de firebase de la siguiente manera:
// Import Firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import the gcip-iap module.
import * as ciap from 'gcip-iap';

No es necesario realizar cambios adicionales en el código.

gcip-iap v2.0.0

A partir de la versión 2.0.0, gcip-iap requiere que vuelvas a escribir tu aplicación de IU personalizada con el formato de SDK modular. Si migras a gcip-iap v2.0.0 o una versión posterior, completa las siguientes acciones:

  • Actualiza la versión de firebase en tu archivo package.json a la v9.8.3 o una posterior.
  • Actualiza las declaraciones de importación de firebase de la siguiente manera:
  // Import Firebase modules.
  import { initializeApp } from 'firebase/app';
  import { getAuth, GoogleAuthProvider } 'firebase/auth';
  // Import the gcip-iap module.
  import * as ciap from 'gcip-iap';

Cómo configurar la IU

Para configurar la IU, crea una clase personalizada que implemente la interfaz AuthenticationHandler:

interface AuthenticationHandler {
  languageCode?: string | null;
  getAuth(apiKey: string, tenantId: string | null): FirebaseAuth;
  startSignIn(auth: FirebaseAuth, match?: SelectedTenantInfo): Promise<UserCredential>;
  selectTenant?(projectConfig: ProjectConfig, tenantIds: string[]): Promise<SelectedTenantInfo>;
  completeSignOut(): Promise<void>;
  processUser?(user: User): Promise<User>;
  showProgressBar?(): void;
  hideProgressBar?(): void;
  handleError?(error: Error | CIAPError): void;
}

Durante la autenticación, la biblioteca llama automáticamente a los métodos de AuthenticationHandler.

Cómo seleccionar usuarios

Para seleccionar un usuario, implementa selectTenant(). Puedes implementar este método para elegir un usuario de manera programática o mostrar una IU para que el usuario pueda seleccionarlo.

En cualquier caso, la biblioteca usa el objeto SelectedTenantInfo que se muestra para completar el flujo de autenticación. Contiene el ID del usuario seleccionado, los ID de cualquier proveedor y el correo electrónico que ingresó el usuario.

Si tienes varios inquilinos en tu proyecto, debes seleccionar uno antes de poder autenticar a un usuario. Si solo tienes un único usuario o utilizas la autenticación a nivel de proyecto, no es necesario que implementes selectTenant().

IAP es compatible con los mismos proveedores que Identity Platform, por ejemplo:

  • Correo electrónico y contraseña
  • OAuth (Google, Facebook, Twitter, GitHub, Microsoft, etcétera)
  • SAML
  • OIDC
  • Número de teléfono
  • Personalizados
  • Anónimo

Los tipos de autenticación personalizada, anónima y por número de teléfono no son compatibles con los multiusuarios.

Cómo seleccionar usuarios de manera programática

Para seleccionar un usuario de manera programática, aprovecha el contexto actual. La clase Authentication contiene getOriginalURL(), que muestra la URL a la que el usuario estaba accediendo antes de la autenticación.

Usa esta opción para encontrar una coincidencia de una lista de usuarios asociados:

// Select provider programmatically.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    auth.getOriginalURL()
      .then((originalUrl) => {
        resolve({
          tenantId: getMatchingTenantBasedOnVisitedUrl(originalUrl),
          // If associated provider IDs can also be determined,
          // populate this list.
          providerIds: [],
        });
      })
      .catch(reject);
  });
}

Permite que los usuarios seleccionen inquilinos

Para permitir que el usuario seleccione un usuario, muestra una lista de usuarios y haz que el usuario elija uno, o pídele que ingrese su dirección de correo electrónico y, luego, busque una coincidencia basada en el dominio:

// Select provider by showing UI.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    renderSelectTenant(
        tenantIds,
        // On tenant selection.
        (selectedTenantId) => {
          resolve({
            tenantId: selectedTenantId,
            // If associated provider IDs can also be determined,
            // populate this list.
            providerIds: [],
            // If email is available, populate this field too.
            email: undefined,
          });
        });
  });
}

Cómo autenticar usuarios

Una vez que tengas un proveedor, implementa getAuth() para mostrar una instancia de Auth, que corresponde a la clave de API y al ID de usuario proporcionado. Si no se proporciona un ID de usuario, usa proveedores de identidad a nivel del proyecto.

getAuth() realiza un seguimiento de dónde se almacena el usuario correspondiente a la configuración proporcionada. También permite actualizar de forma silenciosa un token de ID de Identity Platform de un usuario autenticado anteriormente sin necesidad de que el usuario vuelva a ingresar sus credenciales.

Si usas varios recursos de IAP con distintos usuarios, te recomendamos que uses una instancia de autenticación única para cada recurso. Esto permite que varios recursos con diferentes configuraciones usen la misma página de autenticación. También permite que varios usuarios accedan al mismo tiempo sin cerrar la sesión del usuario anterior.

El siguiente es un ejemplo de cómo implementar getAuth():

gcip-iap v1.0.0

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = firebase.app(tenantId || undefined).auth();
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = firebase.initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = app.auth();
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

gcip-iap v2.0.0

import {initializeApp, getApp} from 'firebase/app';
import {getAuth} from 'firebase/auth';

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = getAuth(getApp(tenantId || undefined));
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = getAuth(app);
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

Permite que los usuarios accedan

Para controlar el acceso, implementa startSignIn(), muestra una IU para que el usuario se autentique y, una vez completado el proceso, devuelve una UserCredential para el usuario que accedió.

En un entorno de multiusuarios, puedes determinar los métodos de autenticación disponibles desde SelectedTenantInfo, si se proporcionó. Esta variable contiene la misma información que muestra selectTenant().

En el siguiente ejemplo, se muestra una implementación de startSignIn() para un usuario existente con un correo electrónico y una contraseña:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
      auth.signInWithEmailAndPassword(email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

gcip-iap v2.0.0

import {signInWithEmailAndPassword} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
        signInWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

También puedes acceder a los usuarios con un proveedor federado, como OIDC o SAML, mediante una ventana emergente o un redireccionamiento:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in may have already been determined from the
    // selectedTenantInfo object.
    const provider = new firebase.auth.SAMLAuthProvider('saml.myProvider');
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    auth.signInWithRedirect(provider);
  });
}

gcip-iap v2.0.0

import {signInWithPopup, SAMLAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in might have already been determined from the
    // selectedTenantInfo object.
    const provider = new SAMLAuthProvider('saml.myProvider');
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    signInWithRedirect(auth, provider);
  });
}

Algunos proveedores de OAuth admiten la aprobación de una sugerencia de acceso para acceder:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new firebase.auth.OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

gcip-iap v2.0.0

import {signInWithPopup, OAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign in or sign up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

Consulta Autentica con multitenancy para obtener más información.

Maneja los errores

Para mostrar mensajes de error a los usuarios o para intentar recuperarse de errores, como los tiempos de espera de la red, implementa handleError().

En el siguiente ejemplo, se implementa handleError():

handleError(error) {
  showAlert({
    code: error.code,
    message: error.message,
    // Whether to show the retry button. This is only available if the error is
    // recoverable via retrial.
    retry: !!error.retry,
  });
  // When user clicks retry, call error.retry();
  $('.alert-link').on('click', (e) => {
    error.retry();
    e.preventDefault();
    return false;
  });
}

En la siguiente tabla, se enumeran los códigos de error específicos de IAP que se pueden mostrar. Identity Platform también puede mostrar errores. Consulta la documentación de firebase.auth.Auth.

Código de error Descripción
invalid-argument El cliente especificó un argumento no válido.
failed-precondition La solicitud no se puede ejecutar en el estado actual del sistema.
out-of-range El cliente especificó un rango no válido.
unauthenticated La solicitud no se autenticó debido a que falta un token de OAuth, no es válido o venció.
permission-denied El cliente no tiene permiso suficiente o la IU está alojada en un dominio no autorizado.
not-found No se encontró el recurso especificado.
aborted Conflicto de concurrencia, como conflicto de lectura-modificación-escritura.
already-exists El recurso que el cliente intentó crear ya existe.
resource-exhausted Sin cuota de recursos o a punto de alcanzar el límite de frecuencia.
cancelled El cliente canceló la solicitud.
data-loss Daño o pérdida de datos no recuperable.
unknown Error de servidor desconocido.
internal Error interno del servidor
not-implemented El servidor no implementó el método de la API.
unavailable Servicio no disponible
restart-process Revisa la URL que te redireccionó a esta página para reiniciar el proceso de autenticación.
deadline-exceeded Se excedió el plazo de la solicitud.
authentication-uri-fail No se pudo generar el URI de autenticación.
gcip-token-invalid Se proporcionó un token de ID de GCIP no válido.
gcip-redirect-invalid URL de redireccionamiento no válida
get-project-mapping-fail No se pudo obtener el ID del proyecto.
gcip-id-token-encryption-error Error de encriptación del token de ID de GCIP.
gcip-id-token-decryption-error Error de desencriptación del token de ID de GCIP.
gcip-id-token-unescape-error Se produjo un error sin escape de base64 seguro para la Web.
resource-missing-gcip-sign-in-url Falta la URL de autenticación GCIP para el recurso IAP especificado.

Personaliza la IU

Puedes personalizar tu página de autenticación con funciones opcionales, como barras de progreso y páginas de salida.

Muestra una IU de progreso

Para mostrar una IU de progreso personalizada al usuario cada vez que el módulo gcip-iap ejecute tareas de red de larga duración, implementa showProgressBar() y hideProgressBar().

Cierre de sesión de los usuarios

En algunos casos, es posible que desees permitir que los usuarios salgan de todas las sesiones actuales que comparten la misma URL de autenticación.

Después de que un usuario sale, es posible que no haya una URL a la cual redirigirlos. Esto suele ocurrir cuando un usuario sale de todas las instancias asociadas con una página de acceso. En este caso, implementa completeSignOut() para mostrar un mensaje que indique que el usuario salió correctamente. Si no implementas este método, aparecerá una página en blanco cuando un usuario salga de su cuenta.

Procesamiento de usuarios

Para modificar un usuario que haya accedido antes de redireccionarlo al recurso de IAP, implementa processUser().

Puedes usar este método para hacer lo siguiente:

  • Vincular a proveedores adicionales
  • Actualizar el perfil del usuario
  • Solicitar al usuario datos adicionales después del registro
  • Procesar tokens de acceso OAuth que muestra getRedirectResult() después de llamar a signInWithRedirect().

El siguiente es un ejemplo de implementación de processUser():

gcip-iap v1.0.0

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

gcip-iap v2.0.0

import {getRedirectResult} from 'firebase/auth';

processUser(user) {
  return getRedirectResult(lastAuthUsed).then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

Si quieres que los cambios a un usuario se vean reflejados en las reclamaciones de token de ID que IAP propaga a tu app, debes forzar su actualización:

gcip-iap v1.0.0

processUser(user) {
  return user.updateProfile({
    photoURL: 'https://example.com/profile/1234/photo.png',
  }).then(function() {
    // To reflect updated photoURL in the ID token, force token
    // refresh.
    return user.getIdToken(true);
  }).then(function() {
    return user;
  });
}

gcip-iap v2.0.0

import {updateProfile} from 'firebase/auth';

processUser(user) {
  return updateProfile(user, {
    photoURL: 'https://example.com/profile/1234/photo.png',
  }).then(function() {
    // To reflect updated photoURL in the ID token, force token
    // refresh.
    return user.getIdToken(true);
  }).then(function() {
    return user;
  });
}

Cómo probar la IU

Después de crear una clase que implemente AuthenticationHandler, puedes usarla para crear una instancia Authentication nueva y, luego, iniciarla:

// Implement interface AuthenticationHandler.
// const authHandlerImplementation = ....
const ciapInstance = new ciap.Authentication(authHandlerImplementation);
ciapInstance.start();

Implementa tu aplicación y navega a la página de autenticación. Deberías ver tu IU de acceso personalizada.

¿Qué sigue?