จัดการเซสชันผู้ใช้

เซสชันการตรวจสอบสิทธิ์ Firebase มีอายุการใช้งานยาวนาน ทุกครั้งที่ผู้ใช้ลงชื่อเข้าใช้ ข้อมูลรับรองของผู้ใช้จะถูกส่งไปยังแบ็กเอนด์การตรวจสอบสิทธิ์ของ Firebase และแลกเปลี่ยนเป็นโทเค็น Firebase ID (JWT) และโทเค็นการรีเฟรช โทเค็น Firebase ID มีอายุสั้นและใช้งานได้นานหนึ่งชั่วโมง โทเค็นการรีเฟรชสามารถใช้เพื่อดึงโทเค็น ID ใหม่ โทเค็นการรีเฟรชจะหมดอายุเมื่อเกิดเหตุการณ์ใดเหตุการณ์หนึ่งต่อไปนี้เท่านั้น:

  • ผู้ใช้ถูกลบ
  • ผู้ใช้ถูกปิดการใช้งาน
  • ตรวจพบการเปลี่ยนแปลงบัญชีที่สำคัญสำหรับผู้ใช้ ซึ่งรวมถึงกิจกรรมต่างๆ เช่น การอัปเดตรหัสผ่านหรือที่อยู่อีเมล

Firebase Admin SDK ให้ความสามารถในการเพิกถอนโทเค็นการรีเฟรชสำหรับผู้ใช้ที่ระบุ นอกจากนี้ยังมี API เพื่อตรวจสอบการเพิกถอนโทเค็น ID ด้วยความสามารถเหล่านี้ คุณสามารถควบคุมเซสชันผู้ใช้ได้มากขึ้น SDK ให้ความสามารถในการเพิ่มข้อจำกัดเพื่อป้องกันไม่ให้มีการใช้เซสชันในสถานการณ์ที่น่าสงสัย ตลอดจนกลไกสำหรับการกู้คืนจากการขโมยโทเค็นที่อาจเกิดขึ้น

เพิกถอนโทเค็นการรีเฟรช

คุณอาจเพิกถอนโทเค็นการรีเฟรชที่มีอยู่ของผู้ใช้เมื่อผู้ใช้รายงานอุปกรณ์ที่สูญหายหรือถูกขโมย ในทำนองเดียวกันถ้าคุณค้นพบช่องโหว่ทั่วไปหรือผู้ต้องสงสัยการรั่วไหลกว้างขนาดของสัญญาณที่ใช้งานคุณสามารถใช้ listUsers API เพื่อค้นหาผู้ใช้และเพิกถอนโทเค็นของพวกเขาสำหรับโครงการที่ระบุ

การรีเซ็ตรหัสผ่านยังเป็นการเพิกถอนโทเค็นที่มีอยู่ของผู้ใช้อีกด้วย อย่างไรก็ตาม แบ็กเอนด์การตรวจสอบสิทธิ์ของ Firebase จะจัดการการเพิกถอนโดยอัตโนมัติในกรณีนั้น ในการเพิกถอน ผู้ใช้จะออกจากระบบและแจ้งให้ตรวจสอบสิทธิ์อีกครั้ง

นี่คือตัวอย่างการใช้งานที่ใช้ Admin 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('Tokens revoked at: {0}'.format(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);

ตรวจจับการเพิกถอนโทเค็น ID

เนื่องจากโทเค็น Firebase ID เป็น JWT ที่ไม่มีสถานะ คุณจึงระบุได้ว่าโทเค็นถูกเพิกถอนแล้วโดยขอสถานะของโทเค็นจากแบ็กเอนด์การตรวจสอบสิทธิ์ของ Firebase เท่านั้น ด้วยเหตุผลนี้ การดำเนินการตรวจสอบบนเซิร์ฟเวอร์ของคุณจึงเป็นการดำเนินการที่มีค่าใช้จ่ายสูง ทำให้ต้องมีการเดินทางไปกลับของเครือข่ายเพิ่มเติม คุณหลีกเลี่ยงการส่งคำขอเครือข่ายนี้ได้โดยการตั้งค่ากฎความปลอดภัยของ Firebase ที่ตรวจสอบการเพิกถอน แทนที่จะใช้ Admin SDK เพื่อตรวจสอบ

ตรวจหาการเพิกถอนโทเค็น ID ในกฎความปลอดภัยของ Firebase

เพื่อให้สามารถตรวจพบการเพิกถอนโทเค็น ID โดยใช้กฎความปลอดภัย เราต้องจัดเก็บข้อมูลเมตาเฉพาะผู้ใช้ก่อน

อัปเดตข้อมูลเมตาเฉพาะผู้ใช้ในฐานข้อมูลเรียลไทม์ของ Firebase

บันทึกการประทับเวลาการเพิกถอนโทเค็นการรีเฟรช สิ่งนี้จำเป็นสำหรับการติดตามการเพิกถอนโทเค็น ID ผ่านกฎความปลอดภัยของ Firebase ซึ่งช่วยให้ตรวจสอบภายในฐานข้อมูลได้อย่างมีประสิทธิภาพ ในตัวอย่างโค้ดด้านล่างใช้ 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

ในการบังคับใช้การตรวจสอบนี้ ให้ตั้งค่ากฎที่ไม่มีสิทธิ์เขียนไคลเอ็นต์เพื่อเก็บเวลาเพิกถอนต่อผู้ใช้ สามารถอัปเดตได้ด้วยการประทับเวลา 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",
      }
    }
  }
}

ข้อมูลใดๆ ที่ต้องมีการเข้าถึงการตรวจสอบสิทธิ์ต้องมีการกำหนดค่ากฎต่อไปนี้ ตรรกะนี้อนุญาตเฉพาะผู้ใช้ที่รับรองความถูกต้องด้วยโทเค็น ID ที่ไม่ถูกเพิกถอนเพื่อเข้าถึงข้อมูลที่ได้รับการป้องกัน:

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

ตรวจหาการเพิกถอนโทเค็น ID ใน SDK

ในเซิร์ฟเวอร์ของคุณ ใช้ตรรกะต่อไปนี้สำหรับการเพิกถอนโทเค็นการรีเฟรชและการตรวจสอบความถูกต้องของโทเค็น ID:

เมื่อโทเค็นรหัสของผู้ใช้จะได้รับการตรวจสอบเพิ่มเติม checkRevoked ธงบูลีนจะต้องมีการส่งผ่านไปยัง verifyIdToken หากโทเค็นของผู้ใช้ถูกเพิกถอน ผู้ใช้ควรออกจากระบบในไคลเอ็นต์หรือขอให้ตรวจสอบสิทธิ์อีกครั้งโดยใช้ API การตรวจสอบสิทธิ์ซ้ำที่จัดเตรียมโดย SDK ไคลเอ็นต์การตรวจสอบสิทธิ์ Firebase

เพื่อเริ่มต้นการดูแลระบบ 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

ไป

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 สำหรับทุกคำขอที่ตรวจสอบสิทธิ์แล้ว ให้ตรวจสอบโทเค็น ID และตรวจสอบว่าที่อยู่ 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!' })
      });
    }
  });
});