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

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

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

توفّر حزمة Firebase Admin SDK إمكانية إبطال علامات إعادة التنشيط لمستخدم محدّد. بالإضافة إلى ذلك، أصبح بالإمكان استخدام واجهة برمجة تطبيقات للتحقّق من إبطال رمز التعريف. باستخدام هذه الإمكانات، يمكنك التحكّم بشكل أكبر في جلسات المستخدِمين. توفّر حزمة تطوير البرامج (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!' })
      });
    }
  });
});