Administrar sesiones de usuario

Las sesiones de Firebase Authentication duran mucho tiempo. Cada vez que un usuario inicia sesión, las credenciales del usuario se envían al backend de Firebase Authentication y se intercambian por un token de ID de Firebase (un JWT) y un token de actualización. Los tokens de ID de Firebase duran poco y duran una hora; el token de actualización se puede utilizar para recuperar nuevos tokens de identificación. Los tokens de actualización caducan solo cuando ocurre una de las siguientes situaciones:

  • El usuario es eliminado
  • El usuario está deshabilitado
  • Se detecta un cambio importante en la cuenta del usuario. Esto incluye eventos como actualizaciones de contraseñas o direcciones de correo electrónico.

El SDK de Firebase Admin brinda la capacidad de revocar tokens de actualización para un usuario específico. Además, también está disponible una API para comprobar la revocación del token de identificación. Con estas capacidades, tiene más control sobre las sesiones de los usuarios. El SDK ofrece la posibilidad de agregar restricciones para evitar que las sesiones se utilicen en circunstancias sospechosas, así como un mecanismo de recuperación ante un posible robo de tokens.

Revocar tokens de actualización

Puede revocar el token de actualización existente de un usuario cuando un usuario reporta un dispositivo perdido o robado. De manera similar, si descubre una vulnerabilidad general o sospecha de una fuga a gran escala de tokens activos, puede usar la API listUsers para buscar todos los usuarios y revocar sus tokens para el proyecto especificado.

Los restablecimientos de contraseña también revocan los tokens existentes de un usuario; sin embargo, el backend de Firebase Authentication maneja la revocación automáticamente en ese caso. Tras la revocación, se cierra la sesión del usuario y se le solicita que se vuelva a autenticar.

A continuación se muestra una implementación de ejemplo que utiliza el SDK de administración para revocar el token de actualización de un usuario determinado. Para inicializar el SDK de administración, siga las instrucciones en la página de configuración .

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

Pitón

# 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))

Ir

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

Detectar revocación de token de ID

Debido a que los tokens de ID de Firebase son JWT sin estado, puedes determinar que un token ha sido revocado solo solicitando el estado del token al backend de Firebase Authentication. Por esta razón, realizar esta verificación en su servidor es una operación costosa que requiere un viaje de ida y vuelta adicional en la red. Puede evitar realizar esta solicitud de red configurando reglas de seguridad de Firebase que verifiquen la revocación en lugar de usar el SDK de administrador para realizar la verificación.

Detectar la revocación del token de ID en las reglas de seguridad de Firebase

Para poder detectar la revocación del token de ID mediante reglas de seguridad, primero debemos almacenar algunos metadatos específicos del usuario.

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

Guarde la marca de tiempo de revocación del token de actualización. Esto es necesario para realizar un seguimiento de la revocación del token de identificación mediante las reglas de seguridad de Firebase. Esto permite realizar comprobaciones eficientes dentro de la base de datos. En los ejemplos de código siguientes, utilice el uid y el tiempo de revocación obtenidos en la sección anterior .

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

Pitón

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

Agregar una marca a las reglas de seguridad de Firebase

Para hacer cumplir esta verificación, configure una regla sin acceso de escritura del cliente para almacenar el tiempo de revocación por usuario. Esto se puede actualizar con la marca de tiempo UTC de la última hora de revocación, como 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",
      }
    }
  }
}

Cualquier dato que requiera acceso autenticado debe tener configurada la siguiente regla. Esta lógica solo permite que los usuarios autenticados con tokens de identificación no revocados accedan a los datos protegidos:

{
  "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()
        )",
      }
    }
  }
}

Detectar la revocación del token de ID en el SDK.

En su servidor, implemente la siguiente lógica para la revocación del token de actualización y la validación del token de ID:

Cuando se debe verificar el token de identificación de un usuario, se debe pasar el indicador booleano checkRevoked adicional a verifyIdToken . Si se revoca el token del usuario, se debe cerrar sesión en el cliente o pedirle que se vuelva a autenticar mediante las API de reautenticación proporcionadas por los SDK del cliente de Firebase Authentication.

Para inicializar el SDK de administración para su plataforma, siga las instrucciones en la página de configuración . En la sección verifyIdToken se encuentran ejemplos de cómo recuperar el token de identificación.

Nodo.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.
  }
}

Pitón

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

Ir

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

Responder a la revocación de token en el cliente

Si el token se revoca a través del SDK de administración, se informa al cliente de la revocación y se espera que el usuario se vuelva a autenticar o cierre 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: hacer cumplir las restricciones de direcciones IP

Un mecanismo de seguridad común para detectar el robo de tokens es realizar un seguimiento de los orígenes de las direcciones IP de las solicitudes. Por ejemplo, si las solicitudes siempre provienen de la misma dirección IP (el servidor que realiza la llamada), se pueden imponer sesiones de dirección IP única. O bien, puede revocar el token de un usuario si detecta que la dirección IP del usuario cambió repentinamente de geolocalización o recibe una solicitud de un origen sospechoso.

Para realizar comprobaciones de seguridad basadas en la dirección IP, para cada solicitud autenticada, inspeccione el token de identificación y verifique si la dirección IP de la solicitud coincide con direcciones IP confiables anteriores o está dentro de un rango confiable antes de permitir el acceso a 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!' })
      });
    }
  });
});