获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

Controle el acceso con reclamos personalizados y reglas de seguridad

El SDK de Firebase Admin admite la definición de atributos personalizados en las cuentas de usuario. Esto brinda la capacidad de implementar varias estrategias de control de acceso, incluido el control de acceso basado en funciones, en las aplicaciones de Firebase. Estos atributos personalizados pueden dar a los usuarios diferentes niveles de acceso (roles), que se aplican en las reglas de seguridad de una aplicación.

Los roles de usuario se pueden definir para los siguientes casos comunes:

  • Otorgar a un usuario privilegios administrativos para acceder a datos y recursos.
  • Definición de diferentes grupos a los que pertenece un usuario.
  • Proporcionar acceso multinivel:
    • Diferenciar suscriptores pagos/no pagos.
    • Diferenciar a los moderadores de los usuarios habituales.
    • Solicitud de profesor/alumno, etc.
  • Agregue un identificador adicional en un usuario. Por ejemplo, un usuario de Firebase podría asignarse a un UID diferente en otro sistema.

Consideremos un caso en el que desea limitar el acceso al nodo de la base de datos "adminContent". Puede hacerlo con una búsqueda en la base de datos en una lista de usuarios administradores. Sin embargo, puede lograr el mismo objetivo de manera más eficiente utilizando un reclamo de usuario personalizado llamado admin con la siguiente regla de Realtime Database:

{
  "rules": {
    "adminContent": {
      ".read": "auth.token.admin === true",
      ".write": "auth.token.admin === true",
    }
  }
}

Se puede acceder a las reclamaciones de usuario personalizadas a través de los tokens de autenticación del usuario. En el ejemplo anterior, solo los usuarios con admin establecido en verdadero en su reclamo de token tendrían acceso de lectura/escritura al nodo adminContent . Como el token de ID ya contiene estas afirmaciones, no se necesita ningún procesamiento o búsqueda adicional para verificar los permisos de administrador. Además, el token de identificación es un mecanismo confiable para entregar estos reclamos personalizados. Todo acceso autenticado debe validar el token de ID antes de procesar la solicitud asociada.

Los ejemplos de código y las soluciones que se describen en esta página se basan tanto en las API de autenticación de Firebase del lado del cliente como en las API de autenticación del lado del servidor proporcionadas por el SDK de administrador .

Establezca y valide reclamos de usuario personalizados a través del SDK de administrador

Las reclamaciones personalizadas pueden contener datos confidenciales, por lo tanto, solo deben configurarse desde un entorno de servidor privilegiado mediante el SDK de Firebase Admin.

Nodo.js

// Set admin privilege on the user corresponding to uid.

getAuth()
  .setCustomUserClaims(uid, { admin: true })
  .then(() => {
    // The new custom claims will propagate to the user's ID token the
    // next time a new one is issued.
  });

Java

// Set admin privilege on the user corresponding to uid.
Map<String, Object> claims = new HashMap<>();
claims.put("admin", true);
FirebaseAuth.getInstance().setCustomUserClaims(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

Pitón

# Set admin privilege on the user corresponding to uid.
auth.set_custom_user_claims(uid, {'admin': True})
# The new custom claims will propagate to the user's ID token the
# next time a new one is issued.

Vamos

// Get an auth client from the firebase.App
client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}

// Set admin privilege on the user corresponding to uid.
claims := map[string]interface{}{"admin": true}
err = client.SetCustomUserClaims(ctx, uid, claims)
if err != nil {
	log.Fatalf("error setting custom claims %v\n", err)
}
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

C#

// Set admin privileges on the user corresponding to uid.
var claims = new Dictionary<string, object>()
{
    { "admin", true },
};
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.

El objeto de notificación personalizado no debe contener ningún nombre de clave reservada de OIDC ni nombres reservados de Firebase . La carga útil de reclamaciones personalizadas no debe superar los 1000 bytes.

Un token de identificación enviado a un servidor back-end puede confirmar la identidad y el nivel de acceso del usuario mediante el SDK de administrador de la siguiente manera:

Nodo.js

// Verify the ID token first.
getAuth()
  .verifyIdToken(idToken)
  .then((claims) => {
    if (claims.admin === true) {
      // Allow access to requested admin resource.
    }
  });

Java

// Verify the ID token first.
FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken);
if (Boolean.TRUE.equals(decoded.getClaims().get("admin"))) {
  // Allow access to requested admin resource.
}

Pitón

# Verify the ID token first.
claims = auth.verify_id_token(id_token)
if claims['admin'] is True:
    # Allow access to requested admin resource.
    pass

Vamos

// Verify the ID token first.
token, err := client.VerifyIDToken(ctx, idToken)
if err != nil {
	log.Fatal(err)
}

claims := token.Claims
if admin, ok := claims["admin"]; ok {
	if admin.(bool) {
		//Allow access to requested admin resource.
	}
}

C#

// Verify the ID token first.
FirebaseToken decoded = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
object isAdmin;
if (decoded.Claims.TryGetValue("admin", out isAdmin))
{
    if ((bool)isAdmin)
    {
        // Allow access to requested admin resource.
    }
}

También puede verificar los reclamos personalizados existentes de un usuario, que están disponibles como una propiedad en el objeto del usuario:

Nodo.js

// Lookup the user associated with the specified uid.
getAuth()
  .getUser(uid)
  .then((userRecord) => {
    // The claims can be accessed on the user record.
    console.log(userRecord.customClaims['admin']);
  });

Java

// Lookup the user associated with the specified uid.
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
System.out.println(user.getCustomClaims().get("admin"));

Pitón

# Lookup the user associated with the specified uid.
user = auth.get_user(uid)
# The claims can be accessed on the user record.
print(user.custom_claims.get('admin'))

Vamos

// Lookup the user associated with the specified uid.
user, err := client.GetUser(ctx, uid)
if err != nil {
	log.Fatal(err)
}
// The claims can be accessed on the user record.
if admin, ok := user.CustomClaims["admin"]; ok {
	if admin.(bool) {
		log.Println(admin)
	}
}

C#

// Lookup the user associated with the specified uid.
UserRecord user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine(user.CustomClaims["admin"]);

Puede eliminar las reclamaciones personalizadas de un usuario pasando null para customClaims .

Propagar reclamos personalizados al cliente

Después de que se modifican los nuevos reclamos en un usuario a través del SDK de administrador, se propagan a un usuario autenticado en el lado del cliente a través del token de ID de las siguientes maneras:

  • Un usuario inicia sesión o vuelve a autenticarse después de modificar las notificaciones personalizadas. El token de identificación emitido como resultado contendrá los últimos reclamos.
  • Una sesión de usuario existente actualiza su token de ID después de que caduca un token anterior.
  • Un token de ID se actualiza a la fuerza llamando a currentUser.getIdToken(true) .

Acceder a reclamaciones personalizadas en el cliente

Las reclamaciones personalizadas solo se pueden recuperar a través del token de ID del usuario. El acceso a estos reclamos puede ser necesario para modificar la interfaz de usuario del cliente según el rol o el nivel de acceso del usuario. Sin embargo, el acceso de back-end siempre debe aplicarse a través del token de ID después de validarlo y analizar sus notificaciones. Los reclamos personalizados no deben enviarse directamente al backend, ya que no se puede confiar en ellos fuera del token.

Una vez que los últimos reclamos se han propagado al token de ID de un usuario, puede obtenerlos recuperando el token de ID:

JavaScript

firebase.auth().currentUser.getIdTokenResult()
  .then((idTokenResult) => {
     // Confirm the user is an Admin.
     if (!!idTokenResult.claims.admin) {
       // Show admin UI.
       showAdminUI();
     } else {
       // Show regular user UI.
       showRegularUI();
     }
  })
  .catch((error) => {
    console.log(error);
  });

Androide

user.getIdToken(false).addOnSuccessListener(new OnSuccessListener<GetTokenResult>() {
  @Override
  public void onSuccess(GetTokenResult result) {
    boolean isAdmin = result.getClaims().get("admin");
    if (isAdmin) {
      // Show admin UI.
      showAdminUI();
    } else {
      // Show regular user UI.
      showRegularUI();
    }
  }
});

Rápido

user.getIDTokenResult(completion: { (result, error) in
  guard let admin = result?.claims?["admin"] as? NSNumber else {
    // Show regular user UI.
    showRegularUI()
    return
  }
  if admin.boolValue {
    // Show admin UI.
    showAdminUI()
  } else {
    // Show regular user UI.
    showRegularUI()
  }
})

C objetivo

user.getIDTokenResultWithCompletion:^(FIRAuthTokenResult *result,
                                      NSError *error) {
  if (error != nil) {
    BOOL *admin = [result.claims[@"admin"] boolValue];
    if (admin) {
      // Show admin UI.
      [self showAdminUI];
    } else {
      // Show regular user UI.
      [self showRegularUI];
    }
  }
}];

Prácticas recomendadas para reclamaciones personalizadas

Las notificaciones personalizadas solo se utilizan para proporcionar control de acceso. No están diseñados para almacenar datos adicionales (como el perfil y otros datos personalizados). Si bien esto puede parecer un mecanismo conveniente para hacerlo, se desaconseja encarecidamente, ya que estos reclamos se almacenan en el token de ID y podrían causar problemas de rendimiento porque todas las solicitudes autenticadas siempre contienen un token de ID de Firebase correspondiente al usuario que inició sesión.

  • Use reclamos personalizados para almacenar datos para controlar el acceso de los usuarios únicamente. Todos los demás datos deben almacenarse por separado a través de la base de datos en tiempo real u otro almacenamiento del lado del servidor.
  • Las reclamaciones personalizadas tienen un tamaño limitado. Pasar una carga útil de reclamos personalizada de más de 1000 bytes generará un error.

Ejemplos y casos de uso

Los siguientes ejemplos ilustran reclamos personalizados en el contexto de casos de uso específicos de Firebase.

Definición de roles a través de Firebase Functions en la creación de usuarios

En este ejemplo, los reclamos personalizados se establecen en un usuario en el momento de la creación mediante Cloud Functions.

Las notificaciones personalizadas se pueden agregar mediante Cloud Functions y propagarse inmediatamente con Realtime Database. La función se llama solo al registrarse mediante un disparador onCreate . Una vez que se establecen las notificaciones personalizadas, se propagan a todas las sesiones existentes y futuras. La próxima vez que el usuario inicie sesión con la credencial de usuario, el token contendrá las notificaciones personalizadas.

Implementación del lado del cliente (JavaScript)

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.catch(error => {
  console.log(error);
});

let callback = null;
let metadataRef = null;
firebase.auth().onAuthStateChanged(user => {
  // Remove previous listener.
  if (callback) {
    metadataRef.off('value', callback);
  }
  // On user login add new listener.
  if (user) {
    // Check if refresh is required.
    metadataRef = firebase.database().ref('metadata/' + user.uid + '/refreshTime');
    callback = (snapshot) => {
      // Force refresh to pick up the latest custom claims changes.
      // Note this is always triggered on first call. Further optimization could be
      // added to avoid the initial trigger when the token is issued and already contains
      // the latest claims.
      user.getIdToken(true);
    };
    // Subscribe new listener to changes on that node.
    metadataRef.on('value', callback);
  }
});

Lógica de funciones en la nube

Se agrega un nuevo nodo de base de datos (metadatos/($uid)} con lectura/escritura restringida al usuario autenticado.

const functions = require('firebase-functions');
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
const { getDatabase } = require('firebase-admin/database');

initializeApp();

// On sign up.
exports.processSignUp = functions.auth.user().onCreate(async (user) => {
  // Check if user meets role criteria.
  if (
    user.email &&
    user.email.endsWith('@admin.example.com') &&
    user.emailVerified
  ) {
    const customClaims = {
      admin: true,
      accessLevel: 9
    };

    try {
      // Set custom user claims on this newly created user.
      await getAuth().setCustomUserClaims(user.uid, customClaims);

      // Update real-time database to notify client to force refresh.
      const metadataRef = getDatabase().ref('metadata/' + user.uid);

      // Set the refresh time to the current UTC timestamp.
      // This will be captured on the client to force a token refresh.
      await  metadataRef.set({refreshTime: new Date().getTime()});
    } catch (error) {
      console.log(error);
    }
  }
});

Reglas de la base de datos

{
  "rules": {
    "metadata": {
      "$user_id": {
        // Read access only granted to the authenticated user.
        ".read": "$user_id === auth.uid",
        // Write access only via Admin SDK.
        ".write": false
      }
    }
  }
}

Definición de roles a través de una solicitud HTTP

El siguiente ejemplo establece notificaciones de usuario personalizadas en un usuario que acaba de iniciar sesión a través de una solicitud HTTP.

Implementación del lado del cliente (JavaScript)

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.then((result) => {
  // User is signed in. Get the ID token.
  return result.user.getIdToken();
})
.then((idToken) => {
  // Pass the ID token to the server.
  $.post(
    '/setCustomClaims',
    {
      idToken: idToken
    },
    (data, status) => {
      // This is not required. You could just wait until the token is expired
      // and it proactively refreshes.
      if (status == 'success' && data) {
        const json = JSON.parse(data);
        if (json && json.status == 'success') {
          // Force token refresh. The token claims will contain the additional claims.
          firebase.auth().currentUser.getIdToken(true);
        }
      }
    });
}).catch((error) => {
  console.log(error);
});

Implementación de back-end (Admin SDK)

app.post('/setCustomClaims', async (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;

  // Verify the ID token and decode its payload.
  const claims = await getAuth().verifyIdToken(idToken);

  // Verify user is eligible for additional privileges.
  if (
    typeof claims.email !== 'undefined' &&
    typeof claims.email_verified !== 'undefined' &&
    claims.email_verified &&
    claims.email.endsWith('@admin.example.com')
  ) {
    // Add custom claims for additional privileges.
    await getAuth().setCustomUserClaims(claims.sub, {
      admin: true
    });

    // Tell client to refresh token on user.
    res.end(JSON.stringify({
      status: 'success'
    }));
  } else {
    // Return nothing.
    res.end(JSON.stringify({ status: 'ineligible' }));
  }
});

Se puede usar el mismo flujo al actualizar el nivel de acceso de un usuario existente. Tomemos, por ejemplo, un usuario gratuito que se actualiza a una suscripción paga. El token de identificación del usuario se envía con la información de pago al servidor backend a través de una solicitud HTTP. Cuando el pago se procesa con éxito, el usuario se configura como suscriptor pago a través del SDK de administrador. Se devuelve una respuesta HTTP exitosa al cliente para forzar la actualización del token.

Definición de roles a través de un script de backend

Se podría configurar un script recurrente (no iniciado por el cliente) para que se ejecute para actualizar las reclamaciones personalizadas del usuario:

Nodo.js

getAuth()
  .getUserByEmail('user@admin.example.com')
  .then((user) => {
    // Confirm user is verified.
    if (user.emailVerified) {
      // Add custom claims for additional privileges.
      // This will be picked up by the user on token refresh or next sign in on new device.
      return getAuth().setCustomUserClaims(user.uid, {
        admin: true,
      });
    }
  })
  .catch((error) => {
    console.log(error);
  });

Java

UserRecord user = FirebaseAuth.getInstance()
    .getUserByEmail("user@admin.example.com");
// Confirm user is verified.
if (user.isEmailVerified()) {
  Map<String, Object> claims = new HashMap<>();
  claims.put("admin", true);
  FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), claims);
}

Pitón

user = auth.get_user_by_email('user@admin.example.com')
# Confirm user is verified
if user.email_verified:
    # Add custom claims for additional privileges.
    # This will be picked up by the user on token refresh or next sign in on new device.
    auth.set_custom_user_claims(user.uid, {
        'admin': True
    })

Vamos

user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
	log.Fatal(err)
}
// Confirm user is verified
if user.EmailVerified {
	// Add custom claims for additional privileges.
	// This will be picked up by the user on token refresh or next sign in on new device.
	err := client.SetCustomUserClaims(ctx, user.UID, map[string]interface{}{"admin": true})
	if err != nil {
		log.Fatalf("error setting custom claims %v\n", err)
	}

}

C#

UserRecord user = await FirebaseAuth.DefaultInstance
    .GetUserByEmailAsync("user@admin.example.com");
// Confirm user is verified.
if (user.EmailVerified)
{
    var claims = new Dictionary<string, object>()
    {
        { "admin", true },
    };
    await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}

Las reclamaciones personalizadas también se pueden modificar de forma incremental a través del SDK de administrador:

Nodo.js

getAuth()
  .getUserByEmail('user@admin.example.com')
  .then((user) => {
    // Add incremental custom claim without overwriting existing claims.
    const currentCustomClaims = user.customClaims;
    if (currentCustomClaims['admin']) {
      // Add level.
      currentCustomClaims['accessLevel'] = 10;
      // Add custom claims for additional privileges.
      return getAuth().setCustomUserClaims(user.uid, currentCustomClaims);
    }
  })
  .catch((error) => {
    console.log(error);
  });

Java

UserRecord user = FirebaseAuth.getInstance()
    .getUserByEmail("user@admin.example.com");
// Add incremental custom claim without overwriting the existing claims.
Map<String, Object> currentClaims = user.getCustomClaims();
if (Boolean.TRUE.equals(currentClaims.get("admin"))) {
  // Add level.
  currentClaims.put("level", 10);
  // Add custom claims for additional privileges.
  FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), currentClaims);
}

Pitón

user = auth.get_user_by_email('user@admin.example.com')
# Add incremental custom claim without overwriting existing claims.
current_custom_claims = user.custom_claims
if current_custom_claims.get('admin'):
    # Add level.
    current_custom_claims['accessLevel'] = 10
    # Add custom claims for additional privileges.
    auth.set_custom_user_claims(user.uid, current_custom_claims)

Vamos

user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
	log.Fatal(err)
}
// Add incremental custom claim without overwriting existing claims.
currentCustomClaims := user.CustomClaims
if currentCustomClaims == nil {
	currentCustomClaims = map[string]interface{}{}
}

if _, found := currentCustomClaims["admin"]; found {
	// Add level.
	currentCustomClaims["accessLevel"] = 10
	// Add custom claims for additional privileges.
	err := client.SetCustomUserClaims(ctx, user.UID, currentCustomClaims)
	if err != nil {
		log.Fatalf("error setting custom claims %v\n", err)
	}

}

C#

UserRecord user = await FirebaseAuth.DefaultInstance
    .GetUserByEmailAsync("user@admin.example.com");
// Add incremental custom claims without overwriting the existing claims.
object isAdmin;
if (user.CustomClaims.TryGetValue("admin", out isAdmin) && (bool)isAdmin)
{
    var claims = new Dictionary<string, object>(user.CustomClaims);
    // Add level.
    claims["level"] = 10;
    // Add custom claims for additional privileges.
    await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}