Amplíe la autenticación de Firebase con funciones de bloqueo


Las funciones de bloqueo le permiten ejecutar código personalizado que modifica el resultado de que un usuario se registre o inicie sesión en su aplicación. Por ejemplo, puede evitar que un usuario se autentique si no cumple con ciertos criterios o actualizar la información de un usuario antes de devolverla a su aplicación cliente.

Antes de que empieces

Para utilizar funciones de bloqueo, debe actualizar su proyecto de Firebase a Firebase Authentication con Identity Platform. Si aún no lo has actualizado, hazlo primero.

Comprender las funciones de bloqueo

Puede registrar funciones de bloqueo para dos eventos:

  • beforeCreate : se activa antes de que se guarde un nuevo usuario en la base de datos de Firebase Authentication y antes de que se devuelva un token a su aplicación cliente.

  • beforeSignIn : se activa después de que se verifican las credenciales de un usuario, pero antes de que Firebase Authentication devuelva un token de identificación a su aplicación cliente. Si su aplicación utiliza autenticación multifactor, la función se activa después de que el usuario verifica su segundo factor. Tenga en cuenta que la creación de un nuevo usuario también activa beforeSignIn , además de beforeCreate .

Tenga en cuenta lo siguiente al utilizar funciones de bloqueo:

  • Su función debe responder en 7 segundos. Después de 7 segundos, Firebase Authentication devuelve un error y la operación del cliente falla.

  • Los códigos de respuesta HTTP distintos de 200 se pasan a sus aplicaciones cliente. Asegúrese de que su código de cliente maneje cualquier error que su función pueda devolver.

  • Las funciones se aplican a todos los usuarios de su proyecto, incluidos los contenidos en un inquilino . Firebase Authentication proporciona información sobre los usuarios de su función, incluidos los inquilinos a los que pertenecen, para que pueda responder en consecuencia.

  • Vincular otro proveedor de identidad a una cuenta reactiva cualquier función registrada beforeSignIn .

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

Implementar una función de bloqueo

Para insertar su código personalizado en los flujos de autenticación de usuario, implemente funciones de bloqueo. Una vez que se implementen sus funciones de bloqueo, su código personalizado debe completarse exitosamente para que la autenticación y la creación de usuarios se realicen correctamente.

Implementas una función de bloqueo de la misma manera que implementas cualquier función. (consulte la página de introducción a Cloud Functions para obtener más detalles). En resumen:

  1. Escriba funciones en la nube que manejen el evento beforeCreate , el evento beforeSignIn o ambos.

    Por ejemplo, para comenzar, puede agregar las siguientes funciones no operativas a 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
    });
    

    Los ejemplos anteriores han omitido la implementación de la lógica de autenticación personalizada. Consulte las siguientes secciones para aprender cómo implementar sus funciones de bloqueo y escenarios comunes para ejemplos específicos.

  2. Implemente sus funciones usando Firebase CLI:

    firebase deploy --only functions
    

    Debe volver a implementar sus funciones cada vez que las actualice.

Obtener información del usuario y del contexto

Los eventos beforeSignIn y beforeCreate proporcionan objetos User y EventContext que contienen información sobre el usuario que inicia sesión. Utilice estos valores en su código para determinar si debe permitir que continúe una operación.

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

El objeto EventContext contiene las siguientes propiedades:

Nombre Descripción Ejemplo
locale La configuración regional de la aplicación. Puede configurar la configuración regional utilizando el SDK del cliente o pasando el encabezado de la configuración regional en la API REST. fr o sv-SE
ipAddress La dirección IP del dispositivo desde el que el usuario final se registra o inicia sesión. 114.14.200.1
userAgent El agente de usuario 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 inicio de sesión asociado utilizado, como Google o correo electrónico/contraseña. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Siempre USER . USER
resource El proyecto o inquilino de Firebase Authentication. projects/ project-id /tenants/ tenant-id
timestamp La hora en que se activó el evento, formateada como una cadena 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

Bloquear el registro o el inicio de sesión

Para bloquear un intento de registro o inicio de sesión, introduzca un HttpsError en su función. Por ejemplo:

Nodo.js

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

La siguiente tabla enumera los errores que puede generar, 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 caducado.
permission-denied 403 El cliente no tiene permiso suficiente.
not-found 404 No se encuentra el recurso especificado.
aborted 409 Conflicto de simultaneidad, como un conflicto de lectura-modificación-escritura.
already-exists 409 El recurso que un cliente intentó crear ya existe.
resource-exhausted 429 Ya sea fuera de la cuota de recursos o alcanzando el límite de velocidad.
cancelled 499 Solicitud cancelada por el cliente.
data-loss 500 Pérdida de datos irrecuperable o corrupción de datos.
unknown 500 Error de servidor desconocido.
internal 500 Error Interno del Servidor.
not-implemented 501 Método API no implementado por el servidor.
unavailable 503 Servicio No Disponible.
deadline-exceeded 504 Se excedió el plazo de solicitud.

También puede especificar un mensaje de error personalizado:

Nodo.js

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

El siguiente ejemplo muestra cómo impedir que los usuarios que no están dentro de un dominio específico se registren en su aplicación:

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

Independientemente de si utiliza un mensaje predeterminado o personalizado, Cloud Functions encapsula el error y lo devuelve al cliente como un error interno. Por ejemplo:

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

Su aplicación debería detectar el error y manejarlo en consecuencia. 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.
    }
  });

Modificar un usuario

En lugar de bloquear un intento de registro o inicio de sesión, puede permitir que la operación continúe, pero modificar el objeto User que se guarda en la base de datos de Firebase Authentication y se devuelve al cliente.

Para modificar un usuario, devuelva un objeto de su controlador de eventos que contenga los campos a modificar. Puede modificar los siguientes campos:

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

Con la excepción de sessionClaims , todos los campos modificados se guardan en la base de datos de Firebase Authentication, lo que significa que se incluyen en el token de respuesta y persisten entre sesiones de usuario.

El siguiente ejemplo muestra cómo establecer un nombre para mostrar predeterminado:

Nodo.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 registra un controlador de eventos tanto para beforeCreate como beforeSignIn , tenga en cuenta que beforeSignIn se ejecuta después de beforeCreate . Los campos de usuario actualizados en beforeCreate son visibles en beforeSignIn . Si configura un campo que no sea sessionClaims en ambos controladores de eventos, el valor establecido en beforeSignIn sobrescribe el valor establecido en beforeCreate . Solo para sessionClaims , se propagan a las notificaciones de token de la sesión actual, pero no persisten ni se almacenan en la base de datos.

Por ejemplo, si se establece cualquier sessionClaims , beforeSignIn los devolverá con cualquier reclamo beforeCreate y se fusionarán. Cuando se fusionan, si una clave sessionClaims coincide con una clave en customClaims , la clave sessionClaims sobrescribirá las customClaims coincidentes en las notificaciones del token. Sin embargo, la clave customClaims exagerada seguirá persistiendo en la base de datos para futuras solicitudes.

Credenciales y datos de OAuth admitidos

Puede pasar credenciales y datos de OAuth para bloquear funciones de varios proveedores de identidad. La siguiente tabla muestra qué credenciales y datos se admiten para cada proveedor de identidad:

Proveedor de identidad ficha de identificación Token de acceso Tiempo de expiración Secreto simbólico Actualizar ficha Iniciar sesión Reclamaciones
Google No No
Facebook No No No No
Gorjeo No No No No
GitHub No No No No No
microsoft No No
LinkedIn No No No No
yahoo No No
Manzana No No
SAML No No No No No
OIDC No

Actualizar fichas

Para usar un token de actualización en una función de bloqueo, primero debe seleccionar la casilla de verificación en la página Funciones de bloqueo de Firebase console.

Ningún proveedor de identidad devolverá tokens de actualización al iniciar sesión directamente con una credencial OAuth, como un token de identificación o un token de acceso. En esta situación, se pasará la misma credencial OAuth del lado del cliente a la función de bloqueo.

Las siguientes secciones describen cada tipo de proveedor de identidad y sus credenciales y datos admitidos.

Proveedores de OIDC genéricos

Cuando un usuario inicia sesión con un proveedor OIDC genérico, se pasarán las siguientes credenciales:

  • Token de ID : proporcionado si se selecciona el flujo id_token .
  • Token de acceso : proporcionado si se selecciona el flujo de código. Tenga en cuenta que actualmente el flujo de código solo se admite a través de la API REST.
  • Token de actualización : proporcionado si se selecciona 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 inicia sesión con Google, se pasarán las siguientes credenciales:

  • ficha de identificación
  • 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 dio su consentimiento previamente y no se solicitó ningún nuevo alcance

Ejemplo:

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

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

Facebook

Cuando un usuario inicia sesión en Facebook, se le transmitirá la siguiente credencial:

  • Token de acceso : se devuelve un token de acceso que se puede canjear por otro token de acceso. Obtenga más información sobre los diferentes tipos de tokens de acceso admitidos por Facebook y cómo puede intercambiarlos por tokens de larga duración .

GitHub

Cuando un usuario inicia sesión con GitHub, se le pasará la siguiente credencial:

  • Token de acceso : no caduca a menos que se revoque.

microsoft

Cuando un usuario inicia sesión en Microsoft, se pasarán las siguientes credenciales:

  • ficha de identificación
  • token de acceso
  • Token de actualización : se pasa a la función de bloqueo si se selecciona 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 inicia sesión en Yahoo, se pasarán las siguientes credenciales sin parámetros ni ámbitos personalizados:

  • ficha de identificación
  • token de acceso
  • Actualizar ficha

LinkedIn

Cuando un usuario inicia sesión en LinkedIn, se le transmitirá la siguiente credencial:

  • token de acceso

Manzana

Cuando un usuario inicia sesión con Apple, se pasarán las siguientes credenciales sin parámetros ni ámbitos personalizados:

  • ficha de identificación
  • token de acceso
  • Actualizar ficha

Escenarios comunes

Los siguientes ejemplos demuestran algunos casos de uso comunes para funciones de bloqueo:

Permitir solo el registro desde un dominio específico

El siguiente ejemplo muestra cómo evitar que los usuarios que no forman parte del dominio example.com se registren en su aplicación:

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

Bloquear el registro de usuarios con correos electrónicos no verificados

El siguiente ejemplo muestra cómo evitar que los usuarios con correos electrónicos no verificados se registren en su aplicación:

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

Requerir verificación por correo electrónico al registrarse

El siguiente ejemplo muestra cómo solicitar que un usuario verifique su correo electrónico después de registrarse:

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

Tratar ciertos correos electrónicos de proveedores de identidad como verificados

El siguiente ejemplo muestra cómo tratar los correos electrónicos de los usuarios de ciertos proveedores de identidad como verificados:

Nodo.js

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

Bloquear el inicio de sesión desde ciertas direcciones IP

El siguiente ejemplo de cómo bloquear el inicio de sesión desde ciertos rangos de direcciones IP:

Nodo.js

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

Configuración de reclamos personalizados y de sesión

El siguiente ejemplo muestra cómo configurar notificaciones personalizadas y de sesión:

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

Seguimiento de direcciones IP para monitorear actividades sospechosas

Puede evitar el robo de tokens rastreando la dirección IP desde la que inicia sesión un usuario y comparándola con la dirección IP en solicitudes posteriores. Si la solicitud parece sospechosa (por ejemplo, las IP provienen de diferentes regiones geográficas), puede pedirle al usuario que inicie sesión nuevamente.

  1. Utilice notificaciones de sesión para rastrear la dirección IP con la que el usuario inicia sesión:

    Nodo.js

    exports.beforeSignIn = functions.auth.user().beforeSignIn((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Cuando un usuario intenta acceder a recursos que requieren autenticación con Firebase Authentication, compare la dirección IP en la solicitud con la IP utilizada para iniciar sesión:

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

Proyección de fotos de usuarios

El siguiente ejemplo muestra cómo desinfectar las fotos de perfil de los usuarios:

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

Para obtener más información sobre cómo detectar y desinfectar imágenes, consulte la documentación de Cloud Vision .

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

El siguiente ejemplo demuestra cómo obtener un token de actualización para un usuario que inició sesión con Google y usarlo para llamar a las API de Google Calendar. El token de actualización se almacena para el acceso sin conexión.

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

Anular el veredicto de reCAPTCHA Enterprise para la operación del usuario

El siguiente ejemplo muestra cómo anular un veredicto de reCAPTCHA Enterprise para flujos de usuarios admitidos.

Consulte Habilitar reCAPTCHA Enterprise para obtener más información sobre cómo integrar reCAPTCHA Enterprise con Firebase Authentication.

Las funciones de bloqueo se pueden utilizar para permitir o bloquear flujos según factores personalizados, anulando así el resultado proporcionado por reCAPTCHA Enterprise.

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