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

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

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

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

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

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

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

نمونه کدها و راه‌حل‌های شرح‌داده‌شده در این صفحه هم از میاناهای برنامه‌سازی کاربردی Firebase Auth سمت کارخواه و هم از میاناهای برنامه‌سازی کاربردی Auth سمت سرور ارائه‌شده توسط کیت توسعه نرم‌افزار 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 باشند. انواع پشتیبانی‌شده شامل رشته‌ها، اعداد، مقادیر بولی، آرایه‌ها، اشیاء، و null است. انواع پشتیبانی‌نشده مانند Date،‏ undefined، توابع، یا دیگر مقادیر غیر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 SDK اصلاح شد، آن‌ها ازطریق کد شناسه به روش‌های زیر به کاربر اصالت‌سنجی‌شده در سمت کارخواه منتشر می‌شوند:

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

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

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

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

جاوا اسکریپت

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

گره پایگاه داده جدیدی (metadata/($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 تنظیم می‌کند.

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

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 به سرور زیرینه ارسال می‌شود. وقتی پرداخت باموفقیت پردازش شد، کاربر ازطریق Admin SDK به‌عنوان مشترک پولی تنظیم می‌شود. پاسخ 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);
}

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

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