Firebase Authentication 工作階段的生命週期很長。每當使用者登入時,系統會將使用者憑證傳送至 Firebase Authentication 後端,並換取 Firebase ID 權杖 (JWT) 和更新權杖。Firebase ID 權杖的有效期限很短,只有一小時;您可以使用重新整理權杖來擷取新的 ID 權杖。只有在發生下列任一情況時,才會到期:
- 使用者已刪除
- 使用者已停用
- 系統偵測到使用者的帳戶發生重大變更。包括密碼或電子郵件地址更新等事件。
Firebase Admin SDK 可讓您撤銷指定使用者的重新整理權杖。此外,我們也提供 API,可用於檢查 ID 權杖撤銷。有了這些功能,您就能進一步控管使用者工作階段。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);
偵測 ID 權杖撤銷
由於 Firebase ID 權杖是無狀態 JWT,您只能透過從 Firebase Authentication 後端要求權杖狀態,判斷權杖是否已遭到撤銷。因此,在伺服器上執行這項檢查作業的成本較高,需要額外的網路往返作業。您可以設定 Firebase Security Rules 來檢查撤銷,而非使用 Admin SDK 進行檢查,藉此避免發出這項網路要求。
在 Firebase Security Rules 中偵測 ID 權杖撤銷
如要使用安全性規則偵測 ID 權杖撤銷,我們必須先儲存一些使用者專屬的中繼資料。
更新 Firebase Realtime Database 中的使用者專屬中繼資料。
儲存更新權杖的撤銷時間戳記。您需要使用這個欄位,才能透過 Firebase Security Rules 追蹤 ID 權杖撤銷。這可在資料庫中進行有效的檢查。在下方的程式碼範例中,請使用上一節中取得的用戶端 ID 和撤銷時間。
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",
}
}
}
}
任何需要經過驗證存取權的資料都必須設定下列規則。這項邏輯只允許已驗證的使用者存取未撤銷的 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()
)",
}
}
}
}
在 SDK 中偵測 ID 權杖撤銷。
在伺服器中,實作下列邏輯,用於重新整理權杖撤銷和 ID 權杖驗證:
驗證使用者的 ID 權杖時,必須將額外的 checkRevoked
布林旗標傳遞至 verifyIdToken
。如果使用者的符記遭到撤銷,請在用戶端上登出使用者,或要求使用者使用 Firebase Authentication 用戶端 SDK 提供的重新驗證 API 重新驗證。
如要為平台初始化 Admin SDK,請按照設定頁面的操作說明進行。請參閱 verifyIdToken
部分,瞭解如何擷取 ID 權杖。
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 位址執行安全性檢查,請針對每個已驗證的要求檢查 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!' })
});
}
});
});