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'utilisateur. 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 les différents groupes auxquels appartient un utilisateur.
  • Fournir un accès à plusieurs niveaux :
    • Différencier les abonnés payants/non payés.
    • Différencier les modérateurs des utilisateurs réguliers.
    • Candidature enseignant/étudiant, etc.
  • Ajouter un identifiant supplémentaire sur un utilisateur. Par exemple, un utilisateur Firebase peut mapper vers 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 obtenir le même objectif plus efficacement à l' aide d' un nom d'utilisateur réclamation personnalisée admin avec les éléments suivants en temps réel règle de base de données:

{
  "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 ensemble à vrai dans leur demande symbolique auraient un accès en lecture / écriture à adminContent nœud. 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 de confiance pour la livraison de ces revendications personnalisées. Tout accès authentifié doit valider le jeton d'identification avant de traiter la demande associée.

Les exemples de code et des solutions décrites dans cette page tirent à la fois les API Firebase côté client et le serveur Auth côté Auth API fourni par le Admin SDK .

Définir et valider les réclamations 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.

admin
  .auth()
  .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 OIDC noms réservés clés ou Firebase noms réservés. 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.
admin
  .auth()
  .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.
admin
  .auth()
  .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 passant nulle pour customClaims .

Propager les revendications personnalisées au client

Une fois que de 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'identification 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 réclamations.
  • Une session utilisateur existante voit son jeton d'identification actualisé après l'expiration d'un jeton plus ancien.
  • Un ID de jeton est la force rafraîchi 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 au 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 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 réclamations personnalisées sont de taille limitée. 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éfinition des rôles via Firebase Functions lors de la création de l'utilisateur

Dans cet exemple, des revendications personnalisées sont définies sur un utilisateur lors de la 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 est appelée uniquement sur inscription en utilisant un onCreate déclencheur. 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 admin = require('firebase-admin');
admin.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 admin.auth().setCustomUserClaims(user.uid, customClaims);

      // Update real-time database to notify client to force refresh.
      const metadataRef = admin.database().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 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 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 admin.auth().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 admin.auth().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 gratuit qui passe à 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 un 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é depuis le client) peut être configuré pour s'exécuter pour mettre à jour les revendications personnalisées de l'utilisateur :

Node.js

admin
  .auth()
  .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 admin.auth().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

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