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

Le SDK Firebase Admin prend en charge la définition d'attributs personnalisés sur les comptes d'utilisateurs. Cela offre la possibilité 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 les 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/étudiant, etc.
  • Ajoutez un identifiant supplémentaire sur un utilisateur. Par exemple, un utilisateur de Firebase pourrait mapper vers un UID différent dans un autre système.

Prenons le cas où vous souhaitez limiter l'accès au nœud de base de données « adminContent ». Vous pouvez le faire avec une recherche dans la base de données sur une liste d'utilisateurs administrateurs. Cependant, vous pouvez atteindre le même objectif plus efficacement en utilisant une revendication 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 dont admin est défini sur true dans leur revendication de jeton auraient un accès en lecture/écriture au nœud adminContent . Comme le jeton d'identification 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 constitue un mécanisme fiable permettant de transmettre ces revendications personnalisées. Tout accès authentifié doit valider le jeton d'identification avant de traiter la requête associée.

Les exemples de code et les solutions décrites 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 Admin

Les revendications personnalisées peuvent contenir des données sensibles. Elles ne doivent donc être définies qu'à partir d'un environnement de serveur privilégié par le SDK d'administration Firebase.

Noeud.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 contenir aucun nom de clé réservé OIDC ou nom réservé Firebase . La charge utile des revendications personnalisées ne doit pas dépasser 1 000 octets.

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

Noeud.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 :

Noeud.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 les 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 des manières suivantes :

  • Un utilisateur se connecte ou se réauthentifie une fois les revendications personnalisées modifiées. Le jeton d'identification émis contiendra les dernières réclamations.
  • Une session utilisateur existante voit son jeton d'identification actualisé après l'expiration d'un ancien jeton.
  • Un jeton d'identification est actualisé 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 sont pas fiables en dehors du jeton.

Une fois que les dernières revendications se sont propagées sur le jeton d'identification d'un utilisateur, vous pouvez les obtenir en récupérant le jeton d'identification :

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

Bonnes pratiques pour les revendications personnalisées

Les revendications personnalisées sont uniquement utilisées 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 un mécanisme pratique, il est fortement déconseillé car ces revendications sont stockées dans le jeton d'identification et pourraient entraîner des problèmes de performances, car toutes les requêtes 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 des revendications personnalisées dans le contexte de cas d'utilisation spécifiques de Firebase.

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

Dans cet exemple, les 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 des fonctions Cloud

Un nouveau nœud de base de données (metadata/($uid)} avec une 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 (SDK Admin)

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 backend 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éfinir des rôles via un script backend

Un script récurrent (non lancé par le client) peut être configuré pour être exécuté pour mettre à jour les revendications personnalisées de l'utilisateur :

Noeud.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 :

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