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

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

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

«کیت توسعه نرم‌افزار Firebase Admin» امکان لغو کردن کدهای بازآوری را برای کاربر مشخصی فراهم می‌کند. علاوه‌براین، یک API برای بررسی فسخ کد شناسایی نیز دردسترس قرار می‌گیرد. با این قابلیت‌ها، کنترل بیشتری روی جلسه‌های کاربر دارید. «کیت توسعه نرم‌افزار» امکان افزودن محدودیت‌هایی را برای جلوگیری از استفاده از جلسه‌ها در شرایط مشکوک، و همچنین سازوکاری برای بازیابی از سرقت احتمالی رمز ارائه می‌دهد.

ابطال کردن کدهای بازآوری

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

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

در اینجا یک پیاده‌سازی نمونه وجود دارد که از «کیت توسعه نرم‌افزار سرپرست» برای لغو کردن توکن بازآوری کاربر داده‌شده استفاده می‌کند. برای مقداردهی اولیه «کیت توسعه نرم‌افزار سرپرست»، دستورالعمل‌های صفحه راه‌اندازی را دنبال کنید.

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

جاوا

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 از نوع JWT بدون حالت هستند، تنها با درخواست وضعیت کد از زیرینه Firebase Authentication می‌توانید تشخیص دهید که کد باطل شده است یا نه. به همین دلیل، انجام این بررسی در سرور شما عملیاتی پرهزینه است که به سفر رفت‌وبرگشت شبکه اضافی نیاز دارد. می‌توانید با راه‌اندازی Firebase Security Rules که به‌جای استفاده از «کیت توسعه نرم‌افزار سرپرست» برای بررسی، ابطال را بررسی می‌کند، از انجام این درخواست شبکه جلوگیری کنید.

شناسایی ابطال کد شناسایی در 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.');
});

جاوا

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()
        )",
      }
    }
  }
}

لغو کردن کد شناسایی را در کیت توسعه نرم‌افزار تشخیص دهید.

در سرورتان، منطق زیر را برای ابطال ژتون بازآوری و اعتبارسنجی ژتون شناسایی پیاده‌سازی کنید:

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

برای مقداردهی اولیه «کیت توسعه نرم‌افزار سرپرست» برای پلاتفرم خود، دستورالعمل‌های صفحه راه‌اندازی را دنبال کنید. نمونه‌هایی از بازیابی شناسه رمز در بخش 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.
    }
  });

جاوا

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

پاسخ به لغو کد در کارخواه

اگر نشان ازطریق «کیت توسعه نرم‌افزار سرپرست» باطل شود، به مشتری اطلاع داده می‌شود که نشان باطل شده است و از کاربر انتظار می‌رود دوباره اصالت‌سنجی کند یا از سیستم خارج شود:

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