إدارة جلسات المستخدمين

تستغرق Firebase Authentication جلسات وقتًا طويلاً. في كل مرة يقوم فيها أحد المستخدمين بتسجيل الدخول، يتم إرسال بيانات اعتماد المستخدم إلى الواجهة الخلفية Firebase Authentication واستبدالها الرمز المميّز لمعرّف Firebase (a JWT) والرمز المميّز لإعادة التحميل. تكون الرموز المميّزة لمعرّف Firebase قصيرة الأجل ويستمر لمدة ساعة يمكن استخدام الرمز المميز للتحديث لاسترداد رموز المعرفات الجديدة. لا تنتهي صلاحية الرموز المميّزة لإعادة التحميل إلا في الحالات التالية:

  • تم حذف المستخدم.
  • تم إيقاف المستخدم.
  • تم رصد تغيير كبير في حساب المستخدم. ويشمل ذلك أحداثًا مثل تعديلات كلمة المرور أو عنوان البريد الإلكتروني.

توفر SDK لمشرف Firebase إمكانية إبطال الرموز المميزة للتحديث المستخدم المحدد. بالإضافة إلى ذلك، تتوفر أيضًا واجهة برمجة تطبيقات للتحقق من إبطال الرمز المميز للمعرف إتاحتها. باستخدام هذه الإمكانيات، يمكنك التحكم بشكل أكبر في إعدادات الجلسات. توفّر حزمة تطوير البرامج (SDK) إمكانية إضافة قيود لمنع الجلسات. من الاستخدام في ظروف مريبة، بالإضافة إلى توفير آلية للتعافي من سرقة الرموز المميزة المحتملة.

إبطال الرموز المميزة لإعادة التحميل

يمكنك إبطال الرمز المميز الحالي لإعادة التحميل لمستخدم عندما يبلغ المستخدم عن فقدان أو مسروقًا. وبالمثل، إذا اكتشفت ثغرة عامة أو شككت في على نطاق واسع للرموز المميزة النشطة، يمكنك استخدام listUsers واجهة برمجة التطبيقات للبحث عن جميع المستخدمين وإبطال الرموز المميّزة الخاصة بهم للمشروع المحدّد

تؤدي عمليات إعادة تعيين كلمات المرور أيضًا إلى إبطال الرموز المميزة الموجودة للمستخدم؛ في المقابل، وتعالج الواجهة الخلفية Firebase Authentication الإبطال تلقائيًا في هذه الحالة. عند الإبطال، يتم تسجيل خروج المستخدم ويُطالَب بإعادة المصادقة.

في ما يلي مثال على عملية تنفيذ تستخدم "SDK للمشرف" لإبطال إعادة التحميل رمز مميز لمستخدم معين. لإعداد SDK للمشرف، اتبع التعليمات الواردة على صفحة الإعداد.

Node.js

// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
getAuth()
  .revokeRefreshTokens(uid)
  .then(() => {
    return getAuth().getUser(uid);
  })
  .then((userRecord) => {
    return new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
  })
  .then((timestamp) => {
    console.log(`Tokens revoked at: ${timestamp}`);
  });

جافا

FirebaseAuth.getInstance().revokeRefreshTokens(uid);
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
// Convert to seconds as the auth_time in the token claims is in seconds too.
long revocationSecond = user.getTokensValidAfterTimestamp() / 1000;
System.out.println("Tokens revoked at: " + revocationSecond);

Python

# Revoke tokens on the backend.
auth.revoke_refresh_tokens(uid)
user = auth.get_user(uid)
# Convert to seconds as the auth_time in the token claims is in seconds.
revocation_second = user.tokens_valid_after_timestamp / 1000
print('Tokens revoked at: {0}'.format(revocation_second))

انتقال

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
if err := client.RevokeRefreshTokens(ctx, uid); err != nil {
	log.Fatalf("error revoking tokens for user: %v, %v\n", uid, err)
}
// accessing the user's TokenValidAfter
u, err := client.GetUser(ctx, uid)
if err != nil {
	log.Fatalf("error getting user %s: %v\n", uid, err)
}
timestamp := u.TokensValidAfterMillis / 1000
log.Printf("the refresh tokens were revoked at: %d (UTC seconds) ", timestamp)

#C

await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync(uid);
var user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine("Tokens revoked at: " + user.TokensValidAfterTimestamp);

رصد إبطال الرمز المميّز للمعرّف

نظرًا لأن الرموز المميزة لمعرّف Firebase هي رموز JWT بدون حالة، يمكنك تحديد أن الرمز المميز تم إبطالها فقط من خلال طلب حالة الرمز المميّز من Firebase Authentication الخلفية. ولهذا السبب، يُعد إجراء هذا الفحص على الخادم مُكلفًا مما يتطلب جولة إضافية ذهابًا وإيابًا عبر الشبكة. يمكنك تجنب جعل هذا طلب الشبكة من خلال إعداد Firebase Security Rules التي تتحقق من الإبطال بدلاً من استخدام "SDK للمشرف" لإجراء عملية التحقّق.

رصد إبطال الرمز المميّز للمعرّف في Firebase Security Rules

ولكي نتمكّن من رصد إبطال الرمز المميّز للمعرّف باستخدام قواعد الأمان، يجب: بتخزين أولاً بعض بيانات التعريف الخاصة بالمستخدم.

يمكنك تعديل البيانات الوصفية الخاصة بالمستخدم في Firebase Realtime Database.

حفظ الطابع الزمني لإبطال الرمز المميّز لإعادة التحميل. هذا الإجراء مطلوب لتتبُّع الرمز المميّز لرقم التعريف. الإبطال عبر Firebase Security Rules. يتيح هذا إجراء عمليات تحقق فعالة داخل قاعدة البيانات. في عيّنات التعليمات البرمجية أدناه، استخدِم المعرِّف الفريد ووقت الإبطال الذي تم الحصول عليهما في القسم السابق.

Node.js

const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
  console.log('Database updated successfully.');
});

جافا

DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
Map<String, Object> userData = new HashMap<>();
userData.put("revokeTime", revocationSecond);
ref.setValueAsync(userData);

Python

metadata_ref = firebase_admin.db.reference("metadata/" + uid)
metadata_ref.set({'revokeTime': revocation_second})

إضافة شيك إلى Firebase Security Rules

لفرض عملية الفحص هذه، يمكنك إعداد قاعدة بدون إذن وصول للكتابة لأي عميل لتخزين مدة الإبطال لكل مستخدم يمكن تعديل هذه السمة بالطابع الزمني للتوقيت العالمي المتفق عليه وقت آخر الإبطال كما هو موضح في الأمثلة السابقة:

{
  "rules": {
    "metadata": {
      "$user_id": {
        // this could be false as it is only accessed from backend or rules.
        ".read": "$user_id === auth.uid",
        ".write": "false",
      }
    }
  }
}

يجب أن تخضع أي بيانات تتطلب إمكانية وصول تمت مصادقتها للقاعدة التالية تكوينه. يسمح هذا المنطق للمستخدمين الذين تمت مصادقتهم فقط باستخدام معرّف لم يتم إبطاله. للوصول إلى البيانات المحمية:

{
  "rules": {
    "users": {
      "$user_id": {
        ".read": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
        ".write": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
      }
    }
  }
}

يمكنك رصد إبطال الرمز المميّز للمعرّف في حزمة تطوير البرامج (SDK).

في الخادم، نفِّذ المنطق التالي لإبطال الرمز المميّز لإعادة التحميل والتحقق من صحة الرمز المميز للمعرف:

عندما يتم إثبات ملكية رمز مميّز لمعرّف المستخدم، تتم إضافة checkRevoked يجب تمرير العلامة المنطقية إلى verifyIdToken. إذا كان الرمز المميز للمستخدم تم إبطالها، يجب أن يخرج المستخدم من حساب العميل أو يُطلب منه إعادة المصادقة باستخدام واجهات برمجة تطبيقات إعادة المصادقة التي توفّرها حِزم تطوير البرامج (SDK) لعميل "Firebase Authentication"

لإعداد حزمة تطوير البرامج (SDK) الخاصة بالمشرف للمنصّة التي تستخدمها، اتّبِع التعليمات الواردة في . أمثلة على استرداد المعرّف في verifyIdToken.

Node.js

// Verify the ID token while checking if the token is revoked by passing
// checkRevoked true.
let checkRevoked = true;
getAuth()
  .verifyIdToken(idToken, checkRevoked)
  .then((payload) => {
    // Token is valid.
  })
  .catch((error) => {
    if (error.code == 'auth/id-token-revoked') {
      // Token has been revoked. Inform the user to reauthenticate or signOut() the user.
    } else {
      // Token is invalid.
    }
  });

جافا

try {
  // Verify the ID token while checking if the token is revoked by passing checkRevoked
  // as true.
  boolean checkRevoked = true;
  FirebaseToken decodedToken = FirebaseAuth.getInstance()
      .verifyIdToken(idToken, checkRevoked);
  // Token is valid and not revoked.
  String uid = decodedToken.getUid();
} catch (FirebaseAuthException e) {
  if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) {
    // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
  } else {
    // Token is invalid.
  }
}

Python

try:
    # Verify the ID token while checking if the token is revoked by
    # passing check_revoked=True.
    decoded_token = auth.verify_id_token(id_token, check_revoked=True)
    # Token is valid and not revoked.
    uid = decoded_token['uid']
except auth.RevokedIdTokenError:
    # Token revoked, inform the user to reauthenticate or signOut().
    pass
except auth.UserDisabledError:
    # Token belongs to a disabled user record.
    pass
except auth.InvalidIdTokenError:
    # Token is invalid
    pass

انتقال

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
token, err := client.VerifyIDTokenAndCheckRevoked(ctx, idToken)
if err != nil {
	if err.Error() == "ID token has been revoked" {
		// Token is revoked. Inform the user to reauthenticate or signOut() the user.
	} else {
		// Token is invalid
	}
}
log.Printf("Verified ID token: %v\n", token)

#C

try
{
    // Verify the ID token while checking if the token is revoked by passing checkRevoked
    // as true.
    bool checkRevoked = true;
    var decodedToken = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(
        idToken, checkRevoked);
    // Token is valid and not revoked.
    string uid = decodedToken.Uid;
}
catch (FirebaseAuthException ex)
{
    if (ex.AuthErrorCode == AuthErrorCode.RevokedIdToken)
    {
        // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
    }
    else
    {
        // Token is invalid.
    }
}

الرد على إبطال الرمز المميّز للعميل

إذا تم إبطال الرمز المميّز من خلال حزمة تطوير البرامج (SDK) الخاصة بالمشرف، سيتم إعلام العميل إبطالها وتوقُّع أن يعيد المستخدم المصادقة أو يسجّل الخروج:

function onIdTokenRevocation() {
  // For an email/password user. Prompt the user for the password again.
  let password = prompt('Please provide your password for reauthentication');
  let credential = firebase.auth.EmailAuthProvider.credential(
      firebase.auth().currentUser.email, password);
  firebase.auth().currentUser.reauthenticateWithCredential(credential)
    .then(result => {
      // User successfully reauthenticated. New ID tokens should be valid.
    })
    .catch(error => {
      // An error occurred.
    });
}

الأمان المتقدم: فرض قيود على عناوين IP

تتمثل إحدى الآليات الأمنية الشائعة للكشف عن سرقة الرمز المميز في تتبع طلب مصادر عنوان IP. على سبيل المثال، إذا كانت الطلبات تأتي دائمًا من نفس عنوان IP (الخادم الذي يجري الاتصال)، ويمكن لجلسات عنوان IP الفردية فرضها. أو يمكنك إبطال الرمز المميز للمستخدم إذا اكتشفت أن أذونات المستخدم تغير عنوان IP الموقع الجغرافي فجأة، أو إذا تلقيت طلبًا من مصدر مريب.

لإجراء عمليات فحص الأمان استنادًا إلى عنوان IP، لكل طلب تمت مصادقته فحص الرمز المميّز للمعرّف والتحقّق من تطابق عنوان IP للطلب مع الحالة السابقة عناوين IP الموثوق بها أو تقع في نطاق موثوق به قبل السماح بالوصول إلى محدودة. على سبيل المثال:

app.post('/getRestrictedData', (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;
  // Verify the ID token, check if revoked and decode its payload.
  admin.auth().verifyIdToken(idToken, true).then((claims) => {
    // Get the user's previous IP addresses, previously saved.
    return getPreviousUserIpAddresses(claims.sub);
  }).then(previousIpAddresses => {
    // Get the request IP address.
    const requestIpAddress = req.connection.remoteAddress;
    // Check if the request IP address origin is suspicious relative to previous
    // IP addresses. The current request timestamp and the auth_time of the ID
    // token can provide additional signals of abuse especially if the IP address
    // suddenly changed. If there was a sudden location change in a
    // short period of time, then it will give stronger signals of possible abuse.
    if (!isValidIpAddress(previousIpAddresses, requestIpAddress)) {
      // Invalid IP address, take action quickly and revoke all user's refresh tokens.
      revokeUserTokens(claims.uid).then(() => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      }, error => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      });
    } else {
      // Access is valid. Try to return data.
      getData(claims).then(data => {
        res.end(JSON.stringify(data);
      }, error => {
        res.status(500).send({ error: 'Server error!' })
      });
    }
  });
});