Créer une page de connexion personnalisée

Pour utiliser des identités externes avec Identity Aware Proxy (IAP), votre application doit disposer d'une page de connexion. IAP redirige les utilisateurs vers cette page afin qu'ils puissent s'authentifier et ainsi accéder à des ressources sécurisées.

Cet article explique comment créer entièrement une page d'authentification. Créer vous-même cette page est une tâche complexe, mais cela vous permet de contrôler entièrement le flux d'authentification et l'expérience utilisateur.

Si vous n'avez pas besoin de personnaliser entièrement votre interface utilisateur, vous pouvez envisager de laisser IAP héberger une page de connexion ou d'utiliser FirebaseUI pour simplifier votre code.

Avant de commencer

Activez les identités externes, puis sélectionnez l'option Je fournirai ma propre interface utilisateur lors de la configuration.

Installer la bibliothèque gcip-iap

Le module NPM gcip-iap fait abstraction des communications entre votre application, IAP et Identity Platform.

Nous vous recommandons vivement d'utiliser la bibliothèque. Elle vous permet de personnaliser l'ensemble du processus d'authentification sans vous soucier des échanges sous-jacents entre l'UI et IAP.

Incluez la bibliothèque en tant que dépendance comme suit :

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

Les instructions précédentes concernent la version 0.1.4 de gcip-iap ou une version antérieure.

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

  • Dans votre fichier package.json, mettez à jour la version firebase vers la version 9.6.0+.
  • Mettez à jour les instructions d'importation firebase comme suit.

Aucune modification supplémentaire du code n'est nécessaire:

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

Mettre en œuvre AuthenticationHandler

Le module gcip-iap définit une interface nommée AuthenticationHandler. La bibliothèque appelle automatiquement ses méthodes au moment opportun pour gérer l'authentification. L'interface ressemble à ceci :

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

Pour personnaliser l'UI, vous devez créer une classe personnalisée qui met en œuvre l'interface. Il n'existe aucune restriction concernant le framework JavaScript que vous utilisez. Les sections ci-dessous fournissent des informations supplémentaires sur la façon de créer chaque méthode.

Sélectionner un locataire

Si vous utilisez l'architecture mutualisée, vous devez sélectionner un locataire pour pouvoir authentifier un utilisateur. Si vous n'avez qu'un seul locataire ou si vous utilisez l'authentification au niveau du projet, vous pouvez ignorer cette étape.

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

Notez que les méthodes d'authentification anonyme, personnalisée et par numéro de téléphone ne sont pas compatibles avec l'architecture mutualisée.

Pour sélectionner un locataire, la bibliothèque appelle le rappel selectTenant(). Vous pouvez mettre en œuvre 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.

Pour choisir un locataire par programmation, utilisez le contexte actuel. La classe Authentication contient une méthode getOriginalURL() qui renvoie l'URL à laquelle l'utilisateur accédait avant de devoir s'authentifier. Vous pouvez vous en servir pour localiser 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);
  });
}

Si vous choisissez d'afficher une UI, différentes approches permettent de déterminer le fournisseur à utiliser. Par exemple, vous pouvez afficher une liste de locataires et demander à l'utilisateur d'en choisir un, ou lui demander 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,
          });
        });
  });
}

Quelle que soit votre approche, l'objet SelectedTenantInfo renvoyé sera utilisé ultérieurement 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.

Obtenir l'objet Auth

Une fois que vous avez un fournisseur, vous avez besoin d'un moyen d'obtenir un objet Auth. Mettez en œuvre le rappel getAuth() pour renvoyer une instance firebase.auth.Auth correspondant à la clé API et à l'ID de locataire fournis. Si aucun ID de locataire n'est fourni, des fournisseurs d'identité au niveau du projet doivent être utilisés à la place.

Le rappel getAuth() permet de savoir 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 Auth unique pour chacune d'entre elles. 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() :

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

Procéder à la connexion des utilisateurs

Pour gérer la connexion, mettez en œuvre le rappel startSignIn(). Il doit afficher une UI permettant à l'utilisateur de s'authentifier, puis renvoyer un objet UserCredential pour l'utilisateur connecté une fois l'opération terminée.

Dans un environnement mutualisé, les méthodes d'authentification disponibles peuvent être déterminées à partir de SelectedTenantInfo, si cette variable a été fournie. Elle contient les mêmes informations que celles renvoyées par le rappel selectTenant().

L'exemple suivant montre comment mettre en œuvre startSignIn() pour un utilisateur existant avec une adresse e-mail et un mot de passe :

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

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 :

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

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

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

Pour en savoir plus sur l'authentification des utilisateurs, consultez la documentation Identity Platform.

Traiter un utilisateur

La méthode processUser() facultative permet de modifier un utilisateur connecté avant de le rediriger vers la ressource IAP. Vous pouvez l'utiliser aux fins 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() :

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

Notez que 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 :

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

Afficher une UI de progression

Mettez en œuvre les rappels showProgressBar() et hideProgressBar() facultatifs afin d'afficher une UI de progression personnalisée pour l'utilisateur lorsque le module gcip-iap exécute des tâches réseau de longue durée.

Traiter les erreurs

handleError() est un rappel facultatif pour la gestion des erreurs. Mettez-le en œuvre pour afficher des messages d'erreur pour les utilisateurs ou pour tenter de résoudre certaines erreurs, telles que l'expiration du réseau.

L'exemple suivant montre comment mettre en œuvre 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 générés. Identity Platform peut également générer 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.

Procéder à la déconnexion 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 n'y ait aucune 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, mettez en œuvre le rappel completeSignOut() pour afficher un message indiquant que l'utilisateur s'est bien déconnecté. Si vous ne le faites pas, une page vierge s'affiche.

Utiliser votre UI personnalisée

Une fois que vous avez créé une classe qui met en œuvre 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.

Étape suivante