סשנים של Firebase Authentication הם ארוכי טווח. בכל פעם שמשתמש נכנס לחשבון, פרטי הכניסה שלו נשלחים אל קצה העורפי Firebase Authentication ומוחלפים באסימון מזהה של Firebase (JWT) ובאסימון רענון. התוקף של אסימונים מזהים של Firebase הוא קצר – שעה אחת. אפשר להשתמש באסימון הרענון כדי לאחזר אסימונים מזהים חדשים. תוקף אסימוני הרענון פג רק אם מתרחש אחד מהמקרים הבאים:
- המשתמש נמחק
- המשתמש מושבת
- זוהה שינוי משמעותי בחשבון של המשתמש. האירועים האלה כוללים עדכונים של סיסמאות או כתובות אימייל.
Firebase Admin SDK מאפשר לבטל את האפשרות להשתמש בטוקנים לרענון עבור משתמש מסוים. בנוסף, יש גם API לבדיקה של ביטול אסימון מזהה. היכולות האלה מאפשרות לכם יותר שליטה על סשנים של משתמשים. ה-SDK מספק אפשרות להוסיף הגבלות כדי למנוע שימוש בסשנים בנסיבות חשודות, וגם מנגנון לשחזור במקרה של גניבת טוקנים.
ביטול טוקנים לרענון
יכול להיות שתבטלו את אסימון הרענון הקיים של משתמש אם הוא ידווח על מכשיר שאבד או נגנב. באופן דומה, אם אתם מגלים פגיעות כללית או חושדים בדליפה רחבת היקף של אסימונים פעילים, אתם יכולים להשתמש ב-API 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. כך אפשר לבצע בדיקות יעילות במסד הנתונים. בדוגמאות הקוד שלמטה, צריך להשתמש ב-uid ובזמן הביטול שהתקבלו בקטע הקודם.
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
כדי לאכוף את הבדיקה הזו, צריך להגדיר כלל ללא גישת כתיבה ללקוח כדי לאחסן את זמן הביטול לכל משתמש. אפשר לעדכן את הערך הזה באמצעות חותמת הזמן בשעון UTC של זמן הביטול האחרון, כמו בדוגמאות הקודמות:
{
"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
. אם האסימון של המשתמש בוטל, צריך להוציא את המשתמש מהחשבון בלקוח או לבקש ממנו לבצע אימות מחדש באמצעות ממשקי API לאימות מחדש שזמינים בערכות ה-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!' })
});
}
});
});