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

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

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

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

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

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

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

مثال‌های کد و راه‌حل‌های شرح داده شده در این صفحه، هم از APIهای احراز هویت Firebase سمت کلاینت و هم از APIهای احراز هویت سمت سرور ارائه شده توسط Admin SDK بهره می‌برند.

ادعاهای کاربر سفارشی را از طریق Admin SDK تنظیم و تأیید کنید

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

نود جی اس

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

شیء claim های سفارشی نباید حاوی هیچ نام کلید رزرو شده OIDC یا نام‌های رزرو شده Firebase باشد. حجم payload نباید از ۱۰۰۰ بایت تجاوز کند. claim های سفارشی باید قابلیت سریال‌سازی JSON را داشته باشند. انواع پشتیبانی شده شامل رشته‌ها، اعداد، مقادیر بولی، آرایه‌ها، اشیاء و null هستند. انواع پشتیبانی نشده مانند Date، undefined، توابع یا سایر مقادیر غیر JSON باعث ایجاد خطا می‌شوند.

یک توکن شناسایی که به سرور backend ارسال می‌شود، می‌تواند هویت و سطح دسترسی کاربر را با استفاده از Admin SDK به شرح زیر تأیید کند:

نود جی اس

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

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

نود جی اس

// 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) به زور رفرش می‌شود.

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

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

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

جاوا اسکریپت

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

اندروید

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

سویفت

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

هدف-سی

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 توکن مربوط به کاربر وارد شده هستند.

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

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

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

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

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

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

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

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

منطق 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

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

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

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

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

تعریف نقش‌ها از طریق اسکریپت backend

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

نود جی اس

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 اصلاح شوند:

نود جی اس

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