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. En construisant cette page vous-même, vous contrôlez totalement le flux d'authentification et l'expérience utilisateur.

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

Présentation

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

  1. Activez les identités externes. Sélectionnez Je fournirai ma propre option d'interface utilisateur lors de la configuration.
  2. Installez la bibliothèque gcip-iap.
  3. Configurez l'UI 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 avec des fonctionnalités supplémentaires telles que des barres de progression, des pages de déconnexion et le traitement des utilisateurs.
  5. Testez votre interface utilisateur.

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. Cela vous permet de personnaliser l'intégralité du flux d'authentification sans avoir à gérer les échanges sous-jacents entre l'UI et IAP.

Utilisez les importations appropriées pour 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 pair v9 de firebase ou une version ultérieure. Si vous migrez vers gcip-iap v1.0.0 ou une version ultérieure, procédez comme suit:

  • Mettez à jour la version de firebase dans votre fichier package.json vers la version 9.6.0 (ou une version 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 autre modification de code n'est nécessaire.

gcip-iap v2.0.0

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

  • Mettez à jour la version de firebase dans votre fichier package.json vers la version 9.8.3 (ou une version 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(). Vous pouvez implémenter cette méthode pour choisir un locataire par programmation ou afficher une UI afin que l'utilisateur puisse en sélectionner un lui-même.

Dans les deux cas, la bibliothèque utilise l'objet SelectedTenantInfo renvoyé pour terminer le flux 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 l'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ée et anonyme ne sont pas compatibles avec l'architecture mutualisée.

Sélectionner des locataires par programmation

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

Utilisez ceci pour trouver 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 la liste des 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 disposez d'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 les fournisseurs d'identité au niveau du projet.

getAuth() suit l'emplacement où est stocké l'utilisateur correspondant à 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 UserCredential pour l'utilisateur connecté une fois l'opération terminée.

Dans un environnement mutualisé, vous pouvez déterminer les méthodes d'authentification disponibles à partir de SelectedTenantInfo, si celui-ci a été fourni. Cette variable contient les mêmes informations renvoyées par selectTenant().

L'exemple suivant présente 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.
      });
    });
}

Pour en savoir plus, consultez la section S'authentifier avec l'architecture mutualisée.

Traiter les erreurs

Pour afficher des messages d'erreur auprès des utilisateurs ou pour tenter de résoudre des erreurs telles que l'expiration du délai sur le 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 spécifiques à IAP qui peuvent être renvoyés. Identity Platform peut également renvoyer des erreurs. Consultez la documentation pour 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'UI

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

Afficher une UI de progression

Pour afficher une UI de progression personnalisée chaque fois que le module gcip-iap exécute des tâches réseau de longue durée, implémentez 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.

Une fois qu'un utilisateur se déconnecte, il est possible qu'il n'y ait plus d'URL vers laquelle le rediriger. 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 vierge s'affiche lorsqu'un utilisateur se déconnecte.

Traitement des 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 soient répercutées dans les revendications de jeton d'ID propagées par IAP à 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 l'utiliser 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.

Étapes suivantes