Join us in person and online for Firebase Summit on October 18, 2022. Learn how Firebase can help you accelerate app development, release your app with confidence, and scale with ease. Register now

Contrôlez l'accès avec des revendications personnalisées et des règles de sécurité

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

Le SDK Firebase Admin prend en charge la définition d'attributs personnalisés sur les comptes d'utilisateurs. Cela permet de mettre en œuvre diverses stratégies de contrôle d'accès, y compris le contrôle d'accès basé sur les rôles, dans les applications Firebase. Ces attributs personnalisés peuvent donner aux utilisateurs différents niveaux d'accès (rôles), qui sont appliqués dans les règles de sécurité d'une application.

Les rôles d'utilisateur peuvent être définis pour les cas courants suivants :

  • Donner à un utilisateur des privilèges administratifs pour accéder aux données et aux ressources.
  • Définir différents groupes auxquels appartient un utilisateur.
  • Fournir un accès à plusieurs niveaux :
    • Différencier les abonnés payants/non payants.
    • Différencier les modérateurs des utilisateurs réguliers.
    • Candidature enseignant/élève, etc.
  • Ajouter un identifiant supplémentaire sur un utilisateur. Par exemple, un utilisateur Firebase peut être mappé à un UID différent dans un autre système.

Considérons un cas où vous souhaitez limiter l'accès au nœud de base de données "adminContent". Vous pouvez le faire avec une recherche de base de données sur une liste d'utilisateurs administrateurs. Cependant, vous pouvez atteindre le même objectif plus efficacement en utilisant une revendication d'utilisateur personnalisée nommée admin avec la règle de base de données en temps réel suivante :

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

Les revendications utilisateur personnalisées sont accessibles via les jetons d'authentification de l'utilisateur. Dans l'exemple ci-dessus, seuls les utilisateurs avec admin défini sur true dans leur revendication de jeton auraient un accès en lecture/écriture au nœud adminContent . Comme le jeton d'ID contient déjà ces assertions, aucun traitement ou recherche supplémentaire n'est nécessaire pour vérifier les autorisations d'administrateur. De plus, le jeton d'identification est un mécanisme fiable pour fournir ces revendications personnalisées. Tous les accès authentifiés doivent valider le jeton d'identification avant de traiter la demande associée.

Les exemples de code et les solutions décrits dans cette page s'inspirent à la fois des API d'authentification Firebase côté client et des API d'authentification côté serveur fournies par le SDK Admin .

Définir et valider les revendications utilisateur personnalisées via le SDK d'administration

Les revendications personnalisées peuvent contenir des données sensibles. Par conséquent, elles ne doivent être définies qu'à partir d'un environnement de serveur privilégié par le SDK Firebase Admin.

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

Python

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

Aller

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

L'objet de revendications personnalisées ne doit pas contenir de noms de clé réservés OIDC ni de noms réservés Firebase . La charge utile des revendications personnalisées ne doit pas dépasser 1 000 octets.

Un jeton d'identification envoyé à un serveur principal peut confirmer l'identité et le niveau d'accès de l'utilisateur à l'aide du SDK Admin comme suit :

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

Python

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

Aller

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

Vous pouvez également vérifier les revendications personnalisées existantes d'un utilisateur, qui sont disponibles en tant que propriété sur l'objet utilisateur :

Node.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"));

Python

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

Aller

// 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"]);

Vous pouvez supprimer les revendications personnalisées d'un utilisateur en transmettant null pour customClaims .

Propager les revendications personnalisées au client

Une fois que de nouvelles revendications sont modifiées sur un utilisateur via le SDK Admin, elles sont propagées à un utilisateur authentifié côté client via le jeton d'ID de la manière suivante :

  • Un utilisateur se connecte ou s'authentifie à nouveau après la modification des revendications personnalisées. Le jeton d'identification émis en conséquence contiendra les dernières revendications.
  • Une session utilisateur existante obtient son jeton d'identification actualisé après l'expiration d'un ancien jeton.
  • Un jeton d'ID est rafraîchi de force en appelant currentUser.getIdToken(true) .

Accéder aux revendications personnalisées sur le client

Les revendications personnalisées ne peuvent être récupérées que via le jeton d'identification de l'utilisateur. L'accès à ces revendications peut être nécessaire pour modifier l'interface utilisateur du client en fonction du rôle ou du niveau d'accès de l'utilisateur. Cependant, l'accès au backend doit toujours être appliqué via le jeton d'identification après l'avoir validé et analysé ses revendications. Les revendications personnalisées ne doivent pas être envoyées directement au backend, car elles ne peuvent pas être approuvées en dehors du jeton.

Une fois que les dernières revendications se sont propagées au jeton d'ID d'un utilisateur, vous pouvez les obtenir en récupérant le jeton d'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);
  });

Android

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

Rapide

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

Objectif c

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

Meilleures pratiques pour les revendications personnalisées

Les revendications personnalisées ne sont utilisées que pour fournir un contrôle d'accès. Ils ne sont pas conçus pour stocker des données supplémentaires (telles que le profil et d'autres données personnalisées). Bien que cela puisse sembler être un mécanisme pratique pour le faire, il est fortement déconseillé car ces revendications sont stockées dans le jeton d'identification et peuvent entraîner des problèmes de performances, car toutes les demandes authentifiées contiennent toujours un jeton d'identification Firebase correspondant à l'utilisateur connecté.

  • Utilisez des revendications personnalisées pour stocker des données afin de contrôler l'accès des utilisateurs uniquement. Toutes les autres données doivent être stockées séparément via la base de données en temps réel ou un autre stockage côté serveur.
  • Les revendications personnalisées sont limitées en taille. La transmission d'une charge utile de revendications personnalisées supérieure à 1 000 octets générera une erreur.

Exemples et cas d'utilisation

Les exemples suivants illustrent les revendications personnalisées dans le contexte de cas d'utilisation Firebase spécifiques.

Définition des rôles via Firebase Functions lors de la création d'utilisateurs

Dans cet exemple, des revendications personnalisées sont définies sur un utilisateur lors de sa création à l'aide de Cloud Functions.

Des revendications personnalisées peuvent être ajoutées à l'aide de Cloud Functions et propagées immédiatement avec Realtime Database. La fonction est appelée uniquement lors de l'inscription à l'aide d'un déclencheur onCreate . Une fois les revendications personnalisées définies, elles se propagent à toutes les sessions existantes et futures. La prochaine fois que l'utilisateur se connectera avec les informations d'identification de l'utilisateur, le jeton contiendra les revendications personnalisées.

Implémentation côté client (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);
  }
});

Logique Cloud Functions

Un nouveau nœud de base de données (metadata/($uid)} avec lecture/écriture restreinte à l'utilisateur authentifié est ajouté.

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

Règles de base de données

{
  "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
      }
    }
  }
}

Définir des rôles via une requête HTTP

L'exemple suivant définit des revendications d'utilisateur personnalisées sur un utilisateur nouvellement connecté via une requête HTTP.

Implémentation côté client (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);
});

Implémentation 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' }));
  }
});

Le même flux peut être utilisé lors de la mise à niveau du niveau d'accès d'un utilisateur existant. Prenons par exemple un utilisateur gratuit passant à un abonnement payant. Le jeton d'identification de l'utilisateur est envoyé avec les informations de paiement au serveur principal via une requête HTTP. Lorsque le paiement est traité avec succès, l'utilisateur est défini comme abonné payant via le SDK Admin. Une réponse HTTP réussie est renvoyée au client pour forcer l'actualisation du jeton.

Définition des rôles via un script backend

Un script récurrent (non lancé à partir du client) peut être configuré pour s'exécuter afin de mettre à jour les revendications personnalisées de l'utilisateur :

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

Python

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

Aller

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

Les revendications personnalisées peuvent également être modifiées de manière incrémentielle via le SDK Admin :

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

Python

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)

Aller

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