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

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

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

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

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

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

การรีเซ็ตรหัสผ่านจะเพิกถอนโทเค็นที่มีอยู่ของผู้ใช้ด้วย อย่างไรก็ตาม แบ็กเอนด์ 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

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

เมื่อโทเค็นรหัสของผู้ใช้ได้รับการยืนยัน 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!' })
      });
    }
  });
});