Join us in person and online for Firebase Summit on October 18, 2022. Learn how Firebase can help you accelerate app development, release your app with confidence, and scale with ease. Register now

管理用戶會話

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

Firebase 身份驗證會話是長期存在的。每次用戶登錄時,用戶憑據都會發送到 Firebase 身份驗證後端並交換 Firebase ID 令牌 (JWT) 和刷新令牌。 Firebase ID 令牌是短暫的,持續一個小時;刷新令牌可用於檢索新的 ID 令牌。刷新令牌僅在發生以下情況之一時過期:

  • 用戶被刪除
  • 用戶被禁用
  • 檢測到用戶的主要帳戶更改。這包括密碼或電子郵件地址更新等事件。

Firebase Admin SDK 提供了撤銷指定用戶的刷新令牌的能力。此外,還提供了用於檢查 ID 令牌撤銷的 API。借助這些功能,您可以更好地控制用戶會話。 SDK 提供了添加限制以防止在可疑情況下使用會話的能力,以及從潛在的令牌盜竊中恢復的機制。

撤銷刷新令牌

當用戶報告設備丟失或被盜時,您可以撤銷用戶現有的刷新令牌。同樣,如果您發現一般漏洞或懷疑活動令牌大規模洩漏,您可以使用listUsers API 查找所有用戶並撤銷指定項目的令牌。

密碼重置也會撤銷用戶現有的令牌;但是,在這種情況下,Firebase 身份驗證後端會自動處理撤銷。撤銷時,用戶將退出並提示重新進行身份驗證。

這是一個使用 Admin SDK 撤銷給定用戶的刷新令牌的示例實現。要初始化 Admin SDK,請按照設置頁面上的說明進行操作。

節點.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}`);
  });

爪哇

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))

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 身份驗證後端請求令牌的狀態來確定令牌已被撤銷。因此,在您的服務器上執行此檢查是一項昂貴的操作,需要額外的網絡往返。您可以通過設置檢查撤銷的 Firebase 安全規則來避免發出此網絡請求,而不是使用 Admin SDK 進行檢查。

在 Firebase 安全規則中檢測 ID 令牌吊銷

為了能夠使用安全規則檢測 ID 令牌撤銷,我們必須首先存儲一些用戶特定的元數據。

更新 Firebase 實時數據庫中用戶特定的元數據。

保存刷新令牌撤銷時間戳。這是通過 Firebase 安全規則跟踪 ID 令牌撤銷所必需的。這允許在數據庫中進行有效檢查。在下面的代碼示例中,使用上一節中獲得的 uid 和吊銷時間。

節點.js

const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
  console.log('Database updated successfully.');
});

爪哇

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 安全規則添加檢查

要強制執行此檢查,請設置一個沒有客戶端寫入權限的規則來存儲每個用戶的吊銷時間。這可以使用上次撤銷時間的 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 身份驗證客戶端 SDK 提供的重新身份驗證 API 重新進行身份驗證。

要為您的平台初始化 Admin SDK,請按照設置頁面上的說明進行操作。檢索 ID 令牌的示例在verifyIdToken部分中。

節點.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.
    }
  });

爪哇

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

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!' })
      });
    }
  });
});