Crear una página de inicio de sesión personalizada

En este artículo se explica cómo crear tu propia página de autenticación con identidades externas e IAP. Si creas esta página tú mismo, tendrás pleno control sobre el flujo de autenticación y la experiencia de usuario.

Si no necesitas personalizar completamente tu interfaz de usuario, puedes dejar que IAP aloje una página de inicio de sesión por ti o usar FirebaseUI para disfrutar de una experiencia más optimizada.

Información general

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

  1. Habilita las identidades externas. Selecciona Proporcionaré mi propia opción de interfaz de usuario durante la configuración.
  2. Instala la biblioteca gcip-iap.
  3. Configura la interfaz de usuario implementando la interfaz AuthenticationHandler. Tu página de autenticación debe gestionar las siguientes situaciones:
    • Selección de inquilinos
    • Autorización de usuario
    • Inicio de sesión de usuario
    • Gestión de errores
  4. Opcional: Personaliza tu página de autenticación con funciones adicionales, como barras de progreso, páginas de cierre de sesión y procesamiento de usuarios.
  5. Prueba tu interfaz de usuario.

Instalar la biblioteca gcip-iap

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

npm install gcip-iap --save

El módulo gcip-iap NPM abstrae las comunicaciones entre tu aplicación, las compras en la aplicación y Identity Platform. De esta forma, puedes personalizar todo el flujo de autenticación sin tener que gestionar los intercambios subyacentes entre la interfaz de usuario y las compras en la aplicación.

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';

gcip-iap v1.0.0 a v1.1.0

A partir de la versión v1.0.0, gcip-iap requiere la dependencia de elemento del mismo nivel firebase v9 o una versión posterior. Si vas a migrar a la versión 1.0.0 de gcip-iap o a una posterior, completa las siguientes acciones:

  • Actualiza la versión de firebase en tu archivo package.json a la 9.6.0 o una posterior.
  • Actualiza las instrucciones 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 se vuelva a escribir tu aplicación de interfaz de usuario personalizada con el formato de SDK modular. Si vas a migrar a gcip-iap v2.0.0 o una versión posterior, sigue estos pasos:

  • Actualiza la versión de firebase en tu archivo package.json a la versión 9.8.3 o posterior.
  • Actualiza las instrucciones 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';

Configurar la interfaz de usuario

Para configurar la interfaz de usuario, 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.

Seleccionar clientes

Para seleccionar un arrendatario, implementa selectTenant(). Puedes implementar este método para elegir un arrendatario mediante programación o mostrar una interfaz de usuario para que el usuario pueda seleccionar uno.

En ambos casos, la biblioteca usa el objeto SelectedTenantInfo devuelto para completar el flujo de autenticación. Contiene el ID del arrendatario seleccionado, los IDs de los proveedores y el correo que ha introducido el usuario.

Si tienes varios inquilinos en tu proyecto, debes seleccionar uno antes de poder autenticar a un usuario. Si solo tienes un inquilino o usas la autenticación a nivel de proyecto, no tienes que implementar selectTenant().

IAP admite los mismos proveedores que Identity Platform, como los siguientes:

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

No se admiten los tipos de autenticación de número de teléfono, personalizada y anónima para la arquitectura multiempresa.

Seleccionar propietarios de forma programática

Para seleccionar un arrendatario de forma programática, aprovecha el contexto actual. La clase Authentication contiene getOriginalURL(), que devuelve la URL a la que accedía el usuario antes de la autenticación.

Úsalo para buscar una coincidencia en una lista de clientes 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);
  });
}

Permitir que los usuarios seleccionen clientes

Para permitir que el usuario seleccione un arrendatario, muestra una lista de arrendatarios y pide al usuario que elija uno o que introduzca su dirección de correo electrónico y, a continuación, busca 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,
          });
        });
  });
}

Autenticar usuarios

Una vez que tengas un proveedor, implementa getAuth() para devolver una instancia de Auth, correspondiente a la clave de API y al ID de arrendatario proporcionados. Si no se proporciona ningún ID de cliente, se usan los proveedores de identidades a nivel de proyecto.

getAuth() registra dónde se almacena el usuario correspondiente a la configuración proporcionada. También permite actualizar de forma silenciosa el token de ID de Identity Platform de un usuario autenticado previamente sin que tenga que volver a introducir sus credenciales.

Si usas varios recursos de IAP con diferentes arrendatarios, te recomendamos que uses una instancia de autenticación única para cada recurso. De esta forma, varios recursos con configuraciones diferentes pueden usar la misma página de autenticación. También permite que varios usuarios inicien sesión al mismo tiempo sin cerrar la sesión del usuario anterior.

A continuación, se muestra 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;
}

Inicio de sesión de usuarios

Para gestionar el inicio de sesión, implementa startSignIn(), muestra una interfaz de usuario para que el usuario se autentique y, a continuación, devuelve un UserCredential para el usuario que ha iniciado sesión cuando se complete el proceso.

En un entorno multiempresa, puedes determinar los métodos de autenticación disponibles a partir de SelectedTenantInfo, si se ha proporcionado. Esta variable contiene la misma información que devuelve selectTenant().

En el siguiente ejemplo se muestra una implementación de startSignIn() para un usuario que ya tiene una dirección de correo 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 iniciar la sesión de los usuarios con un proveedor federado, como SAML u OIDC, mediante una ventana emergente o una redirección:

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 el envío de una sugerencia de inicio de sesión para iniciar sesión:

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 más información sobre la autenticación con multitenencia.

Gestionar errores

Para mostrar mensajes de error a los usuarios o 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 tabla que se muestra más abajo se enumeran los códigos de error específicos de las compras en aplicaciones que se pueden devolver. Identity Platform también puede devolver errores. Consulta la documentación de firebase.auth.Auth.

Código de error Descripción
invalid-argument El cliente especificó un argumento que no es válido.
failed-precondition No se puede realizar la solicitud debido al estado actual del sistema.
out-of-range El cliente especificó un intervalo que no es válido.
unauthenticated La solicitud no se ha autenticado porque el token de OAuth ha caducado, no es válido o no se ha encontrado.
permission-denied El cliente no tiene suficientes permisos o la interfaz de usuario está alojada en un dominio no autorizado.
not-found . No se ha encontrado el recurso especificado.
aborted Se ha producido un conflicto de simultaneidad (por ejemplo, relacionado con acciones de lectura, modificación y escritura).
already-exists El cliente ha intentado crear un recurso que ya existe.
resource-exhausted Se ha agotado la cuota del recurso o se ha alcanzado el límite de frecuencia.
cancelled El cliente ha cancelado la solicitud.
data-loss Se han perdido o dañado los datos de forma irrecuperable.
unknown Se ha producido un error de servidor desconocido.
internal Error de servidor interno.
not-implemented El servidor no ha implementado el método de API.
unavailable Servicio no disponible.
restart-process Vuelve a visitar la URL que te redirigió a esta página para reiniciar el proceso de autenticación.
deadline-exceeded Se ha superado el plazo límite de la solicitud.
authentication-uri-fail No se ha podido generar el URI de autenticación.
gcip-token-invalid Se ha proporcionado un token de ID de GCIP no válido.
gcip-redirect-invalid URL de redirección no válida.
get-project-mapping-fail No se ha podido obtener el ID del proyecto.
gcip-id-token-encryption-error Error de cifrado del token de ID de GCIP.
gcip-id-token-decryption-error Error al descifrar el token de ID de GCIP.
gcip-id-token-unescape-error No se ha podido descodificar la cadena base64 segura para la Web.
resource-missing-gcip-sign-in-url Falta la URL de autenticación de GCIP del recurso de IAP especificado.

Personalizar la interfaz de usuario

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

Mostrar una interfaz de usuario de progreso

Para mostrar una interfaz de usuario 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().

Cerrar la sesión de los usuarios

En algunos casos, puede que quieras permitir que los usuarios cierren sesión en todas las sesiones actuales que compartan la misma URL de autenticación.

Después de que un usuario cierre sesión, puede que no haya ninguna URL a la que redirigirlo. Esto suele ocurrir cuando un usuario cierra sesión en todos los tenants asociados a una página de inicio de sesión. En este caso, implementa completeSignOut() para mostrar un mensaje que indique que el usuario ha cerrado sesión correctamente. Si no implementas este método, se mostrará una página en blanco cuando un usuario cierre sesión.

Procesamiento de usuarios

Para modificar un usuario que ha iniciado sesión antes de redirigirlo al recurso de IAP, implementa processUser().

Puedes usar este método para hacer lo siguiente:

  • Enlace a proveedores adicionales.
  • Actualiza el perfil del usuario.
  • Pedir datos adicionales al usuario después del registro.
  • Procesa los tokens de acceso de OAuth devueltos por getRedirectResult() después de llamar a signInWithRedirect().

A continuación, se muestra 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 que hagas en un usuario se reflejen en las reclamaciones del token de ID que IAP propaga a tu aplicación, debes forzar la actualización del token:

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;
  });
}

Probar la interfaz de usuario

Una vez que hayas creado una clase que implemente AuthenticationHandler, podrás usarla para crear una instancia de Authentication e iniciarla:

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

Implementa tu aplicación y ve a la página de autenticación. Deberías ver tu interfaz de inicio de sesión personalizada.

Siguientes pasos