Firebase Authentication phiên có thời gian tồn tại lâu. Mỗi khi người dùng đăng nhập, thông tin xác thực của người dùng sẽ được gửi đến phần phụ trợ của tính năng Firebase Authentication và được trao đổi để lấy mã thông báo mã nhận dạng Firebase (JWT) và mã làm mới. Mã thông báo mã nhận dạng Firebase có thời gian tồn tại ngắn và kéo dài trong một giờ; bạn có thể dùng mã làm mới để truy xuất mã thông báo mã nhận dạng mới. Mã làm mới chỉ hết hạn khi một trong những trường hợp sau xảy ra:
- Người dùng bị xoá
- Người dùng bị vô hiệu hoá
- Hệ thống phát hiện thấy một thay đổi lớn về tài khoản của người dùng. Thay đổi này bao gồm các sự kiện như cập nhật mật khẩu hoặc địa chỉ email.
SDK của Firebase dành cho quản trị viên cung cấp khả năng thu hồi mã làm mới cho một người dùng cụ thể. Ngoài ra, một API để kiểm tra việc thu hồi mã thông báo mã nhận dạng cũng được cung cấp. Với những khả năng này, bạn có thể kiểm soát phiên của người dùng nhiều hơn. SDK cung cấp khả năng thêm các quy tắc hạn chế để ngăn việc sử dụng phiên trong các trường hợp đáng ngờ, cũng như cơ chế khôi phục trong trường hợp mã thông báo có thể bị đánh cắp.
Thu hồi mã làm mới
Bạn có thể thu hồi mã làm mới hiện có của người dùng khi người dùng báo cáo thiết bị bị mất hoặc bị đánh cắp. Tương tự, nếu phát hiện thấy một lỗ hổng chung hoặc nghi ngờ có
rò rỉ mã thông báo đang hoạt động trên diện rộng, bạn có thể dùng
listUsers
API để tra cứu tất cả người dùng và thu hồi mã thông báo của họ cho dự án cụ thể.
Việc đặt lại mật khẩu cũng sẽ thu hồi mã thông báo hiện có của người dùng; tuy nhiên, phần phụ trợ của tính năng Firebase Authentication sẽ tự động xử lý việc thu hồi trong trường hợp đó. Khi bị thu hồi, người dùng sẽ bị đăng xuất và được nhắc xác thực lại.
Sau đây là ví dụ về cách triển khai sử dụng SDK dành cho quản trị viên để thu hồi mã làm mới của một người dùng nhất định. Để khởi động SDK dành cho quản trị viên, hãy làm theo hướng dẫn trên trang thiết lập.
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}')
Bắt đầu
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);
Phát hiện việc thu hồi mã thông báo mã nhận dạng
Vì mã thông báo mã nhận dạng Firebase là JWT không trạng thái, nên bạn chỉ có thể xác định mã thông báo đã bị thu hồi bằng cách yêu cầu trạng thái của mã thông báo từ Firebase Authentication phần phụ trợ. Vì lý do này, việc thực hiện quy trình kiểm tra này trên máy chủ của bạn là một thao tác tốn kém, đòi hỏi phải có thêm một vòng khứ hồi mạng. Bạn có thể tránh thực hiện yêu cầu mạng này bằng cách thiết lập Firebase Security Rules để kiểm tra việc thu hồi thay vì dùng SDK dành cho quản trị viên để kiểm tra.
Phát hiện việc thu hồi mã thông báo mã nhận dạng trong Firebase Security Rules
Để có thể phát hiện việc thu hồi mã thông báo mã nhận dạng bằng Quy tắc bảo mật, trước tiên, chúng ta phải lưu trữ một số siêu dữ liệu dành riêng cho người dùng.
Cập nhật siêu dữ liệu dành riêng cho người dùng trong Firebase Realtime Database.
Lưu dấu thời gian thu hồi mã làm mới. Bạn cần làm việc này để theo dõi việc thu hồi mã thông báo mã nhận dạng thông qua Firebase Security Rules. Điều này cho phép kiểm tra hiệu quả trong cơ sở dữ liệu. Trong các mẫu mã bên dưới, hãy dùng uid và thời gian thu hồi thu được trong phần trước.
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})
Thêm quy trình kiểm tra vào Firebase Security Rules
Để thực thi quy trình kiểm tra này, hãy thiết lập một quy tắc không có quyền ghi của ứng dụng để lưu trữ thời gian thu hồi cho mỗi người dùng. Bạn có thể cập nhật quy tắc này bằng dấu thời gian UTC của thời gian thu hồi gần đây nhất như trong các ví dụ trước:
{
"rules": {
"metadata": {
"$user_id": {
// this could be false as it is only accessed from backend or rules.
".read": "$user_id === auth.uid",
".write": "false",
}
}
}
}
Mọi dữ liệu yêu cầu quyền truy cập đã xác thực đều phải được định cấu hình theo quy tắc sau. Logic này chỉ cho phép người dùng đã xác thực có mã thông báo mã nhận dạng chưa bị thu hồi truy cập vào dữ liệu được bảo vệ:
{
"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()
)",
}
}
}
}
Phát hiện việc thu hồi mã thông báo mã nhận dạng trong SDK.
Trong máy chủ của bạn, hãy triển khai logic sau đây để thu hồi mã làm mới và xác thực mã thông báo mã nhận dạng:
Khi cần xác minh mã thông báo mã nhận dạng của người dùng, bạn phải truyền cờ boolean checkRevoked bổ sung vào verifyIdToken. Nếu mã thông báo của người dùng bị
thu hồi, thì người dùng sẽ bị đăng xuất trên ứng dụng hoặc được yêu cầu xác thực lại
bằng các API xác thực lại do Firebase Authentication SDK ứng dụng cung cấp.
Để khởi động SDK dành cho quản trị viên cho nền tảng của bạn, hãy làm theo hướng dẫn trên trang thiết lập
. Các ví dụ về cách truy xuất mã thông báo mã nhận dạng có trong phần
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
Bắt đầu
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.
}
}
Phản hồi việc thu hồi mã thông báo trên ứng dụng
Nếu mã thông báo bị thu hồi thông qua SDK dành cho quản trị viên, thì ứng dụng sẽ được thông báo về việc thu hồi và người dùng dự kiến sẽ xác thực lại hoặc bị đăng xuất:
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.
});
}
Bảo mật nâng cao: Thực thi các quy tắc hạn chế về địa chỉ IP
Một cơ chế bảo mật phổ biến để phát hiện hành vi đánh cắp mã thông báo là theo dõi nguồn gốc địa chỉ IP của yêu cầu. Ví dụ: nếu các yêu cầu luôn đến từ cùng một địa chỉ IP (máy chủ thực hiện lệnh gọi), thì bạn có thể thực thi các phiên địa chỉ IP duy nhất. Hoặc bạn có thể thu hồi mã thông báo của người dùng nếu phát hiện thấy địa chỉ IP của người dùng đột ngột thay đổi vị trí địa lý hoặc bạn nhận được yêu cầu từ một nguồn đáng ngờ.
Để thực hiện quy trình kiểm tra bảo mật dựa trên địa chỉ IP, đối với mọi yêu cầu đã xác thực, hãy kiểm tra mã thông báo mã nhận dạng và kiểm tra xem địa chỉ IP của yêu cầu có khớp với các địa chỉ IP đáng tin cậy trước đó hay nằm trong một dải đáng tin cậy hay không trước khi cho phép truy cập vào dữ liệu bị hạn chế. Ví dụ:
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!' })
});
}
});
});