Étendre l'authentification Firebase avec des fonctions de blocage


Les fonctions de blocage vous permettent d'exécuter du code personnalisé qui modifie le résultat de l'inscription ou de la connexion d'un utilisateur à votre application. Par exemple, vous pouvez empêcher un utilisateur de s'authentifier s'il ne répond pas à certains critères, ou mettre à jour les informations d'un utilisateur avant de les renvoyer à votre application client.

Avant que tu commences

Pour utiliser les fonctions de blocage, vous devez mettre à niveau votre projet Firebase vers Firebase Authentication avec Identity Platform. Si vous n'avez pas encore effectué la mise à niveau, faites-le d'abord.

Comprendre les fonctions de blocage

Vous pouvez enregistrer des fonctions de blocage pour deux événements :

  • beforeCreate : se déclenche avant qu'un nouvel utilisateur ne soit enregistré dans la base de données d'authentification Firebase et avant qu'un jeton ne soit renvoyé à votre application client.

  • beforeSignIn : se déclenche après la vérification des informations d'identification d'un utilisateur, mais avant que l'authentification Firebase ne renvoie un jeton d'identification à votre application client. Si votre application utilise l'authentification multifacteur, la fonction se déclenche une fois que l'utilisateur a vérifié son deuxième facteur. Notez que la création d'un nouvel utilisateur déclenche également beforeSignIn , en plus de beforeCreate .

Gardez les points suivants à l’esprit lorsque vous utilisez les fonctions de blocage :

  • Votre fonction doit répondre dans les 7 secondes. Après 7 secondes, l'authentification Firebase renvoie une erreur et l'opération client échoue.

  • Les codes de réponse HTTP autres que 200 sont transmis à vos applications clientes. Assurez-vous que votre code client gère toutes les erreurs que votre fonction peut renvoyer.

  • Les fonctions s'appliquent à tous les utilisateurs de votre projet, y compris ceux contenus dans un client . L'authentification Firebase fournit des informations sur les utilisateurs de votre fonction, y compris les locataires auxquels ils appartiennent, afin que vous puissiez répondre en conséquence.

  • Lier un autre fournisseur d’identité à un compte redéclenche toutes les fonctions enregistrées beforeSignIn .

  • L'authentification anonyme et personnalisée ne déclenche pas de fonctions de blocage.

Déployer une fonction de blocage

Pour insérer votre code personnalisé dans les flux d'authentification des utilisateurs, déployez des fonctions de blocage. Une fois vos fonctions de blocage déployées, votre code personnalisé doit être exécuté avec succès pour que l'authentification et la création d'utilisateurs réussissent.

Vous déployez une fonction de blocage de la même manière que vous déployez n'importe quelle fonction. (voir la page de démarrage de Cloud Functions pour plus de détails). En résumé:

  1. Écrivez des fonctions Cloud qui gèrent l'événement beforeCreate , l'événement beforeSignIn , ou les deux.

    Par exemple, pour commencer, vous pouvez ajouter les fonctions sans opération suivantes à index.js :

    const functions = require('firebase-functions');
    
    exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
      // TODO
    });
    
    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      // TODO
    });
    

    Les exemples ci-dessus ont omis l'implémentation de la logique d'authentification personnalisée. Consultez les sections suivantes pour savoir comment implémenter vos fonctions de blocage et Scénarios courants pour des exemples spécifiques.

  2. Déployez vos fonctions à l'aide de la CLI Firebase :

    firebase deploy --only functions
    

    Vous devez redéployer vos fonctions à chaque fois que vous les mettez à jour.

Obtenir des informations sur l'utilisateur et le contexte

Les événements beforeSignIn et beforeCreate fournissent des objets User et EventContext qui contiennent des informations sur la connexion de l'utilisateur. Utilisez ces valeurs dans votre code pour déterminer s'il convient d'autoriser la poursuite d'une opération.

Pour obtenir la liste des propriétés disponibles sur l'objet User , consultez la référence de l'API UserRecord .

L'objet EventContext contient les propriétés suivantes :

Nom Description Exemple
locale Paramètres régionaux de l'application. Vous pouvez définir les paramètres régionaux à l'aide du SDK client ou en transmettant l'en-tête des paramètres régionaux dans l'API REST. fr ou sv-SE
ipAddress L'adresse IP de l'appareil à partir duquel l'utilisateur final s'inscrit ou se connecte. 114.14.200.1
userAgent L'agent utilisateur déclenchant la fonction de blocage. Mozilla/5.0 (X11; Linux x86_64)
eventId L'identifiant unique de l'événement. rWsyPtolplG2TBFoOkkgyg
eventType Le type d'événement. Cela fournit des informations sur le nom de l'événement, tel que beforeSignIn ou beforeCreate , et la méthode de connexion associée utilisée, comme Google ou e-mail/mot de passe. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Toujours USER . USER
resource Le projet ou le locataire d'authentification Firebase. projects/ project-id /tenants/ tenant-id
timestamp Heure à laquelle l'événement a été déclenché, formatée sous forme de chaîne RFC 3339 . Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Un objet contenant des informations sur l'utilisateur. AdditionalUserInfo
credential Un objet contenant des informations sur les informations d'identification de l'utilisateur. AuthCredential

Blocage de l'inscription ou de la connexion

Pour bloquer une tentative d'inscription ou de connexion, lancez une HttpsError dans votre fonction. Par exemple:

Noeud.js

throw new functions.auth.HttpsError('permission-denied');

Le tableau suivant répertorie les erreurs que vous pouvez générer, ainsi que leur message d'erreur par défaut :

Nom Code Message
invalid-argument 400 Le client a spécifié un argument non valide.
failed-precondition 400 La requête ne peut pas être exécutée dans l'état actuel du système.
out-of-range 400 Le client a spécifié une plage non valide.
unauthenticated 401 Jeton OAuth manquant, invalide ou expiré.
permission-denied 403 Le client ne dispose pas d'une autorisation suffisante.
not-found 404 La ressource spécifiée est introuvable.
aborted 409 Conflit de concurrence, tel qu'un conflit de lecture-modification-écriture.
already-exists 409 La ressource qu'un client a tenté de créer existe déjà.
resource-exhausted 429 Soit en dehors du quota de ressources, soit en atteignant la limite de débit.
cancelled 499 Demande annulée par le client.
data-loss 500 Perte de données irrécupérable ou corruption de données.
unknown 500 Erreur de serveur inconnue.
internal 500 Erreur interne du serveur.
not-implemented 501 Méthode API non implémentée par le serveur.
unavailable 503 Service non disponible.
deadline-exceeded 504 Délai de demande dépassé.

Vous pouvez également spécifier un message d'erreur personnalisé :

Noeud.js

throw new functions.auth.HttpsError('permission-denied', 'Unauthorized request origin!');

L'exemple suivant montre comment empêcher les utilisateurs qui ne font pas partie d'un domaine spécifique de s'inscrire à votre application :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  // (If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, e.g. 'projects/project-id/tenant/tenant-id-1')

  // Only users of a specific domain can sign up.
  if (user.email.indexOf('@acme.com') === -1) {
    throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Que vous utilisiez un message par défaut ou personnalisé, Cloud Functions encapsule l'erreur et la renvoie au client en tant qu'erreur interne. Par exemple:

throw new functions.auth.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);

Votre application doit détecter l'erreur et la gérer en conséquence. Par exemple:

Javascript

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
firebase.auth().createUserWithEmailAndPassword('johndoe@example.com', 'password')
  .then((result) => {
    result.user.getIdTokenResult()
  })
  .then((idTokenResult) => {
    console.log(idTokenResult.claim.admin);
  })
  .catch((error) => {
    if (error.code !== 'auth/internal-error' && error.message.indexOf('Cloud Function') !== -1) {
      // Display error.
    } else {
      // Registration succeeds.
    }
  });

Modification d'un utilisateur

Au lieu de bloquer une tentative d'enregistrement ou de connexion, vous pouvez autoriser la poursuite de l'opération, mais modifier l'objet User enregistré dans la base de données de Firebase Authentication et renvoyé au client.

Pour modifier un utilisateur, renvoyez un objet de votre gestionnaire d'événements contenant les champs à modifier. Vous pouvez modifier les champs suivants :

  • displayName
  • disabled
  • emailVerified
  • photoUrl
  • customClaims
  • sessionClaims ( beforeSignIn uniquement)

À l'exception de sessionClaims , tous les champs modifiés sont enregistrés dans la base de données de Firebase Authentication, ce qui signifie qu'ils sont inclus dans le jeton de réponse et persistent entre les sessions utilisateur.

L'exemple suivant montre comment définir un nom d'affichage par défaut :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  return {
    // If no display name is provided, set it to "Guest".
    displayName: user.displayName || 'Guest';
  };
});

Si vous enregistrez un gestionnaire d'événements pour beforeCreate et beforeSignIn , notez que beforeSignIn s'exécute après beforeCreate . Les champs utilisateur mis à jour dans beforeCreate sont visibles dans beforeSignIn . Si vous définissez un champ autre que sessionClaims dans les deux gestionnaires d'événements, la valeur définie dans beforeSignIn remplace la valeur définie dans beforeCreate . Pour sessionClaims uniquement, elles sont propagées aux revendications de jeton de la session en cours, mais ne sont ni conservées ni stockées dans la base de données.

Par exemple, si des sessionClaims sont définies, beforeSignIn les renverra avec toutes les revendications beforeCreate et elles seront fusionnées. Lorsqu'elles sont fusionnées, si une clé sessionClaims correspond à une clé dans customClaims , les customClaims correspondantes seront écrasées dans les revendications de jeton par la clé sessionClaims . Cependant, la clé customClaims surchargée sera toujours conservée dans la base de données pour les demandes futures.

Informations d'identification et données OAuth prises en charge

Vous pouvez transmettre les informations d'identification et les données OAuth aux fonctions de blocage de divers fournisseurs d'identité. Le tableau suivant indique les informations d'identification et les données prises en charge pour chaque fournisseur d'identité :

Fournisseur d'identité Jeton d'identification Jeton d'accès Date d'expiration Jeton secret Actualiser le jeton Réclamations de connexion
Google Oui Oui Oui Non Oui Non
Facebook Non Oui Oui Non Non Non
Twitter Non Oui Non Oui Non Non
GitHub Non Oui Non Non Non Non
Microsoft Oui Oui Oui Non Oui Non
LinkedIn Non Oui Oui Non Non Non
Yahoo Oui Oui Oui Non Oui Non
Pomme Oui Oui Oui Non Oui Non
SAML Non Non Non Non Non Oui
OIDC Oui Oui Oui Non Oui Oui

Actualiser les jetons

Pour utiliser un jeton d'actualisation dans une fonction de blocage, vous devez d'abord cocher la case sur la page Fonctions de blocage de la console Firebase.

Les jetons d'actualisation ne seront renvoyés par aucun fournisseur d'identité lors de la connexion directe avec un identifiant OAuth, tel qu'un jeton d'identification ou un jeton d'accès. Dans cette situation, les mêmes informations d'identification OAuth côté client seront transmises à la fonction de blocage.

Les sections suivantes décrivent chaque type de fournisseur d'identité ainsi que leurs informations d'identification et données prises en charge.

Fournisseurs OIDC génériques

Lorsqu'un utilisateur se connecte auprès d'un fournisseur OIDC générique, les informations d'identification suivantes seront transmises :

  • Jeton d'identification : fourni si le flux id_token est sélectionné.
  • Jeton d'accès : fourni si le flux de code est sélectionné. Notez que le flux de code n'est actuellement pris en charge que via l'API REST.
  • Jeton d'actualisation : fourni si la portée offline_access est sélectionnée.

Exemple:

const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Google

Lorsqu'un utilisateur se connecte avec Google, les informations d'identification suivantes seront transmises :

  • Jeton d'identification
  • Jeton d'accès
  • Jeton d'actualisation : fourni uniquement si les paramètres personnalisés suivants sont demandés :
    • access_type=offline
    • prompt=consent , si l'utilisateur a déjà consenti et qu'aucune nouvelle portée n'a été demandée

Exemple:

const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);

En savoir plus sur les jetons d'actualisation Google .

Facebook

Lorsqu'un utilisateur se connecte avec Facebook, les informations d'identification suivantes seront transmises :

  • Jeton d'accès : un jeton d'accès est renvoyé et peut être échangé contre un autre jeton d'accès. Apprenez-en davantage sur les différents types de jetons d'accès pris en charge par Facebook et sur la façon dont vous pouvez les échanger contre des jetons à longue durée de vie .

GitHub

Lorsqu'un utilisateur se connecte avec GitHub, les informations d'identification suivantes seront transmises :

  • Jeton d'accès : n'expire pas sauf révocation.

Microsoft

Lorsqu'un utilisateur se connecte avec Microsoft, les informations d'identification suivantes seront transmises :

  • Jeton d'identification
  • Jeton d'accès
  • Jeton d'actualisation : transmis à la fonction de blocage si la portée offline_access est sélectionnée.

Exemple:

const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Yahoo

Lorsqu'un utilisateur se connecte avec Yahoo, les informations d'identification suivantes seront transmises sans aucun paramètre ni étendue personnalisé :

  • Jeton d'identification
  • Jeton d'accès
  • Jeton d'actualisation

LinkedIn

Lorsqu'un utilisateur se connecte avec LinkedIn, les informations d'identification suivantes seront transmises :

  • Jeton d'accès

Pomme

Lorsqu'un utilisateur se connecte avec Apple, les informations d'identification suivantes seront transmises sans aucun paramètre ni étendue personnalisé :

  • Jeton d'identification
  • Jeton d'accès
  • Jeton d'actualisation

Scénarios courants

Les exemples suivants illustrent quelques cas d'utilisation courants pour les fonctions de blocage :

Autoriser uniquement l'enregistrement à partir d'un domaine spécifique

L'exemple suivant montre comment empêcher les utilisateurs qui ne font pas partie du domaine example.com de s'inscrire sur votre application :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (!user.email || user.email.indexOf('@example.com') === -1) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Empêcher les utilisateurs avec des e-mails non vérifiés de s'inscrire

L'exemple suivant montre comment empêcher les utilisateurs disposant d'adresses e-mail non vérifiées de s'inscrire sur votre application :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified) {
    throw new functions.auth.HttpsError(
      'invalid-argument', `Unverified email "${user.email}"`);
  }
});

Exiger une vérification par e-mail lors de l'inscription

L'exemple suivant montre comment demander à un utilisateur de vérifier son adresse e-mail après son inscription :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  const locale = context.locale;
  if (user.email && !user.emailVerified) {
    // Send custom email verification on sign-up.
    return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
      return sendCustomVerificationEmail(user.email, link, locale);
    });
  }
});

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new functions.auth.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});

Traiter certains e-mails de fournisseurs d'identité comme vérifiés

L'exemple suivant montre comment traiter les e-mails des utilisateurs provenant de certains fournisseurs d'identité comme vérifiés :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
    return {
      emailVerified: true,
    };
  }
});

Blocage de la connexion à partir de certaines adresses IP

L'exemple suivant montre comment bloquer la connexion à partir de certaines plages d'adresses IP :

Noeud.js

exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new functions.auth.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

Définition de revendications personnalisées et de session

L'exemple suivant montre comment définir des revendications personnalisées et de session :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'saml.my-provider-id') {
    return {
      // Employee ID does not change so save in persistent claims (stored in
      // Auth DB).
      customClaims: {
        eid: context.credential.claims.employeeid,
      },
      // Copy role and groups to token claims. These will not be persisted.
      sessionClaims: {
        role: context.credential.claims.role,
        groups: context.credential.claims.groups,
      }
    }
  }
});

Suivi des adresses IP pour surveiller les activités suspectes

Vous pouvez empêcher le vol de jetons en suivant l'adresse IP à partir de laquelle un utilisateur se connecte et en la comparant à l'adresse IP lors des demandes ultérieures. Si la demande semble suspecte (par exemple, les adresses IP proviennent de différentes régions géographiques), vous pouvez demander à l'utilisateur de se reconnecter.

  1. Utilisez les revendications de session pour suivre l'adresse IP avec laquelle l'utilisateur se connecte :

    Noeud.js

    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Lorsqu'un utilisateur tente d'accéder à des ressources nécessitant une authentification avec l'authentification Firebase, comparez l'adresse IP indiquée dans la requête avec l'adresse IP utilisée pour se connecter :

    Noeud.js

    app.post('/getRestrictedData', (req, res) => {
      // Get the ID token passed.
      const idToken = req.body.idToken;
      // Verify the ID token, check if revoked and decode its payload.
      admin.auth().verifyIdToken(idToken, true).then((claims) => {
        // Get request IP address
        const requestIpAddress = req.connection.remoteAddress;
        // Get sign-in IP address.
        const signInIpAddress = claims.signInIpAddress;
        // Check if the request IP address origin is suspicious relative to
        // the session IP addresses. The current request timestamp and the
        // auth_time of the ID token can provide additional signals of abuse,
        // especially if the IP address suddenly changed. If there was a sudden
        // geographical change in a short period of time, then it will give
        // stronger signals of possible abuse.
        if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
          // Suspicious IP address change. Require re-authentication.
          // You can also revoke all user sessions by calling:
          // admin.auth().revokeRefreshTokens(claims.sub).
          res.status(401).send({error: 'Unauthorized access. Please login again!'});
        } else {
          // Access is valid. Try to return data.
          getData(claims).then(data => {
            res.end(JSON.stringify(data);
          }, error => {
            res.status(500).send({ error: 'Server error!' })
          });
        }
      });
    });
    

Filtrage des photos des utilisateurs

L'exemple suivant montre comment nettoyer les photos de profil des utilisateurs :

Noeud.js

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (user.photoURL) {
    return isPhotoAppropriate(user.photoURL)
      .then((status) => {
        if (!status) {
          // Sanitize inappropriate photos by replacing them with guest photos.
          // Users could also be blocked from sign-up, disabled, etc.
          return {
            photoUrl: PLACEHOLDER_GUEST_PHOTO_URL,
          };
        }
      });
});

Pour en savoir plus sur la détection et le nettoyage des images, consultez la documentation Cloud Vision .

Accéder aux informations d'identification OAuth du fournisseur d'identité d'un utilisateur

L'exemple suivant montre comment obtenir un jeton d'actualisation pour un utilisateur connecté avec Google et l'utiliser pour appeler les API de Google Calendar. Le jeton d'actualisation est stocké pour un accès hors ligne.

Noeud.js

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'google.com') {
    // Store the refresh token for later offline use.
    // These will only be returned if refresh tokens credentials are included
    // (enabled by Cloud console).
    return saveUserRefreshToken(
        user.uid,
        context.credential.refreshToken,
        'google.com'
      )
      .then(() => {
        // Blocking the function is not required. The function can resolve while
        // this operation continues to run in the background.
        return new Promise((resolve, reject) => {
          // For this operation to succeed, the appropriate OAuth scope should be requested
          // on sign in with Google, client-side. In this case:
          // https://www.googleapis.com/auth/calendar
          // You can check granted_scopes from within:
          // context.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: context.credential.accessToken,
            refresh_token: context.credential.refreshToken,
          });
          const calendar = google.calendar('v3');
          // Setup Onboarding event on user's calendar.
          const event = {/** ... */};
          calendar.events.insert({
            auth: oauth2client,
            calendarId: 'primary',
            resource: event,
          }, (err, event) => {
            // Do not fail. This is a best effort approach.
            resolve();
          });
      });
    })
  }
});

Remplacer le verdict de reCAPTCHA Enterprise pour le fonctionnement de l'utilisateur

L'exemple suivant montre comment remplacer un verdict reCAPTCHA Enterprise pour les flux d'utilisateurs pris en charge.

Reportez-vous à Activer reCAPTCHA Enterprise pour en savoir plus sur l'intégration de reCAPTCHA Enterprise avec l'authentification Firebase.

Les fonctions de blocage peuvent être utilisées pour autoriser ou bloquer les flux en fonction de facteurs personnalisés, remplaçant ainsi le résultat fourni par reCAPTCHA Enterprise.

Noeud.js

 const {
   auth,
 } = require("firebase-functions/v1");

exports.checkrecaptchaV1 = auth.user().beforeSignIn((userRecord, context) => {
 // Allow users with a specific email domain to sign in regardless of their recaptcha score.
 if (userRecord.email && userRecord.email.indexOf('@acme.com') === -1) {
   return {
     recaptchaActionOverride: 'ALLOW',
   };
 }

 // Allow users to sign in with recaptcha score greater than 0.5
 if (context.additionalUserInfo.recaptchaScore > 0.5) {
   return {
     recaptchaActionOverride: 'ALLOW',
   };
 }

 // Block all others.
 return {
   recaptchaActionOverride: 'BLOCK',
 };
});