Firebase Authentication 会话属于长期会话。每次用户登录时,系统都会将用户凭据发送到 Firebase Authentication 后端,用以交换 Firebase ID 令牌 (JWT) 和刷新令牌。Firebase ID 令牌属于短期令牌,有效期只有一个小时;刷新令牌可用来检索新的 ID 令牌。 只有出现以下某种情况时,刷新令牌才会过期:
- 用户被删除
- 用户被停用
- 检测到用户账号出现重大变化,例如密码或电子邮件地址更新等事件。
通过 Firebase Admin SDK,您可以撤消指定用户的刷新令牌。此外,我们还提供了一个用来检查 ID 令牌撤消情况的 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);
检测 ID 令牌撤消
由于 Firebase ID 令牌是无状态的 JWT,您只能通过从 Firebase Authentication 后端请求令牌的状态来确定该令牌是否已撤消。因此,在服务器上执行此类检查是一项代价很高的操作,需要额外的网络往返。您可以设置用于检查撤消的 Firebase Security Rules,而不使用 Admin SDK 进行此类检查,从而避免发出上述网络请求。
在 Firebase Security Rules 中检测 ID 令牌撤消
为了能够使用安全规则来检测 ID 令牌撤消情况,我们必须首先存储一些专门针对特定用户的元数据。
在 Firebase Realtime Database 中更新专门针对特定用户的元数据。
保存刷新令牌的撤消时间戳。在通过 Firebase Security Rules来跟踪 ID 令牌的撤消情况时,需要用到此时间戳。有了该时间戳,您便可在数据库内进行高效的检查。 在下面的代码示例中,使用在上一部分中获得的 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",
}
}
}
}
任何需要通过身份验证才能访问的数据都必须配置以下规则。此逻辑仅允许持有未撤消的 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 令牌时,必须向 verifyIdToken
额外传递一个 checkRevoked
布尔值标志。如果用户的令牌已撤消,应在客户端将用户退出登录,或要求其使用 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!' })
});
}
});
});