ניהול סשנים של משתמשים

Firebase Authentication סשנים הם תוחלת חיים ארוכה. בכל פעם שמשתמש נכנס לחשבון, פרטי הכניסה שלו נשלחים לקצה העורפי של Firebase Authentication ומומרים לאסימון מזהה של Firebase (JWT) ולאסימון רענון. אסימונים מזהים של Firebase הם לטווח קצר, ונמשכים שעה אחת. אפשר להשתמש באסימון הרענון כדי לאחזר אסימונים מזהים חדשים. אסימוני הרענון יפוג תוקפם רק אם מתרחש אחד מהמקרים הבאים:

  • המשתמש נמחק
  • המשתמש הושבת
  • זוהה שינוי משמעותי בחשבון של המשתמש. המידע הזה כולל אירועים כמו עדכונים של סיסמה או כתובת אימייל.

ה-SDK של Firebase Admin מאפשר לבטל אסימוני רענון של משתמש ספציפי. בנוסף, גם ממשק API לבדיקת ביטול של אסימון מזהה להפוך לזמין. יכולות אלו מאפשרות לכם שליטה רבה יותר על המשתמשים סשנים. ה-SDK מאפשר להוסיף הגבלות כדי למנוע שימוש בסשנים בנסיבות חשודות, וגם מנגנון לשחזור במקרה של גניבת אסימונים פוטנציאלית.

ביטול אסימוני רענון

אפשר לבטל אסימון רענון קיים של משתמש כשהמשתמש מדווח על מכשיר שאבד או נגנב. באופן דומה, אם גיליתם נקודת חולשה כללית או אם אתם חושדים במקרה של דליפה בהיקף רחב של אסימונים פעילים, אפשר להשתמש listUsers API לחיפוש כל המשתמשים ולבטל את האסימונים שלהם לפרויקט שצוין.

איפוס סיסמה מבטל גם אסימונים קיימים של המשתמש. עם זאת, במקרה כזה, הקצה העורפי של 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. כך אפשר לבצע בדיקות יעילות בתוך מסד הנתונים. בדוגמי הקוד שבהמשך, צריך להשתמש ב-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!' })
      });
    }
  });
});