Crea una página de acceso personalizada

Tu app necesita una página de acceso para usar identidades externas con Identity-Aware Proxy (IAP). IAP redireccionará a los usuarios a esta página para que se autentiquen antes de que puedan acceder a los recursos seguros.

En este artículo, se explica cómo compilar una página de autenticación desde cero. Crear esta página tú mismo es complejo, pero te brinda un control total sobre el flujo de autenticación y la experiencia del usuario.

Si no necesitas personalizar completamente tu IU, considera permitir que IAP aloje una página de acceso o usa FirebaseUI para simplificar tu código.

Antes de comenzar

Habilita identidades externas y selecciona la opción Proporcionaré mi propia IU durante la configuración.

Instala la biblioteca gcip-iap

El módulo de Administración de socios de red gcip-iap simplifica las comunicaciones entre tu aplicación, IAP y Identity Platform.

Se recomienda usar la biblioteca, ya que esta te permite personalizar todo el flujo de autenticación sin preocuparte por los intercambios subyacentes entre la IU y el IAP.

Incluye la biblioteca como una dependencia como se muestra a continuación:

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

Las instrucciones anteriores se aplican a gcip-iap v0.1.4 o menos.

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

  • Actualiza la versión firebase del archivo package.json a la versión 9.6.0.
  • Actualiza las declaraciones de importación firebase de la siguiente manera.

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

// Import firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import gcip-iap module.
import * as ciap from 'gcip-iap';

Implementa AuthenticationHandler

El módulo gcip-iap define una interfaz llamada AuthenticationHandler, y la biblioteca llama automáticamente a sus métodos en el momento adecuado para manejar la autenticación. La interfaz se ve así:

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

Para personalizar la IU, debes crear una clase personalizada que implemente la interfaz. No hay restricciones sobre el marco de trabajo de JavaScript que usas. Las siguientes secciones proporcionan información adicional sobre cómo compilar cada método.

Selecciona un usuario

Si usas multiusuarios, debes seleccionar uno específico antes de poder autenticar a un usuario del grupo. Si solo tienes un único usuario o utilizas la autenticación a nivel de proyecto, puedes omitir este paso.

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ónima

Ten en cuenta que la autenticación personalizada, anónima y por número de teléfono no son compatibles con los multiusuarios.

Para seleccionar un usuario, la biblioteca invoca la devolución de llamada selectTenant(). Puedes implementar este método para elegir un usuario de manera programática o mostrar una IU para que el usuario pueda seleccionarlo.

Para elegir un usuario de manera programática, aprovecha el contexto actual. La clase Authentication contiene un método getOriginalURL() que muestra la URL a la que el usuario estaba accediendo antes de necesitar la autenticación. Puedes utilizar 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);
  });
}

Si decides mostrar una IU, existen diversos enfoques para determinar qué proveedor usar. Por ejemplo, puedes mostrar una lista de usuarios y hacer que el usuario elija uno, o puedes pedirle 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,
          });
        });
  });
}

Independientemente de tu enfoque, el objeto SelectedTenantInfo que se muestre se usará más adelante 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.

Obtén el objeto Auth

Una vez que tengas un proveedor, necesitas una forma de obtener un objeto Auth. Implementa la devolución de llamada getAuth() para mostrar una instancia firebase.auth.Auth correspondiente a la clave de API y al ID de usuario proporcionado. Si no se proporciona un ID de usuario, entonces debes usar Proveedores de identidad a nivel de proyecto.

getAuth() se usa para realizar un seguimiento de dónde se almacena el usuario específico que corresponde 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 Auth única para cada una. 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():

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. This makes it possible to have
    // multiple users 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;
}

Permite que los usuarios accedan

Para administrar el acceso, implementa la devolución de llamada startSignIn(). Debería mostrar 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, los métodos de autenticación disponibles se pueden determinar desde SelectedTenantInfo, si se proporcionó. Esta variable contiene la misma información que muestra la devolución de llamada selectTenant().

En el siguiente ejemplo, se muestra cómo podrías implementar startSignIn() para un usuario existente con un correo electrónico y una contraseña:

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show 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 user for email and password.
      // 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 error message.
        });
    });
  });
}

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

startSignIn(auth, selectedTenantInfo) {
  // Show UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // 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 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);
  });
}

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

startSignIn(auth, selectedTenantInfo) {
  // Show 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.
    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 error message.
       });
    });
}

Consulta la documentación de Identity Platform para obtener más información sobre la autenticación de usuarios.

Procesa un usuario

El método processUser() opcional te permite modificar un usuario que haya accedido antes de volver a redireccionar al recurso de IAP. Puedes usar este método para realizar las siguientes tareas:

  • 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():

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask 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;
  });
}

Ten en cuenta que si quieres que los cambios a un usuario se vean reflejados en las reclamaciones de token de ID que IAP propaga a tu app, deberás forzar su actualización:

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

Muestra una IU de progreso

Implementa las devoluciones de llamada opcionales showProgressBar() y hideProgressBar() para mostrar una IU de progreso personalizada al usuario siempre que el módulo gcip-iap ejecute tareas de red de larga duración.

Maneja los errores

handleError() es una devolución de llamada opcional para el manejo de errores. Impleméntala con el fin de mostrar mensajes de error a los usuarios o para intentar recuperarse de ciertos errores (como el tiempo de espera de la red).

En el siguiente ejemplo, se muestra cómo implementar 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 generar. Identity Platform también puede generar 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.

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 (normalmente ocurre cuando un usuario sale de todas las instancias asociadas con una página de acceso). En este caso, implementa la devolución de llamada completeSignOut() para mostrar un mensaje que indique que el usuario salió correctamente. De lo contrario, aparecerá una página en blanco.

Usa tu IU personalizada

Una vez que hayas creado 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?