Controle el acceso con reclamos personalizados y reglas de seguridad

El SDK de Firebase Admin admite la definición de atributos personalizados en cuentas de usuario. Esto brinda la capacidad de implementar varias estrategias de control de acceso, incluido el control de acceso basado en roles, en las aplicaciones de Firebase. Estos atributos personalizados pueden brindar 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.
  • Definir los 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/estudiante, etc.
  • Agregue un identificador adicional a un usuario. Por ejemplo, un usuario de Firebase podría asignar 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 base de datos en tiempo real:

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

Se puede acceder a las reclamaciones de usuarios personalizadas a través de los tokens de autenticación del usuario. En el ejemplo anterior, solo los usuarios con admin configurado en verdadero en su reclamo de token tendrían acceso de lectura/escritura al nodo adminContent . Como el token de identificación ya contiene estas afirmaciones, no se necesita ningún procesamiento ni búsqueda adicional para verificar los permisos de administrador. Además, el token de identificación es un mecanismo confiable para entregar estas reclamaciones personalizadas. Todo acceso autenticado debe validar el token de identificación 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 .

Configure y valide reclamos de usuarios personalizados a través del SDK de administración

Las reclamaciones personalizadas pueden contener datos confidenciales; por lo tanto, el SDK de Firebase Admin solo debe configurarlas desde un entorno de servidor privilegiado.

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.

Ir

// 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 notificaciones personalizado no debe contener ningún nombre de clave reservada de OIDC ni nombre reservado de Firebase . La carga útil de reclamos personalizados no debe exceder los 1000 bytes.

Un token de identificación enviado a un servidor backend puede confirmar la identidad del usuario y el nivel de acceso mediante el SDK de administración 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

Ir

// 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 consultar los reclamos personalizados existentes de un usuario, que están disponibles como una propiedad en el objeto de 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'))

Ir

// 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 los reclamos personalizados de un usuario pasando null para customClaims .

Propagar reclamos personalizados al cliente.

Después de que se modifican nuevas reclamaciones 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 se vuelve a autenticar después de modificar las reclamaciones personalizadas. El token de identificación emitido como resultado contendrá las últimas reclamaciones.
  • Una sesión de usuario existente actualiza su token de ID después de que caduque un token más antiguo.
  • Un token de ID se actualiza a la fuerza llamando a currentUser.getIdToken(true) .

Acceda a reclamos personalizados en el cliente

Las reclamaciones personalizadas solo se pueden recuperar a través del token de identificación del usuario. El acceso a estas notificaciones puede ser necesario para modificar la interfaz de usuario del cliente según la función o el nivel de acceso del usuario. Sin embargo, el acceso al backend siempre debe aplicarse a través del token de identificación después de validarlo y analizar sus reclamos. 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 hayan propagado al token de identificación de un usuario, puedes obtenerlos recuperando el token de identificación:

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];
    }
  }
}];

Mejores prácticas para reclamos personalizados

Los reclamos personalizados solo se utilizan para proporcionar control de acceso. No están diseñados para almacenar datos adicionales (como perfiles y otros datos personalizados). Si bien esto puede parecer un mecanismo conveniente para hacerlo, se desaconseja enfáticamente ya que estas afirmaciones 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.

  • Utilice 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 reclamaciones 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, las reclamaciones personalizadas se configuran para un usuario en el momento de su creación mediante Cloud Functions.

Se pueden agregar reclamos personalizados usando Cloud Functions y propagarlos inmediatamente con Realtime Database. La función se llama solo al registrarse utilizando un activador onCreate . Una vez que se establecen los reclamos personalizados, 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
      }
    }
  }
}

Definir roles mediante una solicitud HTTP

El siguiente ejemplo establece reclamos de usuario personalizados para 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 backend (SDK de administrador)

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 utilizar el mismo flujo al actualizar el nivel de acceso de un usuario existente. Tomemos, por ejemplo, un usuario gratuito que 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 mediante una solicitud HTTP. Cuando el pago se procesa correctamente, el usuario se configura como suscriptor pago a través del SDK de administración. Se devuelve una respuesta HTTP exitosa al cliente para forzar la actualización del token.

Definición de roles mediante script de backend

Se podría configurar una secuencia de comandos recurrente (no iniciada desde 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
    })

Ir

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

Los reclamos personalizados 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)

Ir

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