Personalizar el flujo de autenticación con funciones de bloqueo
En este documento se explica cómo ampliar la autenticación de Identity Platform mediante funciones de Cloud Run de bloqueo.
Estas funciones te permiten ejecutar código personalizado que determine lo que ocurrirá cuando un usuario se registre o inicie sesión en tu aplicación. Por ejemplo, puedes impedir que un usuario se autentique si no cumple determinados criterios o actualizar la información de un usuario antes de devolverla a tu aplicación cliente.
Antes de empezar
Crea una aplicación con Identity Platform. Consulta la guía de inicio rápido para saber cómo hacerlo.
Información sobre las funciones de bloqueo
Puedes registrar funciones de bloqueo para dos eventos:
beforeCreate
: se activa antes de que se guarde un nuevo usuario en la base de datos de Identity Platform y antes de que se devuelva un token a tu aplicación cliente.beforeSignIn
: se activa después de que se verifiquen las credenciales de un usuario, pero antes de que Identity Platform devuelva un token de ID a tu aplicación cliente. Si tu aplicación usa la autenticación multifactor, la función se activa después de que el usuario verifique su segundo factor. Ten en cuenta que, al crear un usuario, también se activabeforeSignIn
, además debeforeCreate
.
Ten en cuenta lo siguiente cuando uses las funciones de bloqueo:
Tu función debe responder en un plazo de 7 segundos. Después de 7 segundos, Identity Platform devuelve un error y la operación del cliente falla.
Los códigos de respuesta HTTP distintos de
200
se transfieren a tus aplicaciones cliente. Asegúrate de que tu código de cliente gestione los errores que pueda devolver tu función.Las funciones se aplican a todos los usuarios de tu proyecto, incluidos los que se encuentren en un cliente. Identity Platform proporciona información sobre los usuarios a tu función, incluidos los inquilinos a los que pertenecen, para que puedas responder en consecuencia.
Vincular otro proveedor de identidades a una cuenta vuelve a activar las funciones
beforeSignIn
registradas. No se incluyen 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 las actualizaciones de la función de bloqueo.
Crear una función de bloqueo
En los siguientes pasos se muestra cómo crear una función de bloqueo:
Ve a la página Configuración de Identity Platform en laGoogle Cloud consola.
Selecciona la pestaña Desencadenadores.
Para crear una función de bloqueo para el registro de usuarios, selecciona el menú desplegable Función en Antes de crear (beforeCreate) y, a continuación, haz clic en Crear función. Para crear una función de bloqueo para el inicio de sesión de los usuarios, crea una función en Antes de iniciar sesión (beforeSignIn).
Para crear una función, sigue estos pasos:
Introduzca un nombre para la función.
En el campo Activador, selecciona HTTP.
En el campo Autenticación, selecciona Permitir las invocaciones sin autenticar.
Haz clic en Siguiente.
Abre
index.js
con el editor insertado. Elimina el código de ejemplohelloWorld
y sustitúyelo por uno de los siguientes:Para responder al registro:
import gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => { // TODO });
Para responder al inicio de sesión, sigue estos pasos:
import gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { // TODO });
Abre
package.json
y añade el siguiente bloque de dependencias: Para ver la versión más reciente del SDK, consultagcip-cloud-functions
.{ "type": "module", "name": ..., "version": ..., "dependencies": { "gcip-cloud-functions": "^0.2.0" } }
Define el punto de entrada de la función como
beforeSignIn
Haz clic en Desplegar para publicar tu función.
En la página de funciones de bloqueo de Identity Platform, haz clic en Guardar.
Consulta las siguientes secciones para saber cómo implementar tu función. Debes volver a implementar tu función cada vez que la actualices.
También puedes crear y gestionar funciones con la CLI de Google Cloud o la API REST. Consulta la documentación de Cloud Run Functions para saber cómo usar la CLI de Google Cloud para desplegar una función.
Obtener información de usuarios y de contexto
Los eventos beforeSignIn
y beforeCreate
proporcionan objetos User
y EventContext
que contienen información sobre el usuario que inicia sesión. Usa estos valores en tu código para determinar si se permite que se lleve a cabo una operación.
Para ver una lista de las propiedades disponibles en el objeto User
, consulta la referencia de la API UserRecord
.
El objeto EventContext
contiene las siguientes propiedades:
Nombre | Descripción | Ejemplo |
---|---|---|
locale |
Configuración regional de la aplicación. Puedes definir la configuración regional mediante el SDK de cliente o pasando el encabezado de 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
| Identificador único del evento. | rWsyPtolplG2TBFoOkkgyg |
eventType
|
El tipo de evento. Proporciona información sobre el nombre del evento, como beforeSignIn o beforeCreate , y el método de inicio de sesión asociado que se ha usado, como Google o correo electrónico y contraseña.
|
providers/cloud.auth/eventTypes/user.beforeSignIn:password
|
authType
| Siempre USER . |
USER
|
resource
| El proyecto o el propietario de Identity Platform. |
projects/project-id/tenants/tenant-id
|
timestamp
| La hora en la que se activó el evento, con el formato de una cadena RFC 3339. | Tue, 23 Jul 2019 21:10:57 GMT
|
additionalUserInfo
| Objeto que contiene información sobre el usuario. |
AdditionalUserInfo
|
credential
| Objeto que contiene información sobre la credencial del usuario. |
AuthCredential
|
Bloquear el registro o el inicio de sesión
Para bloquear un registro o un intento de inicio de sesión, lanza una 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 puede generar, junto con su mensaje de error predeterminado:
Nombre | Código | Mensaje |
---|---|---|
invalid-argument |
400 |
El cliente especificó un argumento que no es válido. |
failed-precondition |
400 |
No se puede realizar la solicitud debido al estado actual del sistema. |
out-of-range |
400 |
El cliente especificó un intervalo que no es válido. |
unauthenticated |
401 |
Falta el token de OAuth, no es válido o ha caducado. |
permission-denied |
403 |
El cliente no tiene suficientes permisos. |
not-found |
404 |
No se ha encontrado el recurso especificado. |
aborted |
409 |
Se ha producido un conflicto de simultaneidad (por ejemplo, relacionado con acciones de lectura, modificación y escritura). |
already-exists |
409 |
El cliente ha intentado crear un recurso que ya existe. |
resource-exhausted |
429 |
Se ha agotado la cuota del recurso o se ha alcanzado el límite de frecuencia. |
cancelled |
499 |
El cliente ha cancelado la solicitud. |
data-loss |
500 |
Se han perdido o dañado los datos de forma irrecuperable. |
unknown |
500 |
Se ha producido un error de servidor desconocido. |
internal |
500 |
Error de servidor interno. |
not-implemented |
501 |
El servidor no ha implementado el método de API. |
unavailable |
503 |
Servicio no disponible. |
deadline-exceeded |
504 |
Se ha superado el plazo límite 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 bloquear el registro en tu aplicación de usuarios que no pertenezcan a 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}"`);
}
});
Tanto si usas un mensaje predeterminado como uno personalizado, Cloud Run Functions envolverá el error y lo devolverá al cliente como un error interno. Por ejemplo, si se produce el siguiente error en tu función:
throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);
Se devuelve un error similar al siguiente a tu aplicación cliente (si usas el SDK de cliente, el error se encapsula 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 aplicación debe detectar el error y gestionarlo como 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.
}
});
Modificar un usuario
En lugar de bloquear un intento de registro o inicio de sesión, 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 devuelve al cliente.
Para modificar un usuario, devuelve un objeto desde tu controlador de eventos que contenga los campos que quieras modificar. Puedes modificar los siguientes campos:
displayName
disabled
emailVerified
photoURL
customClaims
sessionClaims
(solobeforeSignIn
)
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 sesiones de usuario.
En el siguiente ejemplo se muestra cómo definir 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 define un campo que no sea sessionClaims
en ambos controladores de eventos, el valor definido en beforeSignIn
sobrescribirá el valor definido en beforeCreate
. En el caso de sessionClaims
, se propagan a las reclamaciones de tokens de la sesión actual, pero no se conservan ni se almacenan en la base de datos.
Por ejemplo, si se ha definido algún sessionClaims
, beforeSignIn
los devolverá
con cualquier reclamación beforeCreate
y se combinarán. Cuando se combinan, si una clave sessionClaims
coincide con una clave de customClaims
, la clave customClaims
coincidente se sobrescribirá en las reclamaciones de token con la clave sessionClaims
. Sin embargo, la clave customClaims
sobrescrita se sigue conservando en la base de datos para futuras solicitudes.
Credenciales y datos de OAuth admitidos
Puedes transferir credenciales y datos de OAuth a funciones de bloqueo desde varios proveedores de identidades. En la siguiente tabla se muestran las credenciales y los datos admitidos para cada proveedor de identidades:
Proveedor de identidades | Token de ID | Token de acceso | Fecha de caducidad | Secreto de token | Token de actualización | Reclamaciones de inicio de sesión |
---|---|---|---|---|---|---|
Sí | Sí | Sí | No | Sí | No | |
No | Sí | Sí | No | No | No | |
No | Sí | No | Sí | No | No | |
GitHub | No | Sí | No | No | No | No |
Microsoft | Sí | Sí | Sí | No | Sí | No |
No | Sí | Sí | No | No | No | |
Yahoo | Sí | Sí | Sí | No | Sí | No |
Apple | Sí | Sí | Sí | No | Sí | No |
SAML | No | No | No | No | No | Sí |
OIDC | Sí | Sí | Sí | No | Sí | Sí |
Tokens de actualización
Para usar un token de actualización en una función de bloqueo, primero debes seleccionar la casilla de verificación de la sección Activadores del menú desplegable Incluir credenciales de token de la consola de Google Cloud .
Ningún proveedor de identidades devolverá tokens de actualización al iniciar sesión directamente con una credencial de OAuth, como un token de ID o un token de acceso. En este caso, la misma credencial de OAuth del lado del cliente se pasará a la función de bloqueo. Sin embargo, en los flujos de 3 pasos, puede haber un token de actualización si el proveedor de identidades lo admite.
En las siguientes secciones se describen los tipos de proveedores de identidades y las credenciales y los datos que admiten.
Proveedores de OIDC genéricos
Cuando un usuario inicia sesión con un proveedor de OIDC genérico, se transfieren las siguientes credenciales:
- Token de ID: se proporciona si se selecciona el flujo
id_token
. - Token de acceso: se proporciona si se selecciona el flujo de código. Ten en cuenta que el flujo de código solo se admite actualmente a través de la API REST.
- Token de actualización: se proporciona si se selecciona el permiso
offline_access
.
Ejemplo:
const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Cuando un usuario inicia sesión con Google, se transfieren 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 ya había dado su consentimiento y no se ha solicitado ningún ámbito nuevo
Ejemplo:
const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
'access_type': 'offline',
'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);
Más información sobre los tokens de actualización de Google
Cuando un usuario inicia sesión con Facebook, se transfiere la siguiente credencial:
- Token de acceso: se devuelve un token de acceso que se puede canjear por otro token de acceso. Consulta más información sobre los distintos tipos de tokens de acceso que admite Facebook y cómo puedes intercambiarlos por tokens de larga duración.
GitHub
Cuando un usuario inicia sesión con GitHub, se transmite la siguiente credencial:
- Token de acceso: no caduca a menos que se revoque.
Microsoft
Cuando un usuario inicia sesión con Microsoft, se transfieren las siguientes credenciales:
- Token de ID
- Token de acceso
- Token de actualización: se envía a la función de bloqueo si se selecciona el ámbito
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 con Yahoo, se transfieren las siguientes credenciales sin parámetros ni permisos personalizados:
- Token de ID
- Token de acceso
- Token de actualización
Cuando un usuario inicia sesión con LinkedIn, se transfiere la siguiente credencial:
- Token de acceso
Apple
Cuando un usuario inicia sesión con Apple, se transfieren las siguientes credenciales sin parámetros ni permisos personalizados:
- Token de ID
- Token de acceso
- Token de actualización
Situaciones habituales
En los siguientes ejemplos se muestran algunos casos prácticos habituales de las funciones de bloqueo:
Permitir el registro solo desde un dominio específico
En el siguiente ejemplo se muestra cómo impedir que los usuarios que no forman parte del dominio example.com
se registren en tu aplicación:
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}"`);
}
});
Bloquear el registro de usuarios con correos no verificados
En el siguiente ejemplo se muestra cómo impedir que los usuarios con correos no verificados se registren en tu aplicación:
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}"`);
}
});
Requerir verificación por correo electrónico al registrarse
En el siguiente ejemplo se muestra cómo solicitar a un usuario que verifique su correo 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 como verificados determinados correos de proveedores de identidades
En el siguiente ejemplo se muestra cómo tratar como verificados los correos de usuarios de determinados proveedores de identidades:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
return {
emailVerified: true,
};
}
});
Bloquear el inicio de sesión desde determinadas direcciones IP
En el siguiente ejemplo se muestra cómo bloquear el inicio de sesión desde determinados intervalos 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!');
}
});
Definir reclamaciones personalizadas y de sesión
En el siguiente ejemplo se muestra cómo definir 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,
}
}
}
});
Monitorizar direcciones IP para detectar actividad sospechosa
Para evitar el robo de tokens, puedes monitorizar la dirección IP desde la que inicia sesión un usuario y compararla con la dirección IP de las solicitudes posteriores. Si la solicitud parece sospechosa (por ejemplo, las IPs proceden de diferentes regiones geográficas), puedes pedirle al usuario que vuelva a iniciar sesión.
Usa las reclamaciones de sesión para monitorizar la dirección IP con la que inicia sesión el usuario:
Node.js
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });
Cuando un usuario intenta acceder a recursos que requieren autenticación con Identity Platform, compara la dirección IP de la solicitud con la IP utilizada para iniciar sesión:
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!' }) }); } }); });
Filtrar fotos de usuarios
En el siguiente ejemplo se muestra cómo sanear 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,
};
}
});
});
Para obtener más información sobre cómo detectar y anonimizar imágenes, consulta la documentación de Cloud Vision.
Acceder a las credenciales de OAuth del proveedor de identidades de un usuario
En el siguiente ejemplo se muestra cómo obtener un token de actualización de un usuario que ha iniciado sesión con Google y cómo usarlo para llamar a las APIs de Google Calendar. 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();
});
});
})
}
});
Siguientes pasos
- Amplía la autenticación con funciones asíncronas.
- Consulta más información sobre las funciones de Cloud Run.