Firebase Authentication เซสชันมีอายุการใช้งานยาวนาน ทุกครั้งที่ผู้ใช้ลงชื่อเข้าใช้ ระบบจะส่งข้อมูลเข้าสู่ระบบของผู้ใช้ไปยังFirebase Authenticationแบ็กเอนด์ และแลกเป็น โทเค็นรหัส Firebase (JWT) และโทเค็นการรีเฟรช โทเค็นรหัส Firebase มีอายุการใช้งานสั้นและมีอายุ 1 ชั่วโมง ส่วนโทเค็นการรีเฟรชสามารถใช้เพื่อดึงโทเค็นรหัสใหม่ได้ โทเค็นการรีเฟรชจะหมดอายุเมื่อเกิดเหตุการณ์ใดเหตุการณ์หนึ่งต่อไปนี้เท่านั้น
- ผู้ใช้ถูกลบ
- ผู้ใช้ถูกปิดใช้งาน
- ตรวจพบการเปลี่ยนแปลงที่สำคัญในบัญชีของผู้ใช้ ซึ่งรวมถึงเหตุการณ์ต่างๆ เช่น การอัปเดตรหัสผ่านหรืออีเมล
Firebase Admin SDK มีความสามารถในการเพิกถอนโทเค็นการรีเฟรชของผู้ใช้ที่ระบุ นอกจากนี้ เรายังได้จัดเตรียม API สำหรับตรวจสอบการเพิกถอนโทเค็นรหัสด้วย ความสามารถเหล่านี้จะช่วยให้คุณควบคุมเซสชันของผู้ใช้ได้มากขึ้น SDK มีความสามารถในการเพิ่มข้อจำกัดเพื่อป้องกันไม่ให้มีการใช้เซสชันในสถานการณ์ที่น่าสงสัย รวมถึงกลไกการกู้คืนจากการโจรกรรมโทเค็นที่อาจเกิดขึ้น
เพิกถอนโทเค็นการรีเฟรช
คุณอาจเพิกถอนโทเค็นการรีเฟรชที่มีอยู่ของผู้ใช้เมื่อผู้ใช้รายงานว่าอุปกรณ์สูญหายหรือถูกขโมย ในทำนองเดียวกัน หากคุณพบช่องโหว่ทั่วไปหรือสงสัยว่ามีการรั่วไหลของโทเค็นที่ใช้งานอยู่อย่างกว้างขวาง คุณสามารถใช้ API เพื่อค้นหาผู้ใช้ทั้งหมดและเพิกถอนโทเค็นของผู้ใช้สำหรับโปรเจ็กต์ที่ระบุlistUsers
การรีเซ็ตรหัสผ่านจะเพิกถอนโทเค็นที่มีอยู่ของผู้ใช้ด้วย อย่างไรก็ตาม 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(f'Tokens revoked at: {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
ในเซิร์ฟเวอร์ ให้ใช้ตรรกะต่อไปนี้สำหรับการเพิกถอนโทเค็นการรีเฟรชและการตรวจสอบโทเค็นรหัส
เมื่อต้องตรวจสอบโทเค็นรหัสของผู้ใช้ คุณต้องส่งแฟล็กบูลีน checkRevoked เพิ่มเติมไปยัง verifyIdToken หากโทเค็นของผู้ใช้ถูก
เพิกถอน ระบบควรนำผู้ใช้ออกจากระบบในไคลเอ็นต์หรือขอให้ผู้ใช้ตรวจสอบสิทธิ์อีกครั้ง
โดยใช้ API การตรวจสอบสิทธิ์อีกครั้งที่จัดเตรียมโดย Firebase Authentication Client SDK
หากต้องการเริ่มต้นใช้งาน 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!' })
});
}
});
});