کنترل دسترسی با ادعاهای سفارشی و قوانین امنیتی

‫Firebase Admin SDK از تعریف مشخصه‌های سفارشی در حساب‌های کاربر پشتیبانی می‌کند. این ویژگی امکان پیاده‌سازی استراتژی‌های مختلف کنترل دسترسی، ازجمله کنترل دسترسی نقش‌مبنا، در برنامه‌های Firebase را فراهم می‌کند. این مشخصه‌های سفارشی می‌توانند سطوح دسترسی مختلفی (نقش‌ها) به کاربران بدهند که در قوانین امنیتی برنامه اعمال می‌شوند.

نقش‌های کاربر را می‌توان برای موارد رایج زیر تعریف کرد:

  • اعطای امتیازهای سرپرست به کاربر برای دسترسی به داده‌ها و منابع.
  • تعریف گروه‌های مختلفی که کاربر به آن‌ها تعلق دارد.
  • ارائه دسترسی چندسطحی:
    • تمایز بین مشترکان پولی/غیرپولی.
    • تمایز بین تعدیل‌گران و کاربران عادی.
    • برنامه معلم/محصل و غیره
  • شناسه دیگری به کاربر اضافه کنید. برای مثال، کاربر Firebase می‌تواند به UID دیگری در سیستم دیگری نگاشت شود.

بیایید موردی را درنظر بگیریم که می‌خواهید دسترسی به گره پایگاه داده «adminContent» را محدود کنید. می‌توانید این کار را با جستجوی پایگاه داده در فهرست کاربران سرپرست انجام دهید. بااین‌حال، می‌توانید بااستفاده از ادعای کاربر سفارشی به‌نام admin با قانون Realtime Database زیر، به همان هدف به‌طور کارآمدتر دست یابید:

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

ادعاهای کاربر سفارشی ازطریق نشان‌های اصالت‌سنجی کاربر دردسترس است. در مثال بالا، فقط کاربرانی که admin در ادعای کدشان روی درست تنظیم شده است به گره adminContent دسترسی خواندن/نوشتن خواهند داشت. ازآنجایی‌که نشان هویت ازقبل حاوی این ادعاها است، برای بررسی اجازه‌های سرپرست، پردازش یا جستجوی اضافی لازم نیست. علاوه‌براین، شناسه وب یک سازوکار مطمئن برای ارائه این ادعاهای سفارشی است. همه دسترسی‌های اصیل‌سازی‌شده باید پیش‌از پردازش درخواست مرتبط، «شناسه نشان» را اعتبارسنجی کنند.

نمونه‌های کد و راه‌حل‌های شرح‌داده‌شده در این صفحه از هر دو میانای برنامه‌سازی کاربردی «احراز هویت Firebase» سمت کارخواه و میانای برنامه‌سازی کاربردی «احراز هویت» سمت سرور ارائه‌شده توسط کیت توسعه نرم‌افزار Admin استفاده می‌کنند.

تنظیم و اعتبارسنجی ادعاهای کاربر سفارشی ازطریق «کیت توسعه نرم‌افزار Admin»

ادعاهای سفارشی می‌تواند حاوی داده‌های حساس باشد، بنابراین باید فقط از محیط سرور ممتاز توسط Firebase Admin SDK تنظیم شود.

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

جاوا

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

پایتون

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

رفتن

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

سی شارپ

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

شیء ادعاهای سفارشی نباید حاوی هیچ‌یک از نام‌های کلید رزروشده OIDC یا نام‌های رزروشده Firebase باشد. پایه‌بار نباید از ۱۰۰۰ بایت بیشتر باشد. ادعاهای سفارشی باید JSON-serializable باشند. انواع پشتیبانی‌شده شامل رشته‌ها، اعداد، بولی‌ها، آرایه‌ها، اشیا، و تهی است. انواع پشتیبانی‌نشده مثل «تاریخ»، «تعریف‌نشده»، «توابع»، یا مقادیر غیرJSON باعث بروز خطا می‌شوند.

کد شناسایی ارسال‌شده به سرور پشتیبان می‌تواند هویت کاربر و سطح دسترسی او را بااستفاده از «کیت توسعه نرم‌افزار سرپرست» به‌صورت زیر تأیید کند:

Node.js

// Verify the ID token first.
getAuth()
  .verifyIdToken(idToken)
  .then((claims) => {
    if (claims.admin === true) {
      // Allow access to requested admin resource.
    }
  });

جاوا

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

پایتون

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

رفتن

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

سی شارپ

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

همچنین می‌توانید ادعاهای سفارشی موجود کاربر را که به‌عنوان یک دارایی در شیء کاربر دردسترس است بررسی کنید:

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

جاوا

// Lookup the user associated with the specified uid.
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
System.out.println(user.getCustomClaims().get("admin"));

پایتون

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

رفتن

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

سی شارپ

// Lookup the user associated with the specified uid.
UserRecord user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine(user.CustomClaims["admin"]);

می‌توانید ادعاهای سفارشی کاربر را با ارسال مقدار null برای customClaims حذف کنید.

ادعاهای سفارشی را به مشتری منتقل کنید

پس‌از اینکه ادعاهای جدید در کاربری ازطریق «کیت توسعه نرم‌افزار Admin» اصلاح شد، این ادعاها ازطریق کد شناسایی به کاربر اصالت‌سنجی‌شده در سمت کارخواه به روش‌های زیر منتشر می‌شود:

  • کاربر پس‌از اصلاح ادعاهای سفارشی وارد سیستم می‌شود یا دوباره اصالت‌سنجی می‌کند. شناسه نشانی که درنتیجه صادر می‌شود حاوی جدیدترین ادعاها خواهد بود.
  • جلسه کاربر موجود پس‌از انقضای کد قدیمی‌تر، کد شناسایی خود را تازه‌سازی می‌کند.
  • کد شناسایی با فراخوانی currentUser.getIdToken(true) به‌اجبار بازآوری می‌شود.

دسترسی به ادعاهای سفارشی در کارخواه

ادعاهای سفارشی فقط ازطریق شناسه رمز کاربر قابل‌بازیابی است. دسترسی به این ادعاها ممکن است برای اصلاح رابط کاربری مشتری براساس نقش کاربر یا سطح دسترسی ضروری باشد. بااین‌حال، دسترسی به زیرینه باید همیشه ازطریق توکن ID پس‌از اعتبارسنجی و تجزیه ادعاهای آن اعمال شود. ادعاهای سفارشی نباید مستقیماً به زیرینه ارسال شوند، زیرا خارج از رمز قابل اعتماد نیستند.

پس‌از اینکه جدیدترین ادعاها به شناسه رمز کاربر منتقل شد، می‌توانید آن‌ها را با بازیابی کردن شناسه رمز دریافت کنید:

جاوا اسکریپت

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

روال‌های مطلوب برای ادعاهای سفارشی

از ادعاهای سفارشی فقط برای ارائه کنترل دسترسی استفاده می‌شود. این برچسب‌ها برای ذخیره داده‌های اضافی (مثل نمایه و سایر داده‌های سفارشی) طراحی نشده‌اند. اگرچه این ممکن است به‌نظر سازوکار مناسبی برای انجام این کار برسد، اما به‌شدت توصیه می‌شود که از آن استفاده نکنید زیرا این ادعاها در کد شناسه ذخیره می‌شوند و می‌توانند باعث بروز مشکلات عملکرد شوند زیرا همه درخواست‌های اصالت‌سنجی‌شده همیشه حاوی کد شناسه Firebase مربوط به کاربر واردشده به سیستم هستند.

  • از ادعاهای سفارشی برای ذخیره داده‌ها فقط به‌منظور کنترل دسترسی کاربر استفاده کنید. همه داده‌های دیگر باید به‌صورت جداگانه ازطریق پایگاه داده هم‌زمان یا فضای ذخیره‌سازی سمت سرور دیگر ذخیره شوند.
  • اندازه ادعاهای سفارشی محدود است. گذراندن بار داده ادعاهای سفارشی بزرگ‌تر از ۱۰۰۰ بایت باعث ایجاد خطا می‌شود.

مثال‌ها و موارد استفاده

مثال‌های زیر ادعاهای سفارشی را در بافت موارد استفاده خاص Firebase نشان می‌دهند.

تعریف نقش‌ها ازطریق «توابع Firebase» در ایجاد کاربر

در این مثال، ادعاهای سفارشی هنگام ایجاد کاربر بااستفاده از Cloud Functions تنظیم می‌شوند.

ادعاهای سفارشی را می‌توان بااستفاده از Cloud Functions اضافه کرد و بلافاصله با Realtime Database منتشر کرد. این تابع فقط در زمان ثبت‌نام بااستفاده از یک onCreate راه‌انداز فراخوانده می‌شود. پس‌از تنظیم ادعاهای سفارشی، این ادعاها به همه جلسات موجود و آینده منتقل می‌شوند. دفعه بعدی که کاربر با اطلاعات اعتباری کاربر به سیستم وارد می‌شود، کد حاوی ادعاهای سفارشی است.

پیاده‌سازی سمت کارخواه (جاوا اسکریپت)

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

گره پایگاه داده جدیدی (فراداده/($uid)} با دسترسی خواندن/نوشتن محدود به کاربر اصیل‌سازی‌شده اضافه می‌شود.

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

قوانین پایگاه داده

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

تعریف نقش‌ها ازطریق درخواست HTTP

مثال زیر ادعاهای کاربر سفارشی را روی کاربری که به‌تازگی ازطریق درخواست HTTP به سیستم وارد شده است تنظیم می‌کند.

پیاده‌سازی سمت کارخواه (جاوا اسکریپت)

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

پیاده‌سازی زیرینه (سرپرست 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' }));
  }
});

از همین جریان می‌توان برای ارتقا دادن سطح دسترسی کاربر موجود استفاده کرد. برای مثال، کاربری رایگان را درنظر بگیرید که به اشتراک پولی ارتقا می‌دهد. شناسه کاربر توکن همراه با اطلاعات پرداخت ازطریق درخواست HTTP به سرور زیرینه ارسال می‌شود. وقتی پرداخت باموفقیت پردازش شد، کاربر ازطریق «کیت توسعه نرم‌افزار سرپرست» به‌عنوان مشترک پولی تنظیم می‌شود. پاسخ موفق HTTP به کاربر برگردانده می‌شود تا بازآوری کد اجباری شود.

تعریف نقش‌ها ازطریق دستورگان زیرینه

می‌توان اسکریپت تکرارشونده‌ای (که ازسوی کارخواه آغاز نشده است) تنظیم کرد تا برای به‌روزرسانی ادعاهای سفارشی کاربر اجرا شود:

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

جاوا

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

پایتون

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

رفتن

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

}

سی شارپ

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

ادعاهای سفارشی را می‌توان ازطریق «کیت توسعه نرم‌افزار سرپرست» به‌صورت افزایشی اصلاح کرد:

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

جاوا

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

پایتون

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)

رفتن

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

}

سی شارپ

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