Administra sesiones de usuarios

Las sesiones de Firebase Authentication son de larga duración. Cada vez que un usuario accede, sus credenciales se envían al backend de Firebase Authentication y se cambian por un token de ID de Firebase (un JWT) y un token de actualización. Los tokens de ID de Firebase tienen una duración de una hora, y el token de actualización se puede usar para obtener tokens de ID nuevos. Los tokens de actualización vencen únicamente en los siguientes casos:

  • Si se borró al usuario
  • Si se inhabilitó al usuario
  • Si se detecta un cambio importante en la cuenta del usuario (esto incluye eventos como actualizaciones de contraseña o de dirección de correo electrónico)

El SDK de Firebase Admin permite revocar los tokens de actualización de un usuario específico. También está disponible una API para verificar las revocaciones de tokens de ID. Estas funciones te ayudan a tener más control sobre las sesiones de los usuarios. El SDK proporciona la capacidad de agregar restricciones que impidan usar las sesiones en determinadas circunstancias sospechosas, así como un mecanismo de recuperación en caso de robo de tokens.

Revoca los tokens de actualización

Puedes revocar los tokens de actualización cuando un usuario informe el robo o la pérdida de un dispositivo. Del mismo modo, si descubres una vulnerabilidad general o sospechas de una filtración masiva de tokens activos, puedes usar la API de listUsers para encontrar a todos los usuarios y revocar sus tokens del proyecto que especifiques.

El restablecimiento de contraseñas también revoca los tokens existentes de un usuario. Sin embargo, en estos casos, el backend de Firebase Authentication maneja automáticamente las revocaciones. Luego de que se realiza este proceso, el usuario sale de su sesión y se le pide volver a autenticarse.

Esta es una implementación de ejemplo que usa el SDK de Admin para revocar el token de actualización de un usuario específico. Sigue las instrucciones en la página de configuración para inicializar el SDK de Admin.

Node.js

// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
getAuth()
  .revokeRefreshTokens(uid)
  .then(() => {
    return getAuth().getUser(uid);
  })
  .then((userRecord) => {
    return new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
  })
  .then((timestamp) => {
    console.log(`Tokens revoked at: ${timestamp}`);
  });

Java

FirebaseAuth.getInstance().revokeRefreshTokens(uid);
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
// Convert to seconds as the auth_time in the token claims is in seconds too.
long revocationSecond = user.getTokensValidAfterTimestamp() / 1000;
System.out.println("Tokens revoked at: " + revocationSecond);

Python

# Revoke tokens on the backend.
auth.revoke_refresh_tokens(uid)
user = auth.get_user(uid)
# Convert to seconds as the auth_time in the token claims is in seconds.
revocation_second = user.tokens_valid_after_timestamp / 1000
print('Tokens revoked at: {0}'.format(revocation_second))

Go

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
if err := client.RevokeRefreshTokens(ctx, uid); err != nil {
	log.Fatalf("error revoking tokens for user: %v, %v\n", uid, err)
}
// accessing the user's TokenValidAfter
u, err := client.GetUser(ctx, uid)
if err != nil {
	log.Fatalf("error getting user %s: %v\n", uid, err)
}
timestamp := u.TokensValidAfterMillis / 1000
log.Printf("the refresh tokens were revoked at: %d (UTC seconds) ", timestamp)

C#

await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync(uid);
var user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine("Tokens revoked at: " + user.TokensValidAfterTimestamp);

Detecta la revocación de tokens de ID

Dado que los tokens de ID de Firebase son JWT sin estado, puedes solicitar el estado de un token en el backend de Firebase Authentication para saber si se revocó. Debido a lo anterior, realizar esta verificación en tu servidor es una operación costosa, ya que requiere un proceso de ida y vuelta adicional en la red. Para evitar la realización de esta solicitud de red, puedes configurar Firebase Security Rules que verifiquen las revocaciones, en lugar de usar el SDK de Admin para hacerlo.

Detecta la revocación de tokens de ID en Firebase Security Rules

Para poder detectar la revocación de tokens de ID con reglas de seguridad, primero debes almacenar algunos metadatos del usuario.

Actualiza los metadatos específicos del usuario en Firebase Realtime Database.

Guarda la marca de tiempo de la revocación de tokens de actualización. Es necesaria para hacer el seguimiento de la revocación de tokens de ID con Firebase Security Rules. Esto permite realizar verificaciones eficaces en la base de datos. En las siguientes muestras de código, usa el uid y el tiempo de revocación que obtuviste en la sección anterior.

Node.js

const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
  console.log('Database updated successfully.');
});

Java

DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
Map<String, Object> userData = new HashMap<>();
userData.put("revokeTime", revocationSecond);
ref.setValueAsync(userData);

Python

metadata_ref = firebase_admin.db.reference("metadata/" + uid)
metadata_ref.set({'revokeTime': revocation_second})

Agrega una verificación a Firebase Security Rules

Para aplicar la verificación, configura una regla sin acceso de escritura al cliente con el fin de almacenar el tiempo de revocación por usuario. Puedes actualizar la regla con la marca de tiempo en UTC de la última vez que se realizó una revocación, según se muestra en los ejemplos anteriores:

{
  "rules": {
    "metadata": {
      "$user_id": {
        // this could be false as it is only accessed from backend or rules.
        ".read": "$user_id === auth.uid",
        ".write": "false",
      }
    }
  }
}

Todos los datos que requieran acceso con autenticación deben tener configurada la siguiente regla. Esta lógica solo permite acceder a los datos protegidos a los usuarios autenticados con tokens de ID sin revocar:

{
  "rules": {
    "users": {
      "$user_id": {
        ".read": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
        ".write": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
      }
    }
  }
}

Detecta la revocación de tokens de ID en el SDK

En tu servidor, implementa la siguiente lógica para la revocación de tokens de actualización y la validación de tokens de ID:

Cuando se deba verificar el token de ID de un usuario, la marca booleana checkRevoked adicional se debe cambiar a verifyIdToken. Si se revoca el token del usuario, este debe salir de su sesión en el cliente o volver a autenticarse con las APIs de reautenticación que proporcionan los SDK de cliente de Firebase Authentication.

Sigue las instrucciones de la página de configuración a fin de inicializar el SDK de Admin para tu plataforma. Encontrarás ejemplos de recuperación de tokens de ID en la sección verifyIdToken.

Node.js

// Verify the ID token while checking if the token is revoked by passing
// checkRevoked true.
let checkRevoked = true;
getAuth()
  .verifyIdToken(idToken, checkRevoked)
  .then((payload) => {
    // Token is valid.
  })
  .catch((error) => {
    if (error.code == 'auth/id-token-revoked') {
      // Token has been revoked. Inform the user to reauthenticate or signOut() the user.
    } else {
      // Token is invalid.
    }
  });

Java

try {
  // Verify the ID token while checking if the token is revoked by passing checkRevoked
  // as true.
  boolean checkRevoked = true;
  FirebaseToken decodedToken = FirebaseAuth.getInstance()
      .verifyIdToken(idToken, checkRevoked);
  // Token is valid and not revoked.
  String uid = decodedToken.getUid();
} catch (FirebaseAuthException e) {
  if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) {
    // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
  } else {
    // Token is invalid.
  }
}

Python

try:
    # Verify the ID token while checking if the token is revoked by
    # passing check_revoked=True.
    decoded_token = auth.verify_id_token(id_token, check_revoked=True)
    # Token is valid and not revoked.
    uid = decoded_token['uid']
except auth.RevokedIdTokenError:
    # Token revoked, inform the user to reauthenticate or signOut().
    pass
except auth.UserDisabledError:
    # Token belongs to a disabled user record.
    pass
except auth.InvalidIdTokenError:
    # Token is invalid
    pass

Go

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
token, err := client.VerifyIDTokenAndCheckRevoked(ctx, idToken)
if err != nil {
	if err.Error() == "ID token has been revoked" {
		// Token is revoked. Inform the user to reauthenticate or signOut() the user.
	} else {
		// Token is invalid
	}
}
log.Printf("Verified ID token: %v\n", token)

C#

try
{
    // Verify the ID token while checking if the token is revoked by passing checkRevoked
    // as true.
    bool checkRevoked = true;
    var decodedToken = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(
        idToken, checkRevoked);
    // Token is valid and not revoked.
    string uid = decodedToken.Uid;
}
catch (FirebaseAuthException ex)
{
    if (ex.AuthErrorCode == AuthErrorCode.RevokedIdToken)
    {
        // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
    }
    else
    {
        // Token is invalid.
    }
}

Responde a la revocación de tokens en el cliente

Si el token se revoca mediante el SDK de Admin, se informará al cliente sobre ello y se esperará a que el usuario vuelva a autenticarse, o se cerrará su sesión:

function onIdTokenRevocation() {
  // For an email/password user. Prompt the user for the password again.
  let password = prompt('Please provide your password for reauthentication');
  let credential = firebase.auth.EmailAuthProvider.credential(
      firebase.auth().currentUser.email, password);
  firebase.auth().currentUser.reauthenticateWithCredential(credential)
    .then(result => {
      // User successfully reauthenticated. New ID tokens should be valid.
    })
    .catch(error => {
      // An error occurred.
    });
}

Seguridad avanzada: aplicación de restricciones de direcciones IP

Un mecanismo de seguridad habitual para detectar el robo de tokens es hacer un seguimiento de los orígenes de las direcciones IP de solicitud. Por ejemplo, si las solicitudes provienen siempre de la misma dirección IP (el servidor que realiza la llamada), se pueden aplicar sesiones de una única dirección IP. También puedes revocar el token de un usuario si detectas que la ubicación geográfica de su dirección IP cambió de forma repentina o si recibes una solicitud de un sitio sospechoso.

Para poder realizar controles de seguridad basados en direcciones IP, debes inspeccionar el token de ID de cada solicitud autenticada y comprobar si su dirección IP coincide con alguna de confianza o se encuentra dentro de un rango de confianza antes de permitir el acceso a los datos restringidos. Por ejemplo:

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 the user's previous IP addresses, previously saved.
    return getPreviousUserIpAddresses(claims.sub);
  }).then(previousIpAddresses => {
    // Get the request IP address.
    const requestIpAddress = req.connection.remoteAddress;
    // Check if the request IP address origin is suspicious relative to previous
    // 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 location change in a
    // short period of time, then it will give stronger signals of possible abuse.
    if (!isValidIpAddress(previousIpAddresses, requestIpAddress)) {
      // Invalid IP address, take action quickly and revoke all user's refresh tokens.
      revokeUserTokens(claims.uid).then(() => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      }, error => {
        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!' })
      });
    }
  });
});