Le SDK Firebase Admin permet de définir des attributs personnalisés sur les comptes utilisateur. Vous pouvez ainsi mettre en œuvre différentes stratégies de contrôle d'accès, y compris le contrôle des accès basé sur les rôles, dans les applications Firebase. Ces attributs personnalisés peuvent accorder 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 utilisateur peuvent être définis dans les cas courants suivants :
- Accorder à un utilisateur des droits d'administrateur 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 et non payants.
- Différencier les modérateurs des utilisateurs standards.
- Application pour les enseignants/élèves, etc.
- Ajouter un identifiant supplémentaire à un utilisateur. Par exemple, un utilisateur Firebase peut être associé à 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 en effectuant une recherche dans la base de données sur une liste d'utilisateurs administrateurs. Toutefois, vous pouvez atteindre le même objectif plus efficacement à l'aide de
une revendication utilisateur personnalisée nommée admin avec la règle Realtime Database 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 la valeur admin est définie sur "true" dans leur revendication de jeton
auront un accès en lecture/écriture
au nœud adminContent. Comme le jeton d'ID contient déjà ces assertions, aucun traitement ni recherche supplémentaires ne sont nécessaires pour vérifier les autorisations d'administrateur. De plus, le jeton d'ID est un mécanisme de confiance pour diffuser ces revendications personnalisées. Tous les accès authentifiés doivent valider le jeton d'ID avant de traiter la requête associée.
Les exemples de code et les solutions décrits sur cette page s'appuient à la fois sur les API Firebase Auth côté client et sur les API Auth côté serveur fournies par le SDK Admin.
Définir et valider des 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 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.
Go
// 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 ni nom réservé Firebase. La charge utile ne doit pas dépasser 1 000 octets. Les revendications personnalisées doivent être sérialisables au format JSON. Les types compatibles incluent les chaînes, les nombres, les booléens, les tableaux, les objets et les valeurs nulles. Les types non compatibles tels que Date, undefined, les fonctions ou d'autres valeurs non JSON génèrent des erreurs.
Un jeton d'ID envoyé à un serveur backend 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
Go
// 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'))
Go
// 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 la valeur nulle pour customClaims.
Propager des revendications personnalisées au client
Une fois que les nouvelles revendications ont été 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 l'une des manières suivantes :
- Un utilisateur se connecte ou se réauthentifie après la modification des revendications personnalisées. Le jeton d'ID émis contient les dernières revendications.
- Le jeton d'ID d'une session utilisateur existante est actualisé après l'expiration d'un jeton plus ancien.
- Un jeton d'ID est actualisé de force en appelant
currentUser.getIdToken(true).
Accéder à des revendications personnalisées sur le client
Les revendications personnalisées ne peuvent être récupérées que via le jeton d'ID 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. Toutefois, l'accès au backend doit toujours être appliqué via le jeton d'ID après sa validation et l'analyse de ses revendications. Les revendications personnalisées ne doivent pas être envoyées directement au backend, car elles ne peuvent pas être considérées comme fiables en dehors du jeton.
Une fois que les dernières revendications ont été propagées au jeton d'ID d'un utilisateur, vous pouvez les obtenir en récupérant le jeton d'ID :
JavaScript
import { getAuth } from "firebase/auth";
getAuth().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();
}
}
});
Swift
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()
}
})
Objective-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 concernant les revendications personnalisées
Les revendications personnalisées ne sont utilisées que pour fournir un contrôle d'accès. Elles ne sont pas conçues 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, il est fortement déconseillé, car ces revendications sont stockées dans le jeton d'ID et peuvent entraîner des problèmes de performances, car toutes les requêtes authentifiées contiennent toujours un jeton d'ID Firebase correspondant à l'utilisateur connecté.
- Utilisez des revendications personnalisées pour stocker des données uniquement pour contrôler l'accès des utilisateurs. 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.
- La taille des revendications personnalisées est limitée. La transmission d'une charge utile de revendications personnalisées supérieure à 1 000 octets génère 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éfinir des rôles via des fonctions Firebase lors de la création d'un utilisateur
Dans cet exemple, des revendications personnalisées sont définies sur un utilisateur lors de sa création à l'aide de Cloud Functions.
Les revendications personnalisées peuvent être ajoutées à l'aide de Cloud Functions et propagées immédiatement
avec Realtime Database. La fonction n'est appelée que lors de l'inscription à l'aide d'un déclencheur onCreate. Une fois les revendications personnalisées définies, elles sont propagées à toutes les sessions existantes et futures. La prochaine fois que l'utilisateur se connectera avec ses identifiants, le jeton contiendra les revendications personnalisées.
Implémentation côté client (JavaScript)
import { GoogleAuthProvider, signInWithPopup, getAuth, onAuthStateChanged } from "firebase/auth";
import { getDatabase, onValue, ref } from "firebase/database";
const auth = getAuth();
const database = getDatabase();
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider).catch(error => {
console.log(error);
});
let unsubscribeFn = null;
let metadataRef = null;
onAuthStateChanged(auth, user => {
// Remove previous listener.
if (unsubscribeFn) {
unsubscribeFn();
}
// On user login add new listener.
if (user) {
// Check if refresh is required.
metadataRef = ref(database, 'metadata/' + user.uid + '/refreshTime');
// Subscribe new listener to changes on that node.
unsubscribeFn = onValue(metadataRef, async (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);
});
}
});
Cloud Functions logique
Un nouveau nœud de base de données (metadata/($uid)} avec un accès en lecture/écriture limité à 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 utilisateur personnalisées sur un nouvel utilisateur connecté via une requête HTTP.
Implémentation côté client (JavaScript)
import { GoogleAuthProvider, signInWithPopup, getAuth, onAuthStateChanged } from "firebase/auth";
import { getDatabase, onValue, ref } from "firebase/database";
const auth = getAuth();
const database = getDatabase();
const provider = new GoogleAuthProvider();
signInWithPopup(auth, 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.
auth.currentUser.getIdToken(true);
}
}
});
}).catch((error) => {
console.log(error);
});
Implémentation du backend (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 l'exemple d'un utilisateur sans frais qui passe à un abonnement payant. Le jeton d'ID de l'utilisateur est envoyé avec les informations de paiement au serveur backend via une requête HTTP. Une fois le paiement traité, 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 initié par le 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
})
Go
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)
Go
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 = user.CustomClaims.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
// Add level.
var level = 10;
claims["level"] = level;
// Add custom claims for additional privileges.
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}