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