Join us for Firebase Summit on November 10, 2021. Tune in to learn how Firebase can help you accelerate app development, release with confidence, and scale with ease. Register

管理用戶會話

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

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

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

撤銷刷新令牌

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

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

下面是一個示例實現,它使用 Admin SDK 撤銷給定用戶的刷新令牌。要初始化管理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 進行檢查來避免發出此網絡請求。

在數據庫規則中檢測 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})

向數據庫規則添加檢查

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

要初始化管理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!' })
      });
    }
  });
});