Personnaliser le flux d'authentification à l'aide de fonctions de blocage

Ce document explique comment étendre l'authentification Identity Platform à l'aide du blocage de Cloud Functions.

Les fonctions de blocage vous permettent d'exécuter un code personnalisé qui modifie le résultat d'un utilisateur qui s'enregistre ou se connecte à 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 le renvoyer à votre application cliente.

Avant de commencer

Créer une application avec Identity Platform Pour savoir comment, consultez le guide de démarrage rapide.

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 Identity Platform et avant qu'un jeton ne soit renvoyé à votre application cliente.

  • beforeSignIn : se déclenche après la validation des identifiants des utilisateurs, mais avant qu'Identity Platform renvoie un jeton d'ID à l'application cliente. Si votre application utilise l'authentification multifacteur, la fonction se déclenche une fois que l'utilisateur a validé son second facteur. Notez que la création d'un utilisateur déclenche également beforeSignIn, en plus de beforeCreate.

Tenez compte des points suivants lorsque vous utilisez des fonctions de blocage :

  • Votre fonction doit répondre dans un délai de sept secondes. Au bout de sept secondes, Identity Platform 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 le code client gère toutes les erreurs que votre fonction peut renvoyer.

  • Les fonctions s'appliquent à tous les utilisateurs de votre projet, y compris les éléments contenus dans un locataire. Identity Platform fournit des informations sur les utilisateurs de votre fonction, y compris sur les locataires auxquels ils appartiennent. Vous pouvez ainsi répondre en conséquence.

  • Associer un autre fournisseur d'identité à un compte déclenche à nouveau les fonctions beforeSignIn enregistrées.

  • L'authentification anonyme et personnalisée n'accepte pas les fonctions de blocage.

Créer une fonction de blocage

Pour créer une fonction de blocage, procédez comme suit :

  1. Accédez à la page Paramètres d'Identity Platform dans Cloud Console.

    Accéder à la page Paramètres

  2. Sélectionnez l'onglet Triggers (Déclencheurs).

  3. Pour créer une fonction de blocage pour l'inscription des utilisateurs, sélectionnez le menu déroulant Fonction sous Avant la création (beforeCreate), puis cliquez sur Créer une fonction. Pour créer une fonction de blocage pour la connexion utilisateur, créez une fonction sous Avant la connexion (beforeSignIn).

  4. Créez une fonction :

    1. Attribuez un nom à votre fonction.

    2. Dans le champ Trigger (Déclencheur), sélectionnez HTTP.

    3. Dans le champ Authentication (Authentification), cochez Allow unauthenticated invocations (Autoriser les appels non authentifiés).

    4. Cliquez sur Next (Suivant).

  5. Dans l'éditeur intégré, ouvrez index.js. Supprimez l'exemple de code helloWorld et remplacez-le par l'un des éléments suivants :

    Pour répondre à l'inscription, procédez comme suit :

    import * as gcipCloudFunctions from 'gcip-cloud-functions';
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
      // TODO
    });
    

    Pour répondre à la connexion, procédez comme suit :

    import * as gcipCloudFunctions from 'gcip-cloud-functions';
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      // TODO
    });
    
  6. Ouvrez package.json et ajoutez ce qui suit :

    "dependencies": {
      "gcip-cloud-functions": "^0.0.1"
    }
    
  7. Cliquez sur Deploy (Déployer) pour publier votre fonction.

  8. Cliquez sur Save (Enregistrer) sur la page des fonctions de blocage d'Identity Platform.

Consultez les sections suivantes pour découvrir comment mettre en œuvre votre fonction. Vous devez redéployer votre fonction chaque fois que vous la mettez à jour.

Vous pouvez également créer et gérer des fonctions à l'aide de l'outil de ligne de commande gcloud ou de l'API REST. Consultez la documentation de Cloud Functions pour découvrir comment déployer une fonction à l'aide de l'outil de ligne de commande gcloud.

Obtenir des informations sur l'utilisateur et le contexte

Les événements beforeSignIn et beforeCreate fournissent des objets User et EventContext contenant des informations sur la connexion de l'utilisateur. Utilisez ces valeurs dans votre code pour déterminer si vous souhaitez autoriser une opération.

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

L'objet EventContext comporte 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 Adresse IP de l'appareil à partir duquel l'utilisateur final s'enregistre ou se connecte. 114.14.200.1
userAgent User-agent qui déclenche la fonction de blocage. Mozilla/5.0 (X11; Linux x86_64)
eventId Identifiant unique de l'événement. rWsyPtolplG2TBFoOkkgyg
eventType Type d'événement. Fournit des informations sur le nom de l'événement, tels que beforeSignIn ou beforeCreate, ainsi que sur la méthode de connexion associée utilisée, par exemple Google ou la messagerie/mot de passe. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Toujours USER. USER
resource Le projet ou le locataire Identity Platform. projects/project-id/tenants/tenant-id
timestamp Heure à laquelle l'événement a été déclenché, au format de chaîne RFC 3339. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Objet contenant des informations sur l'utilisateur. AdditionalUserInfo
credential Objet contenant des informations sur les identifiants de l'utilisateur. AuthCredential

Blocage de l'enregistrement ou de la connexion

Pour bloquer une tentative d'enregistrement ou de connexion, envoyez une commande HttpsError dans votre fonction. Exemple :

Node.js

throw new gcipCloudFunctions.https.HttpsError('permission-denied');

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

Nom Code Message
invalid-argument 400 Le client a spécifié un argument incorrect.
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, non valide ou ayant expiré.
permission-denied 403 Le client ne dispose pas d'une autorisation suffisante.
not-found 404 Ressource indiquée introuvable.
aborted 409 Un conflit de simultanéité existe, tel qu'un conflit lecture-modification-écriture.
already-exists 409 La ressource qu'un client a essayé de créer existe déjà.
resource-exhausted 429 Le quota de ressources est dépassé ou la limite du débit est atteinte.
cancelled 499 La demande a été annulée par le client.
data-loss 500 Perte de données irrécupérable ou corruption de données.
unknown 500 Erreur du serveur inconnue.
internal 500 Erreur interne du serveur.
not-implemented 501 Méthode d'API non mise en œuvre par le serveur.
unavailable 503 Service indisponible.
deadline-exceeded 504 Délai de requête dépassé.

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

Node.js

throw new gcipCloudFunctions.https.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:

Node.js

// Import the Cloud Auth Admin module.
const gcipCloudFunctions = require('gcip-cloud-functions');
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Functions.
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  // If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, eg. '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 gcipCloudFunctions.https.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, si vous générez l'erreur suivante dans votre fonction :

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

Une erreur semblable à l'exemple suivant est renvoyée à votre application cliente (si vous utilisez le SDK client, l'erreur est encapsulée en tant qu'erreur interne) :

{
  "error": {
    "code": 400,
    "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
    "errors": [
      {
        "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
        "domain": "global",
        "reason": "invalid"
      }
    ]
  }
}

Votre application doit détecter l'erreur et la traiter en conséquence. 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.
    }
  });

Modifier un compte utilisateur

Plutôt que de bloquer une tentative d'enregistrement ou de connexion, vous pouvez autoriser l'opération à continuer, mais modifier l'objet User enregistré dans la base de données Identity Platform et renvoyé au client.

Pour modifier un utilisateur, renvoyez un objet à partir 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 Identity Platform, ce qui signifie qu'ils sont inclus dans le jeton de réponse et conservés entre les sessions utilisateur.

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

Node.js

exports.beforeCreate = authClient.functions().beforeCreateHandler((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 écrase la valeur définie dans beforeCreate. Pour sessionClaims uniquement, ils sont transmis aux revendications de jetons de la session en cours, mais ne sont pas conservés ni stockés dans la base de données.

Par exemple, si un élément sessionClaims est défini, beforeSignIn les renverra avec les revendications beforeCreate, et celles-ci sont fusionnées. Lorsqu'une clé sessionClaims correspond à une clé de customClaims, la clé customClaims correspondante est écrasée dans les revendications de jeton par le sessionClaims. Cependant, la clé overwiten customClaims sera conservée dans la base de données pour les futures requêtes.

Identifiants et données OAuth compatibles

Vous pouvez transmettre des identifiants et des données OAuth à des fonctions de blocage provenant de divers fournisseurs d'identité. Le tableau suivant indique les identifiants et les données compatibles avec chaque fournisseur d'identité :

Fournisseur d'identité Jeton d'ID Jeton d'accès Date/Heure d'expiration Code secret du jeton Jeton d'actualisation Revendications 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
Apple Oui Oui Oui Non Oui Non
SAML Non Non Non Non Non Oui
OIDC Oui Oui Oui Non Oui Oui

Jetons d'actualisation

Pour utiliser un jeton d'actualisation dans une fonction de blocage, vous devez d'abord cocher la case dans la section Déclencheurs du menu déroulant Inclure les identifiants de jeton dans Cloud Console.

Les jetons d'actualisation ne sont pas renvoyés par les fournisseurs d'identité lorsqu'ils se connectent directement avec des identifiants OAuth, tels qu'un jeton d'ID ou un jeton d'accès. Dans ce cas, les mêmes identifiants OAuth côté client seront transmis à la fonction de blocage. Toutefois, pour les flux en trois étapes, un jeton d'actualisation peut être disponible si le fournisseur d'identité le propose.

Les sections suivantes décrivent chaque type de fournisseur d'identité, ainsi que les identifiants et les données compatibles.

Fournisseurs OIDC génériques

Lorsqu'un utilisateur se connecte avec un fournisseur OIDC générique, les identifiants suivants sont transmis :

  • 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 compatible qu'avec l'API REST.
  • Jeton d'actualisation : fourni si le champ d'application offline_access est sélectionné.

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 identifiants suivants sont transmis :

  • Jeton d'ID
  • Jeton d'accès
  • Jeton d'actualisation : cet élément n'est fourni que si les paramètres personnalisés suivants sont demandés :
    • access_type=offline
    • prompt=consent, si l'utilisateur a déjà donné l'autorisation et qu'aucun nouveau champ d'application n'a été demandé

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 identifiants suivants sont transmis :

  • Jeton d'accès : un jeton d'accès est renvoyé qui peut être échangé pour un autre jeton d'accès. Découvrez les différents types de jetons d'accès acceptés par Facebook et apprenez à les échanger contre des jetons de longue durée.

GitHub

Lorsqu'un utilisateur se connecte avec GitHub, les identifiants suivants sont transmis :

  • Jeton d'accès : n'expire pas, sauf s'il est révoqué.

Microsoft

Lorsqu'un utilisateur se connecte avec Microsoft, les identifiants suivants sont transmis :

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 identifiants suivants sont transmis sans paramètre ni champ d'application personnalisés :

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

LinkedIn

Lorsqu'un utilisateur se connecte avec LinkedIn, les identifiants suivants sont transmis :

  • Jeton d'accès

Apple

Lorsqu'un utilisateur se connecte avec Apple, les identifiants suivants sont transmis sans paramètre ni champ d'application personnalisés :

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

Scénarios courants

Les exemples suivants illustrent certains cas d'utilisation courants des fonctions de blocage :

Autoriser uniquement l'enregistrement depuis un domaine spécifique

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

Node.js

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

Empêcher les utilisateurs dont l'adresse e-mail n'a pas été validée

L'exemple suivant montre comment empêcher les utilisateurs associés à des adresses e-mail non validées d'être enregistrés dans votre application :

Node.js

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

Exiger la validation de l'adresse e-mail au moment de l'inscription

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

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((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);
    });
  }
});

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

Traiter certaines adresses e-mail du fournisseur d'identité comme validées

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

Node.js

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

Bloquer la connexion à partir de certaines adresses IP

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

Node.js

export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new gcipCloudFunctions.https.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

Définir des revendications personnalisées et de session

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

Node.js

export.beforeSignIn = authClient.functions().beforeCreateHandler((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,
      }
    }
  }
});

Suivre les adresses IP pour surveiller les activités suspectes

Vous pouvez éviter le vol de jetons en effectuant le suivi de l'adresse IP à laquelle l'utilisateur se connecte et en la comparant à l'adresse IP lors des requêtes suivantes. Si la requête semble suspecte (par exemple, si 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 :

    Node.js

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

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

Filtrer les photos des utilisateurs

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

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((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 de Cloud Vision.

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

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

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

export.beforeCreate = authClient.functions().beforeCreateHandler((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();
          });
      });
    })
  }
});

Étape suivante