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

סשנים של 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('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!' })
      });
    }
  });
});