تكون جلسات Firebase Authentication طويلة الأمد. في كل مرة يسجّل فيها المستخدم الدخول، يتم إرسال بيانات اعتماد المستخدم إلى الخلفية في Firebase Authentication واستبدالها برمز تعريف Firebase (وهو JWT) ورمز مميّز لإعادة التحميل. تكون رموز تعريف Firebase قصيرة الأمد وتستمر لمدة ساعة واحدة، ويمكن استخدام الرمز المميّز لإعادة التحميل لاسترداد رموز تعريف جديدة. تنتهي صلاحية الرموز المميّزة لإعادة التحميل فقط عند حدوث أحد الإجراءات التالية:
- حذف المستخدم
- إيقاف حساب المستخدم
- رصد تغيير كبير في حساب المستخدم ويشمل ذلك أحداثًا مثل تعديلات كلمة المرور أو عنوان البريد الإلكتروني.
توفّر حزمة Firebase Admin SDK إمكانية إبطال الرموز المميّزة لإعادة التحميل لمستخدم محدّد. بالإضافة إلى ذلك، تتوفّر أيضًا واجهة برمجة تطبيقات للتحقّق من إبطال رمز التعريف. باستخدام هذه الإمكانات، يمكنك التحكّم بشكل أكبر في جلسات المستخدمين. توفّر حزمة SDK إمكانية إضافة قيود لمنع استخدام الجلسات في ظروف مريبة، بالإضافة إلى آلية للاسترداد من سرقة الرموز المميزة المحتملة.
إبطال الرموز المميّزة لإعادة التحميل
قد تبطل الرمز المميّز الحالي لإعادة التحميل الخاص بمستخدم عندما يبلّغ عن جهاز مفقود أو مسروق. وبالمثل، إذا اكتشفت ثغرة أمنية عامة أو اشتبهت في حدوث تسريب واسع النطاق للرموز المميّزة النشطة، يمكنك استخدام واجهة برمجة التطبيقات
listUsers
للبحث عن جميع المستخدمين وإبطال رموزهم المميّزة للمشروع المحدّد.
تؤدي عمليات إعادة ضبط كلمة المرور أيضًا إلى إبطال الرموز المميّزة الحالية للمستخدم، ولكن تعالج الخلفية في Firebase Authentication عملية الإبطال تلقائيًا في هذه الحالة. عند الإبطال، يتم تسجيل خروج المستخدم ويُطلب منه إعادة المصادقة.
في ما يلي مثال على عملية تنفيذ تستخدم حزمة Admin SDK لإبطال الرمز المميّز لإعادة التحميل لمستخدم معيّن. لبدء حزمة Admin 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}`);
});
Java
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(f'Tokens revoked at: {revocation_second}')
Go
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 التي تتحقّق من الإبطال بدلاً من استخدام حزمة Admin 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.');
});
Java
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.
لبدء حزمة Admin 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.
}
});
Java
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
Go
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.
}
}
الردّ على إبطال الرمز المميّز على العميل
إذا تم إبطال الرمز المميّز من خلال حزمة Admin 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!' })
});
}
});
});