Personaliza el flujo de autenticación mediante funciones de bloqueo

En este documento, se muestra cómo extender la autenticación de Identity Platform con funciones de Cloud Run de bloqueo.

Las funciones de bloqueo te permiten ejecutar un código personalizado que modifica el resultado de un usuario que se registra o accede a tu app. Por ejemplo, puedes evitar que se autentique un usuario si no cumple con ciertos criterios o actualizar la información de un usuario antes de mostrársela a tu app cliente.

Antes de comenzar

Crea una app con Identity Platform. Consulta la Guía de inicio rápido para aprender a hacer esto.

Información sobre las funciones de bloqueo

Puedes registrar funciones de bloqueo para dos eventos:

  • beforeCreate: Se activa antes de que se guarde un usuario nuevo en la base de datos de Identity Platform y antes de que se muestre un token a la app cliente.

  • beforeSignIn: Se activa después de que se verifican las credenciales de un usuario, pero antes de que Identity Platform muestre un token de ID en tu app cliente. Si tu app usa la autenticación de varios factores, la función se activará después de que el usuario verifique su segundo factor. Ten en cuenta que crear un usuario nuevo también activa beforeSignIn, además de beforeCreate.

Ten en cuenta lo siguiente cuando uses funciones de bloqueo:

  • La función debe responder en un plazo de 7 segundos. Después de ese plazo, Identity Platform muestra un error y la operación del cliente falla.

  • Los códigos de respuesta HTTP que no sean 200 se pasan a las apps cliente. Asegúrate de que el código de cliente administre cualquier error que pueda mostrar tu función.

  • Las funciones se aplican a todos los usuarios de tu proyecto, incluidos los que se encuentren en un grupo de usuarios. Identity Platform proporciona información sobre los usuarios a tu función, incluidos los grupos de usuarios a los que pertenecen, de modo que puedas responder en consecuencia.

  • La vinculación de otro proveedor de identidad con una cuenta vuelve a activar cualquier función beforeSignIn registrada. Esto no incluye a los proveedores de correo electrónico y contraseñas.

  • La autenticación anónima y personalizada no admite funciones de bloqueo.

  • Si también usas funciones asíncronas, el objeto de usuario que recibe una función asíncrona no contiene actualizaciones de la función de bloqueo.

Crea una función de bloqueo

En los siguientes pasos, se muestra cómo crear una función de bloqueo:

  1. Ve a la página Configuración de Identity Platform en la console de Google Cloud.

    Ir a la página de Configuración

  2. Selecciona la pestaña Activadores.

  3. Si deseas crear una función de bloqueo para el registro de usuarios, selecciona el menú desplegable Función en Antes de crear (beforeCreate) y, luego, haz clic en Crear función. Si deseas crear una función de bloqueo para el acceso del usuario, crea una función en Antes de acceder (beforebefore).

  4. Crea una función nueva:

    1. Ingresa un Nombre para la función.

    2. En el campo Activador, selecciona HTTP.

    3. En el campo Autenticación, selecciona Permitir invocaciones no autenticadas.

    4. Haz clic en Siguiente.

  5. Con el editor directo, abre index.js. Borra el código helloWorld de ejemplo y reemplázalo por una de las siguientes opciones:

    Para responder al registro, usa este código:

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

    Para responder al acceso, usa este código:

    import gcipCloudFunctions from 'gcip-cloud-functions';
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      // TODO
    });
    
  6. Abre package.json y agrega el siguiente bloque de dependencias: Para obtener la versión más reciente del SDK, consulta gcip-cloud-functions.

    {
      "type": "module",
      "name": ...,
      "version": ...,
    
      "dependencies": {
        "gcip-cloud-functions": "^0.2.0"
      }
    }
    
  7. Establece el punto de entrada de la función en beforeSignIn.

  8. Haz clic en Implementar para publicar tu función.

  9. Haz clic en Guardar en la página de funciones de bloqueo de Identity Platform.

Consulta las siguientes secciones para aprender a implementar tu función. Debes volver a implementarla cada vez que la actualices.

También puedes crear y administrar funciones con Google Cloud CLI o la API de REST. Consulta la documentación de las funciones de Cloud Run para aprender a usar Google Cloud CLI para implementar una función.

Obtén información sobre el usuario y el contexto

Los eventos beforeSignIn y beforeCreate proporcionan objetos User y EventContext que contienen información sobre el acceso del usuario. Usa estos valores en tu código para determinar si permites que una operación continúe.

Para obtener una lista de las propiedades disponibles en el objeto User, consulta la referencia de la API de UserRecord.

El objeto EventContext contiene las siguientes propiedades:

Nombre Descripción Ejemplo
locale La configuración regional de la aplicación. Para configurar la configuración regional, puedes usar el SDK de cliente o pasar el encabezado de configuración regional en la API de REST. fr o sv-SE
ipAddress La dirección IP del dispositivo desde el que se registra o accede el usuario final. 114.14.200.1
userAgent El usuario-agente que activa la función de bloqueo. Mozilla/5.0 (X11; Linux x86_64)
eventId El identificador único del evento. rWsyPtolplG2TBFoOkkgyg
eventType El tipo de evento. Esto proporciona información sobre el nombre del evento, como beforeSignIn o beforeCreate, y el método de acceso asociado, como Google o correo electrónico y contraseña. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Siempre USER. USER
resource El usuario o el proyecto de Identity Platform. projects/project-id/tenants/tenant-id
timestamp El momento en que se activó el evento, en formato de string RFC 3339. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Un objeto que contiene información sobre el usuario. AdditionalUserInfo
credential Un objeto que contiene información sobre la credencial del usuario. AuthCredential

Bloquea el registro o el acceso

Para bloquear un intento de acceso o registro, arroja un HttpsError en tu función. Por ejemplo:

Node.js

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

En la siguiente tabla, se enumeran los errores que puedes arrojar, junto con su mensaje de error predeterminado:

Nombre Código Mensaje
invalid-argument 400 El cliente especificó un argumento no válido.
failed-precondition 400 La solicitud no se puede ejecutar en el estado actual del sistema.
out-of-range 400 El cliente especificó un rango no válido.
unauthenticated 401 Token de OAuth faltante, no válido o vencido.
permission-denied 403 El cliente no cuenta con los permisos necesarios.
not-found 404 No se encontró el recurso especificado.
aborted 409 Conflicto de simultaneidad, como un conflicto de lectura-modificación-escritura.
already-exists 409 El recurso que el cliente intentó crear ya existe.
resource-exhausted 429 Sin cuota de recursos o a punto de alcanzar el límite de frecuencia.
cancelled 499 El cliente canceló la solicitud.
data-loss 500 Daño o pérdida de datos no recuperable.
unknown 500 Error de servidor desconocido.
internal 500 Error interno del servidor
not-implemented 501 El servidor no implementó el método de la API.
unavailable 503 Servicio no disponible.
deadline-exceeded 504 Se excedió el plazo de la solicitud.

También puedes especificar un mensaje de error personalizado:

Node.js

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

En el siguiente ejemplo, se muestra cómo impedir que se registren en tu app los usuarios que no son miembros de un dominio específico:

Node.js

// Import the Cloud Auth Admin module.
import gcipCloudFunctions from 'gcip-cloud-functions';
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Run 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.endsWith('@acme.com')) {
    throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Independientemente de si usas un mensaje predeterminado o personalizado, las funciones de Cloud Run unen el error y lo muestran al cliente como un error interno. Por ejemplo, si arrojas el siguiente error en tu función:

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

Se muestra un error similar al siguiente en tu aplicación cliente (si usas el SDK cliente, el error se une como un error interno):

{
  "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"
      }
    ]
  }
}

Tu app debe detectar el error y procesarlo según corresponda. Por ejemplo:

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

Modifica un usuario

En vez de bloquear un intento de acceso o registro, puedes permitir que la operación continúe, pero modificar el objeto User que se guarda en la base de datos de Identity Platform y se muestra al cliente.

Si quieres modificar un usuario, muestra un objeto de tu controlador de eventos que contenga los campos que deseas modificar. Puedes modificar los campos siguientes:

  • displayName
  • disabled
  • emailVerified
  • photoURL
  • customClaims
  • sessionClaims (solo beforeSignIn)

A excepción de sessionClaims, todos los campos modificados se guardan en la base de datos de Identity Platform, lo que significa que se incluyen en el token de respuesta y se conservan entre las sesiones del usuario.

En el siguiente ejemplo, se muestra cómo configurar un nombre visible predeterminado:

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 registras un controlador de eventos para beforeCreate y beforeSignIn, ten en cuenta que beforeSignIn se ejecuta después de beforeCreate. Los campos de usuario actualizados en beforeCreate se pueden ver en beforeSignIn. Si estableces un campo distinto de sessionClaims en ambos controladores de eventos, el valor establecido en beforeSignIn reemplaza al valor establecido en beforeCreate. Solo para sessionClaims, se propagan a las reclamaciones de token de la sesión actual, pero no se conservan ni se almacenan en la base de datos.

Por ejemplo, si se configuran sessionClaims, beforeSignIn los mostrará con cualquier reclamación beforeCreate y se fusionarán. Cuando se fusionan, si una clave de sessionClaims coincide con otra de customClaims, el elemento customClaims coincidente se reemplazará en los reclamos del token por la clave de sessionClaims. Sin embargo, la clave de customClaims reemplazada se mantiene en la base de datos para solicitudes futuras.

Datos y credenciales de OAuth admitidos

Puedes pasar credenciales y datos de OAuth a las funciones de bloqueo desde varios proveedores de identidad. En la siguiente tabla, se muestran las credenciales y los datos compatibles con cada proveedor de identidad:

Proveedor de identidad Token de ID Token de acceso Hora de vencimiento Secret del token Token de actualización Reclamaciones de acceso
Google No No
Facebook No No No No
Twitter No No No No
GitHub No No No No No
Microsoft No No
LinkedIn No No No No
Yahoo No No
Apple No No
SAML No No No No No
OIDC No

Tokens de actualización

Para usar un token de actualización en una función de bloqueo, primero debes seleccionar el la casilla de verificación en la sección Activadores de la página Incluir credenciales de token en el menú desplegable de la consola de Google Cloud.

Ningún proveedor de identidad mostrará tokens de actualización cuando acceda directamente con una credencial de OAuth, como un token de ID o de acceso. En este caso, la misma credencial de OAuth del cliente se pasará a la función de bloqueo. Sin embargo, para los flujos de 3 segmentos, es posible que haya un token de actualización disponible si el proveedor de identidad lo admita.

En las siguientes secciones, se describe cada tipo de proveedor de identidad, así como sus credenciales y datos admitidos.

Proveedores de OIDC genéricos

Cuando un usuario accede con un proveedor de OIDC genérico, se pasan las siguientes credenciales:

  • Token de ID: Se proporciona si está seleccionado el flujo id_token.
  • Token de acceso: Se proporciona si está seleccionado el flujo de código. Ten en cuenta que, por el momento, el flujo de código solo es compatible con la API de REST.
  • Token de actualización: Se proporciona si está seleccionado el alcance offline_access.

Ejemplo:

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

Google

Cuando un usuario accede con Google, se pasan las siguientes credenciales:

  • Token de ID
  • Token de acceso
  • Token de actualización: Solo se proporciona si se solicitan los siguientes parámetros personalizados:
    • access_type=offline
    • prompt=consent, si el usuario otorgó su consentimiento antes y no se solicitó un permiso nuevo

Ejemplo:

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

Obtén más información sobre los tokens de actualización de Google.

Facebook

Cuando un usuario accede con Facebook, se pasa la siguiente credencial:

  • Token de acceso: Se muestra un token de acceso que se puede intercambiar por otro. Obtén más información sobre los diferentes tipos de tokens de acceso compatibles con Facebook y cómo puedes intercambiarlos por tokens de larga duración.

GitHub

Cuando un usuario accede con GitHub, se pasa la siguiente credencial:

  • Token de acceso: No vence, a menos que lo revoques.

Microsoft

Cuando un usuario accede con Microsoft, se pasan las siguientes credenciales:

  • Token de ID
  • Token de acceso
  • Token de actualización: Se pasa a la función de bloqueo si está seleccionado el alcance offline_access.

Ejemplo:

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

Yahoo

Cuando un usuario accede con Yahoo, se pasan las siguientes credenciales sin ningún parámetro o alcance personalizado:

  • Token de ID
  • Token de acceso
  • Token de actualización

LinkedIn

Cuando un usuario accede con LinkedIn, se pasa la siguiente credencial:

  • Token de acceso

Apple

Cuando un usuario accede con Apple, se pasan las siguientes credenciales sin ningún parámetro o alcance personalizado:

  • Token de ID
  • Token de acceso
  • Token de actualización

Situaciones comunes

En los siguientes ejemplos, se muestran algunos casos de uso comunes de las funciones de bloqueo:

Solo permite registros de un dominio específico

En el siguiente ejemplo, se muestra cómo impedir que se registren en tu app los usuarios que no forman parte del dominio example.com:

Node.js

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

Impide que se registren los usuarios con correos electrónicos no verificados

En el siguiente ejemplo, se muestra cómo impedir que se registren en tu app los usuarios con correos electrónicos no verificados:

Node.js

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

Exige la verificación por correo electrónico durante el registro

En el siguiente ejemplo, se muestra cómo exigir que un usuario verifique su correo electrónico después de registrarse:

Node.js

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

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

Tratar ciertos correos electrónicos del proveedor de identidad como verificados

En el siguiente ejemplo, se muestra cómo tratar los correos electrónicos de los usuarios de ciertos proveedores de identidad como verificados:

Node.js

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

Bloquea el acceso desde ciertas direcciones IP

En el siguiente ejemplo, se muestra cómo bloquear el acceso desde ciertos rangos de direcciones IP:

Node.js

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

Configura reclamaciones personalizadas y de sesión

En el siguiente ejemplo, se muestra cómo configurar reclamaciones personalizadas y de sesión:

Node.js

exports.beforeCreate = 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,
      }
    }
  }
});

Haz un seguimiento de las direcciones IP para supervisar actividades sospechosas

Puedes evitar el robo de tokens realizando un seguimiento de la dirección IP desde la que accede un usuario y comparándola con la dirección IP de las solicitudes posteriores. Si la solicitud parece sospechosa (por ejemplo, las IP provienen de distintas regiones geográficas), puedes pedirle al usuario que vuelva a acceder.

  1. Usa reclamaciones de sesión para hacer un seguimiento de la dirección IP con la que accede el usuario:

    Node.js

    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Cuando un usuario intenta acceder a los recursos que requieren autenticación con Identity Platform, compara la dirección IP en la solicitud con la IP que se usó para acceder:

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

Filtra fotos del usuario

En el siguiente ejemplo, se muestra cómo limpiar las fotos de perfil de los usuarios:

Node.js

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

Si necesitas más información para detectar y limpiar imágenes, consulta la documentación de Cloud Vision.

Accede a las credenciales de OAuth del proveedor de identidad de un usuario

En el siguiente ejemplo, se muestra cómo obtener un token de actualización para un usuario que accedió con Google y usarlo a fin de llamar a las API de Calendario de Google. El token de actualización se almacena para el acceso sin conexión.

Node.js

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
const gcipCloudFunctions = require('gcip-cloud-functions');

// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

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

Próximos pasos