Özel Talepler ve Güvenlik Kurallarıyla Erişimi Kontrol Edin

Firebase Admin SDK, kullanıcı hesaplarında özel özelliklerin tanımlanmasını destekler. Bu, Firebase uygulamalarında rol tabanlı erişim kontrolü de dahil olmak üzere çeşitli erişim kontrolü stratejilerinin uygulanmasına olanak sağlar. Bu özel nitelikler, kullanıcılara bir uygulamanın güvenlik kurallarında uygulanan farklı erişim düzeyleri (roller) verebilir.

Kullanıcı rolleri aşağıdaki yaygın durumlar için tanımlanabilir:

  • Verilere ve kaynaklara erişmesi için kullanıcıya yönetici ayrıcalıkları vermek.
  • Bir kullanıcının ait olduğu farklı grupları tanımlama.
  • Çok düzeyli erişim sağlama:
    • Ücretli/ücretsiz abone ayrımı.
    • Moderatörleri normal kullanıcılardan ayıran özellikler.
    • Öğretmen/öğrenci başvurusu vb.
  • Bir kullanıcıya ek bir tanımlayıcı ekleyin. Örneğin, bir Firebase kullanıcısı başka bir sistemdeki farklı bir UID ile eşleşebilir.

"adminContent" veritabanı düğümüne erişimi sınırlamak istediğiniz bir durumu ele alalım. Bunu yönetici kullanıcılar listesindeki bir veritabanı aramasıyla yapabilirsiniz. Ancak admin adlı özel bir kullanıcı talebini aşağıdaki Gerçek Zamanlı Veritabanı kuralıyla kullanarak aynı hedefe daha verimli bir şekilde ulaşabilirsiniz:

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

Özel kullanıcı taleplerine, kullanıcının kimlik doğrulama belirteçleri aracılığıyla erişilebilir. Yukarıdaki örnekte, yalnızca belirteç taleplerinde admin true olarak ayarlandığı kullanıcılar adminContent düğümüne okuma/yazma erişimine sahip olacaktır. Kimlik belirteci zaten bu iddiaları içerdiğinden, yönetici izinlerini kontrol etmek için herhangi bir ek işleme veya aramaya gerek yoktur. Ayrıca kimlik belirteci, bu özel talepleri iletmek için güvenilir bir mekanizmadır. Kimliği doğrulanmış tüm erişimlerin, ilgili isteği işleme koymadan önce kimlik belirtecini doğrulaması gerekir.

Bu sayfada açıklanan kod örnekleri ve çözümler, hem istemci tarafı Firebase Auth API'lerinden hem de Admin SDK tarafından sağlanan sunucu tarafı Auth API'lerinden alınmıştır.

Admin SDK aracılığıyla özel kullanıcı taleplerini ayarlayın ve doğrulayın

Özel talepler hassas veriler içerebileceğinden, yalnızca Firebase Admin SDK tarafından ayrıcalıklı bir sunucu ortamından ayarlanmalıdır.

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.

Gitmek

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

Özel talepler nesnesi, OIDC'ye ayrılmış anahtar adları veya Firebase'e ayrılmış adlar içermemelidir. Özel talep yükü 1000 baytı aşmamalıdır.

Bir arka uç sunucusuna gönderilen bir kimlik belirteci, Yönetici SDK'sını kullanarak kullanıcının kimliğini ve erişim düzeyini aşağıdaki şekilde doğrulayabilir:

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

Gitmek

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

Ayrıca, bir kullanıcının, kullanıcı nesnesinde bir özellik olarak mevcut olan mevcut özel taleplerini de kontrol edebilirsiniz:

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

Gitmek

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

customClaims için null değerini ileterek bir kullanıcının özel taleplerini silebilirsiniz.

Özel talepleri müşteriye yayma

Yönetici SDK'sı aracılığıyla bir kullanıcı üzerinde yeni talepler değiştirildikten sonra, kimlik belirteci aracılığıyla istemci tarafındaki kimliği doğrulanmış bir kullanıcıya aşağıdaki yollarla yayılır:

  • Özel talepler değiştirildikten sonra kullanıcı oturum açar veya yeniden kimlik doğrulaması yapar. Sonuç olarak verilen kimlik belirteci en son talepleri içerecektir.
  • Mevcut bir kullanıcı oturumu, eski bir belirtecin süresi dolduktan sonra kimlik belirtecini yeniler.
  • Bir kimlik belirteci, currentUser.getIdToken(true) çağrılarak zorla yenilenir.

İstemcideki özel taleplere erişin

Özel talepler yalnızca kullanıcının kimlik belirteciyle alınabilir. İstemci kullanıcı arayüzünü kullanıcının rolüne veya erişim düzeyine göre değiştirmek için bu taleplere erişim gerekli olabilir. Ancak arka uç erişimi, kimlik belirteci doğrulandıktan ve talepleri ayrıştırıldıktan sonra her zaman kimlik belirteci aracılığıyla zorunlu kılınmalıdır. Özel talepler, belirtecin dışında güvenilir olamayacağından doğrudan arka uca gönderilmemelidir.

En son talepler kullanıcının kimlik belirtecine iletildikten sonra, kimlik belirtecini alarak bunları alabilirsiniz:

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

Süratli

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

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

Özel hak talepleri için en iyi uygulamalar

Özel talepler yalnızca erişim kontrolü sağlamak için kullanılır. Ek verileri (profil ve diğer özel veriler gibi) depolamak için tasarlanmamışlardır. Bu, bunu yapmak için uygun bir mekanizma gibi görünse de, bu talepler kimlik belirtecinde saklandığından ve kimliği doğrulanmış tüm istekler her zaman oturum açmış kullanıcıya karşılık gelen bir Firebase Kimlik belirtecini içerdiğinden performans sorunlarına neden olabileceğinden kesinlikle önerilmez.

  • Yalnızca kullanıcı erişimini denetlemek amacıyla verileri depolamak için özel talepleri kullanın. Diğer tüm veriler, gerçek zamanlı veritabanı veya diğer sunucu tarafı depolama yoluyla ayrı olarak saklanmalıdır.
  • Özel taleplerin boyutu sınırlıdır. 1000 bayttan büyük bir özel talep yükünün iletilmesi hataya neden olur.

Örnekler ve kullanım örnekleri

Aşağıdaki örnekler, belirli Firebase kullanım durumları bağlamında özel talepleri göstermektedir.

Kullanıcı oluşturmada Firebase İşlevleri aracılığıyla roller tanımlama

Bu örnekte, Cloud Functions kullanılarak oluşturulan kullanıcı üzerinde özel talepler ayarlanır.

Özel talepler Cloud Functions kullanılarak eklenebilir ve Realtime Database ile anında yayılabilir. İşlev yalnızca onCreate tetikleyicisi kullanılarak kayıt sırasında çağrılır. Özel talepler ayarlandıktan sonra mevcut ve gelecekteki tüm oturumlara yayılır. Kullanıcının kullanıcı kimlik bilgileriyle bir sonraki oturum açma işleminde belirteç, özel talepleri içerir.

İstemci tarafı uygulaması (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);
  }
});

Bulut İşlevleri mantığı

Kimliği doğrulanmış kullanıcıyla sınırlı okuma/yazma özelliğine sahip yeni bir veritabanı düğümü (meta veriler/($uid)} eklenir.

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

Veritabanı kuralları

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

Rolleri bir HTTP isteği aracılığıyla tanımlama

Aşağıdaki örnek, bir HTTP isteği aracılığıyla yeni oturum açmış bir kullanıcıyla ilgili özel kullanıcı taleplerini ayarlar.

İstemci tarafı uygulaması (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);
});

Arka uç uygulaması (Yönetici SDK'sı)

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

Mevcut bir kullanıcının erişim düzeyi yükseltilirken aynı akış kullanılabilir. Örneğin, ücretsiz bir kullanıcının ücretli aboneliğe yükseltmesini ele alalım. Kullanıcının kimlik belirteci, ödeme bilgileriyle birlikte bir HTTP isteği aracılığıyla arka uç sunucusuna gönderilir. Ödeme başarıyla işlendiğinde kullanıcı, Yönetici SDK'sı aracılığıyla ücretli abone olarak ayarlanır. Belirteç yenilemeyi zorlamak için istemciye başarılı bir HTTP yanıtı döndürülür.

Arka uç komut dosyası aracılığıyla rolleri tanımlama

Kullanıcının özel taleplerini güncellemek için yinelenen bir komut dosyası (istemciden başlatılmayan) çalışacak şekilde ayarlanabilir:

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

Gitmek

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

Özel talepler ayrıca Yönetici SDK'sı aracılığıyla aşamalı olarak değiştirilebilir:

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)

Gitmek

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