Créer une page de connexion personnalisée

Cet article explique comment créer votre propre page d'authentification à l'aide d'identités externes et d'IAP. Créer cette page vous-même vous donne le contrôle total le flux d'authentification et l'expérience utilisateur.

Si vous n'avez pas besoin de personnaliser entièrement votre interface utilisateur, vous pouvez laisser IAP héberger une page de connexion ou utiliser FirebaseUI pour une expérience plus fluide.

Présentation

Pour créer votre propre page d'authentification, procédez comme suit :

  1. Activez les identités externes. Sélectionnez l'option Je fournirai ma propre interface utilisateur lors de la configuration.
  2. Installez la bibliothèque gcip-iap.
  3. Configurez l'interface utilisateur en implémentant l'interface AuthenticationHandler. Votre page d'authentification doit gérer les scénarios suivants:
    • La sélection de locataires
    • Autorisation des utilisateurs
    • Connexion de l'utilisateur
    • Gestion des exceptions
  4. Facultatif: Personnalisez votre page d'authentification à l'aide de fonctionnalités supplémentaires, telles que des barres de progression, des pages de déconnexion et le traitement des utilisateurs.
  5. Testez votre UI.

Installer la bibliothèque gcip-iap

Pour installer la bibliothèque gcip-iap, exécutez la commande suivante :

npm install gcip-iap --save

Le module NPM gcip-iap fait abstraction des communications entre votre application, IAP et Identity Platform. Vous pouvez ainsi personnaliser processus d'authentification sans avoir à gérer les échanges sous-jacents entre l'UI et IAP.

Utilisez les importations correspondant à la version de votre SDK:

gcip-iap v0.1.4 ou version antérieure

// 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 à v1.1.0

À partir de la version 1.0.0, gcip-iap nécessite la dépendance de paire firebase v9 ou ultérieure. Si vous migrez vers gcip-iap v1.0.0 ou version ultérieure, procédez comme suit : actions:

  • Mettez à jour la version de firebase dans votre fichier package.json vers la version 9.6.0 (ou ultérieure).
  • Mettez à jour les instructions d'importation firebase comme suit :
// Import Firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import the gcip-iap module.
import * as ciap from 'gcip-iap';

Aucune modification de code supplémentaire n'est nécessaire.

gcip-iap v2.0.0

À partir de la version 2.0.0, gcip-iap nécessite la réécriture de votre application d'interface utilisateur personnalisée à l'aide du format SDK modulaire. Si vous migrez vers la version 2.0.0 ou ultérieure de gcip-iap, procédez comme suit :

  • Mettez à jour la version firebase dans votre fichier package.json vers la version 9.8.3 ou ultérieure.
  • Mettez à jour les instructions d'importation firebase comme suit:
  // Import Firebase modules.
  import { initializeApp } from 'firebase/app';
  import { getAuth, GoogleAuthProvider } 'firebase/auth';
  // Import the gcip-iap module.
  import * as ciap from 'gcip-iap';

Configurer l'interface utilisateur

Pour configurer l'UI, créez une classe personnalisée qui implémente l'interface 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;
}

Lors de l'authentification, la bibliothèque appelle automatiquement les méthodes de AuthenticationHandler.

Sélectionner des locataires

Pour sélectionner un locataire, implémentez selectTenant(). Toi pouvez implémenter cette méthode pour choisir un locataire par programmation, ou afficher une UI afin que l'utilisateur puisse en sélectionner une lui-même.

Dans les deux cas, la bibliothèque utilise l'objet SelectedTenantInfo renvoyé pour terminer le processus d'authentification. Il contient l'ID du locataire sélectionné, les éventuels ID de fournisseur et l'e-mail saisi par l'utilisateur.

Si votre projet comporte plusieurs locataires, vous devez en sélectionner un avant de pouvoir authentifier un utilisateur. Si vous n'avez qu'un seul locataire ou si vous utilisez authentification au niveau du projet, vous n'avez pas besoin d'implémenter selectTenant().

IAP est compatible avec les mêmes fournisseurs qu'Identity Platform, par exemple :

  • Adresse e-mail et mot de passe
  • OAuth (Google, Facebook, Twitter, GitHub, Microsoft, etc.)
  • SAML
  • OIDC
  • N° de téléphone
  • Personnalisé
  • Anonyme

Les types d'authentification par numéro de téléphone, personnalisé et anonyme ne sont pas acceptés pour l'architecture mutualisée.

Sélectionner des locataires par programmation

Pour sélectionner un locataire par programmation, utilisez le contexte actuel. La classe Authentication contient getOriginalURL() qui renvoie l'URL à laquelle l'utilisateur accédait avant l'authentification.

Utilisez-le pour recherchez une correspondance dans une liste de locataires associés:

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

Autoriser les utilisateurs à sélectionner des locataires

Pour permettre à l'utilisateur de sélectionner un locataire, affichez une liste de locataires et demandez-lui d'en choisir un, ou demandez-lui de saisir son adresse e-mail, puis de rechercher une correspondance en fonction du domaine :

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

Authentifier les utilisateurs

Une fois que vous avez un fournisseur, implémentez getAuth() pour renvoyer une instance Auth correspondant à la clé API et à l'ID de locataire fournis. Si aucun ID de locataire n'est fourni, utilisez des fournisseurs d'identité au niveau du projet.

getAuth() suit les moments où l'utilisateur correspond à la configuration fournie. Il permet également d'actualiser silencieusement un jeton d'ID Identity Platform d'un utilisateur précédemment authentifié, sans que celui-ci doive saisir de nouveau ses identifiants.

Si vous utilisez plusieurs ressources IAP avec différents locataires, nous vous recommandons d'utiliser une instance d'authentification unique pour chaque ressource. Cela permet à plusieurs ressources ayant des configurations différentes d'utiliser la même page d'authentification. Cela permet également à plusieurs utilisateurs de se connecter en même temps sans déconnecter l'utilisateur précédent.

Vous trouverez ci-dessous un exemple de mise en œuvre de 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;
}

Procéder à la connexion des utilisateurs

Pour gérer la connexion, implémentez startSignIn(), affichez une UI permettant à l'utilisateur de s'authentifier, puis renvoyez un objet UserCredential pour l'utilisateur connecté une fois l'opération terminée.

Dans un environnement multi-tenant, vous pouvez déterminer les méthodes d'authentification disponibles à partir de SelectedTenantInfo, si cette variable a été fournie. Cette variable contient les mêmes informations que celles renvoyées par selectTenant().

L'exemple suivant montre une implémentation de startSignIn() pour un utilisateur existant avec une adresse e-mail et un mot de passe :

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

Vous pouvez également connecter des utilisateurs avec un fournisseur fédéré, comme SAML ou OIDC, à l'aide d'un pop-up ou d'une redirection :

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

Certains fournisseurs OAuth acceptent la transmission d'un indice de connexion pour la connexion :

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

Consultez la section S'authentifier avec l'architecture mutualisée. pour en savoir plus.

Traiter les erreurs

Pour afficher des messages d'erreur aux utilisateurs ou pour tenter de résoudre des erreurs telles que les expirations réseau, implémentez handleError().

L'exemple suivant implémente 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;
  });
}

Le tableau ci-dessous répertorie les codes d'erreur propres à IAP qui peuvent être renvoyés. Identity Platform peut également renvoyer des erreurs. Consultez la documentation concernant firebase.auth.Auth.

Code d'erreur Description
invalid-argument Le client a spécifié un argument incorrect.
failed-precondition La requête ne peut pas être exécutée dans l'état actuel du système.
out-of-range Le client a spécifié une plage non valide.
unauthenticated La requête n'a pas été authentifiée en raison d'un jeton OAuth manquant, non valide ou ayant expiré.
permission-denied Le client ne dispose pas d'une autorisation suffisante, ou l'UI est hébergée sur un domaine non autorisé.
not-found . Ressource indiquée introuvable.
aborted Un conflit de simultanéité existe, tel qu'un conflit lecture-modification-écriture.
already-exists La ressource qu'un client a essayé de créer existe déjà.
resource-exhausted Le quota de ressources est dépassé ou la limite du débit est atteinte.
cancelled La demande a été annulée par le client.
data-loss Perte de données irrécupérable ou corruption de données.
unknown Erreur du serveur inconnue.
internal Erreur interne du serveur.
not-implemented Méthode d'API non mise en œuvre par le serveur.
unavailable Service indisponible.
restart-process Accédez de nouveau à l'URL qui vous a redirigé vers cette page pour relancer le processus d'authentification.
deadline-exceeded Délai de requête dépassé.
authentication-uri-fail Échec de la génération de l'URI d'authentification.
gcip-token-invalid Le jeton d'ID GCIP fourni n'est pas valide.
gcip-redirect-invalid L'URL de redirection n'est pas valide.
get-project-mapping-fail Échec de l'obtention de l'ID du projet.
gcip-id-token-encryption-error Erreur de chiffrement du jeton d'ID GCIP.
gcip-id-token-decryption-error Erreur de déchiffrement du jeton d'ID GCIP.
gcip-id-token-unescape-error Échec de la suppression de l'échappement en base64 adapté au Web.
resource-missing-gcip-sign-in-url URL d'authentification GCIP manquante pour la ressource IAP spécifiée.

Personnaliser l'interface utilisateur

Vous pouvez personnaliser votre page d'authentification avec des fonctionnalités facultatives telles que des barres de progression et des pages de déconnexion.

Afficher une UI de progression

Pour afficher une interface utilisateur de progression personnalisée chaque fois que le module gcip-iap exécute des tâches réseau de longue durée : implémenter showProgressBar() et hideProgressBar().

Déconnecter des utilisateurs

Dans certains cas, vous pouvez autoriser les utilisateurs à se déconnecter de toutes les sessions en cours qui partagent la même URL d'authentification.

Lorsqu'un utilisateur se déconnecte, il est possible qu'il ne dispose plus d'URL vers laquelle il sera redirigé. Cela se produit généralement lorsqu'un utilisateur se déconnecte de tous les locataires associés à une page de connexion. Dans ce cas, implémentez completeSignOut() pour afficher un message indiquant que l'utilisateur s'est bien déconnecté. Si vous n'implémentez pas cette méthode, une page vide s'affiche lorsqu'un utilisateur se déconnecte.

Traiter les utilisateurs

Pour modifier un utilisateur connecté avant de le rediriger vers la ressource IAP, implémentez processUser().

Cette méthode vous permet d'effectuer les opérations suivantes:

  • Établir une association à des fournisseurs supplémentaires
  • Mettre à jour le profil de l'utilisateur
  • Demander à l'utilisateur de fournir des données supplémentaires après l'inscription
  • Traiter les jetons d'accès OAuth renvoyés par getRedirectResult() après avoir appelé signInWithRedirect()

Voici un exemple de mise en œuvre 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 vous souhaitez que les modifications apportées à un utilisateur apparaissent dans les revendications de jeton d'ID propagées par IAP vers votre application, vous devez forcer l'actualisation du jeton :

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

Tester l'interface utilisateur

Après avoir créé une classe qui implémente AuthenticationHandler, vous pouvez utilisez-la pour créer une instance Authentication et la démarrer:

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

Déployez votre application et accédez à la page d'authentification. Votre UI de connexion personnalisée doit s'afficher.

Étape suivante