Join us for Firebase Summit on November 10, 2021. Tune in to learn how Firebase can help you accelerate app development, release with confidence, and scale with ease. Register

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

เซสชันการตรวจสอบสิทธิ์ 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 ในกฎของฐานข้อมูล

เพื่อให้สามารถตรวจจับการเพิกถอนโทเค็น 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})

เพิ่มการตรวจสอบกฎฐานข้อมูล

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