مدیریت جلسات کاربر

جلسات Firebase Authentication sessions) عمر طولانی دارند. هر بار که کاربر وارد سیستم می‌شود، اطلاعات کاربری به backend Firebase Authentication ارسال شده و با یک توکن شناسه فایربیس (JWT) و توکن تازه‌سازی (refresh token) مبادله می‌شود. توکن‌های شناسه فایربیس عمر کوتاهی دارند و به مدت یک ساعت دوام می‌آورند. توکن تازه‌سازی می‌تواند برای بازیابی توکن‌های شناسه جدید استفاده شود. توکن‌های تازه‌سازی فقط زمانی منقضی می‌شوند که یکی از موارد زیر رخ دهد:

  • کاربر حذف شده
  • کاربر غیرفعال است
  • یک تغییر عمده در حساب کاربری کاربر شناسایی شود. این شامل رویدادهایی مانند به‌روزرسانی رمز عبور یا آدرس ایمیل می‌شود.

کیت توسعه نرم‌افزاری مدیریت فایربیس (Firebase Admin SDK) امکان لغو توکن‌های به‌روزرسانی (refresh tokens) برای یک کاربر مشخص را فراهم می‌کند. علاوه بر این، یک API برای بررسی لغو توکن شناسه (ID token) نیز در دسترس قرار گرفته است. با این قابلیت‌ها، شما کنترل بیشتری بر جلسات کاربر دارید. این کیت توسعه نرم‌افزاری امکان افزودن محدودیت‌هایی را برای جلوگیری از استفاده از جلسات در شرایط مشکوک و همچنین مکانیسمی برای بازیابی از سرقت احتمالی توکن فراهم می‌کند.

لغو توکن‌های به‌روزرسانی

شما می‌توانید توکن به‌روزرسانی موجود یک کاربر را زمانی که کاربر گم شدن یا دزدیده شدن دستگاهش را گزارش می‌دهد، لغو کنید. به طور مشابه، اگر یک آسیب‌پذیری عمومی را کشف کردید یا به نشت گسترده توکن‌های فعال مشکوک شدید، می‌توانید از API listUsers برای جستجوی همه کاربران و لغو توکن‌های آنها برای پروژه مشخص شده استفاده کنید.

بازنشانی رمز عبور همچنین توکن‌های موجود کاربر را لغو می‌کند؛ با این حال، backend Firebase Authentication در این صورت، ابطال را به طور خودکار انجام می‌دهد. در صورت ابطال، کاربر از سیستم خارج شده و از او خواسته می‌شود که دوباره احراز هویت کند.

در اینجا یک نمونه پیاده‌سازی ارائه شده است که از Admin SDK برای لغو توکن به‌روزرسانی یک کاربر مشخص استفاده می‌کند. برای مقداردهی اولیه Admin SDK، دستورالعمل‌های موجود در صفحه تنظیمات را دنبال کنید.

نود جی اس

// 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}`);
  });

جاوا

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);

پایتون

# 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}')

برو

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)

سی شارپ

await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync(uid);
var user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine("Tokens revoked at: " + user.TokensValidAfterTimestamp);

تشخیص ابطال توکن شناسه

از آنجا که توکن‌های Firebase ID، JWTهای بدون وضعیت هستند، می‌توانید تنها با درخواست وضعیت توکن از backend Firebase Authentication ، از ابطال آن اطمینان حاصل کنید. به همین دلیل، انجام این بررسی روی سرور شما یک عملیات پرهزینه است که نیاز به یک رفت و برگشت اضافی در شبکه دارد. می‌توانید با تنظیم Firebase Security Rules که ابطال را بررسی می‌کنند، به جای استفاده از Admin SDK برای انجام این بررسی، از انجام این درخواست شبکه جلوگیری کنید.

تشخیص ابطال توکن شناسه در Firebase Security Rules

برای اینکه بتوانیم با استفاده از قوانین امنیتی، ابطال توکن شناسه را تشخیص دهیم، ابتدا باید برخی از فراداده‌های خاص کاربر را ذخیره کنیم.

به‌روزرسانی فراداده‌های خاص کاربر در Firebase Realtime Database

مهر زمانی ابطال توکن به‌روزرسانی را ذخیره کنید. این برای ردیابی ابطال توکن شناسه از طریق Firebase Security Rules مورد نیاز است. این امر امکان بررسی‌های کارآمد در پایگاه داده را فراهم می‌کند. در نمونه کدهای زیر، از شناسه کاربری و زمان ابطال به‌دست‌آمده در بخش قبل استفاده کنید.

نود جی اس

const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
  console.log('Database updated successfully.');
});

جاوا

DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
Map<String, Object> userData = new HashMap<>();
userData.put("revokeTime", revocationSecond);
ref.setValueAsync(userData);

پایتون

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.

در سرور خود، منطق زیر را برای ابطال توکن refresh و اعتبارسنجی توکن ID پیاده‌سازی کنید:

وقتی قرار است توکن شناسه کاربر تأیید شود، باید پرچم بولی checkRevoked اضافی به verifyIdToken ارسال شود. اگر توکن کاربر لغو شود، باید از کاربر در کلاینت خارج شد یا از او خواسته شود با استفاده از APIهای تأیید اعتبار مجدد ارائه شده توسط SDKهای کلاینت Firebase Authentication دوباره احراز هویت شود.

برای مقداردهی اولیه SDK مدیریت برای پلتفرم خود، دستورالعمل‌های موجود در صفحه تنظیمات را دنبال کنید. نمونه‌هایی از بازیابی توکن شناسه در بخش verifyIdToken آمده است.

نود جی اس

// 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.
    }
  });

جاوا

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.
  }
}

پایتون

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

برو

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)

سی شارپ

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!' })
      });
    }
  });
});