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

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

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

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

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

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

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

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

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

ادعاهای سفارشی می‌توانند حاوی داده‌های حساس باشند، بنابراین فقط باید از یک محیط سرور ممتاز توسط 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 باشد. حجم مطالبات سفارشی نباید از 1000 بایت تجاوز کند.

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

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، آنها از طریق شناسه شناسه به روش های زیر به یک کاربر تأیید شده در سمت سرویس گیرنده منتشر می شوند:

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

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

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

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

جاوا اسکریپت

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

هدف-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];
    }
  }
}];

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

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

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

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

مثال‌های زیر ادعاهای سفارشی را در زمینه موارد استفاده خاص 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);
});

پیاده سازی Backend (Admin 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);
}