了解 2023 年 Google I/O 大会上介绍的 Firebase 亮点。了解详情

Quản lý phiên của người dùng

Các phiên Xác thực Firebase đã tồn tại lâu. Mỗi khi người dùng đăng nhập, thông tin đăng nhập của người dùng sẽ được gửi đến phần phụ trợ Xác thực Firebase và được đổi lấy mã thông báo ID Firebase (JWT) và mã làm mới. Mã thông báo ID Firebase tồn tại trong thời gian ngắn và tồn tại trong một giờ; mã làm mới có thể được sử dụng để truy xuất mã thông báo ID mới. Làm mới mã thông báo chỉ hết hạn khi một trong những điều sau xảy ra:

  • Người dùng đã bị xóa
  • Người dùng bị vô hiệu hóa
  • Một thay đổi lớn về tài khoản được phát hiện cho người dùng. Điều 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 quản trị Firebase cung cấp khả năng thu hồi mã thông báo làm mới cho một người dùng được chỉ định. Ngoài ra, một API để kiểm tra việc thu hồi mã thông báo ID cũng được cung cấp. Với những khả năng này, bạn có nhiều quyền kiểm soát hơn đối với các phiên của người dùng. SDK cung cấp khả năng thêm các hạn chế để ngăn các phiên được sử dụng trong các trường hợp đáng ngờ, cũng như cơ chế khôi phục từ hành vi trộm cắp mã thông báo tiềm ẩn.

Thu hồi mã thông báo làm mới

Bạn có thể thu hồi mã thông báo 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 bạn phát hiện ra một lỗ hổng chung hoặc nghi ngờ một sự cố rò rỉ mã thông báo đang hoạt động trên diện rộng, bạn có thể sử dụng API listUsers để 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 đã chỉ định.

Đặt lại mật khẩu cũng thu hồi các mã thông báo hiện có của người dùng; tuy nhiên, phần phụ trợ Xác thực Firebase sẽ tự động xử lý việc thu hồi trong trường hợp đó. Khi thu hồi, người dùng được đăng xuất và được nhắc xác thực lại.

Dưới đây là một ví dụ về triển khai sử dụng SDK quản trị để thu hồi mã thông báo làm mới của một người dùng nhất định. Để khởi chạy SDK quản trị, 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('Tokens revoked at: {0}'.format(revocation_second))

Đi

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 thu hồi mã thông báo ID

Vì mã thông báo ID Firebase là JWT không trạng thái, bạn có thể xác định mã thông báo đã bị thu hồi chỉ bằng cách yêu cầu trạng thái của mã thông báo từ chương trình phụ trợ Xác thực Firebase. Vì lý do này, việc thực hiện kiểm tra này trên máy chủ của bạn là một hoạt động tốn kém, yêu cầu thêm một vòng 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 Quy tắc bảo mật Firebase để kiểm tra việc thu hồi thay vì sử dụng SDK quản trị để thực hiện kiểm tra.

Phát hiện thu hồi mã thông báo ID trong Quy tắc bảo mật Firebase

Để có thể phát hiện việc thu hồi mã thông báo ID bằng Quy tắc bảo mật, trước tiên chúng tôi 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 Cơ sở dữ liệu thời gian thực của Firebase.

Lưu dấu thời gian thu hồi mã thông báo làm mới. Điều này là cần thiết để theo dõi việc thu hồi mã thông báo ID thông qua Quy tắc bảo mật của Firebase. Đ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 sử dụng uid và thời gian thu hồi có đượ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 dấu kiểm vào Quy tắc bảo mật của Firebase

Để thực thi việc kiểm tra này, hãy thiết lập quy tắc không có quyền ghi của ứng dụng khách để lưu trữ thời gian thu hồi cho mỗi người dùng. Điều này có thể được cập nhật với dấu thời gian UTC của thời gian thu hồi cuối cùng như được hiển thị 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",
      }
    }
  }
}

Bất kỳ dữ liệu nào yêu cầu quyền truy cập được xác thực phải có quy tắc sau được định cấu hình. Logic này chỉ cho phép người dùng đã xác thực có mã thông báo ID chưa được 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 thu hồi mã thông báo ID trong SDK.

Trong máy chủ của bạn, hãy triển khai logic sau để thu hồi mã thông báo làm mới và xác thực mã thông báo ID:

Khi mã thông báo ID của người dùng được xác minh, cờ boolean checkRevoked bổ sung phải được chuyển cho verifyIdToken . Nếu mã thông báo của người dùng bị thu hồi, người dùng phải đăng xuất trên máy khách hoặc được yêu cầu xác thực lại bằng cách sử dụng API xác thực lại do SDK ứng dụng khách xác thực Firebase cung cấp.

Để khởi chạy SDK quản trị 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 . Ví dụ về việc truy xuất mã thông báo ID 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

Đi

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 máy khách

Nếu mã thông báo bị thu hồi qua SDK quản trị, khách hàng sẽ được thông báo về việc thu hồi và người dùng sẽ xác thực lại hoặc đă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 giới hạn địa chỉ IP

Một cơ chế bảo mật phổ biến để phát hiện hành vi trộm 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 cuộc gọi), các phiên địa chỉ IP đơn có thể được thực thi. Hoặc, bạn có thể thu hồi mã thông báo của người dùng nếu bạn 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 gốc đáng ngờ.

Để thực hiện kiểm tra bảo mật dựa trên địa chỉ IP, đối với mọi yêu cầu được xác thực, hãy kiểm tra mã thông báo ID 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 đó hoặc nằm trong phạm vi đá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!' })
      });
    }
  });
});