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

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

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

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

ביטול טוקנים של רענון

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

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

הנה דוגמה להטמעה שמשתמשת ב-SDK לאדמינים כדי לבטל את טוקן הרענון של משתמש נתון. כדי להפעיל את 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 בלי שמירת מצב, אפשר לקבוע אם אסימון בוטל רק על ידי בקשת סטטוס האסימון מה-backend של 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.

כדי להפעיל את 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.
    }
}

תגובה לביטול טוקן בצד הלקוח

אם האסימון מבוטל דרך ה-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!' })
      });
    }
  });
});