管理使用者工作階段

Firebase Authentication 場會議已結束。使用者每次登入後 會將使用者憑證傳送至 Firebase Authentication 後端,然後交換 Firebase ID 權杖 (JWT) 和更新權杖。Firebase ID 權杖的效期較短 然後持續一小時更新憑證可用於擷取新的 ID 符記。 只有在發生下列其中一種情況時,重新整理權杖才會失效:

  • 已刪除使用者
  • 這位使用者已停用
  • 系統偵測到這位使用者有重大帳戶變更。包括密碼或電子郵件地址更新等活動。

Firebase Admin SDK 可讓您撤銷 指定使用者。此外,您也可以使用這個 API 檢查 ID 權杖是否撤銷情形 可用的虛擬機器有了這些功能,您就能進一步掌控 工作階段。SDK 提供新增限制的功能 並在可疑情況下使用不當行為,以及復原機制 避免憑證遭竊。

撤銷更新權杖

當使用者回報遺失或遺失資料, 遭竊。同樣地,如果您發現一般的安全漏洞 因此能大規模洩漏有效權杖 listUsers敬上 查詢所有使用者及撤銷所選專案的權杖。

密碼重設也會撤銷使用者現有的權杖;不過, 在這種情況下,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 中的使用者專屬中繼資料。

儲存更新權杖撤銷時間戳記。你必須完成這個步驟,才能追蹤 ID 權杖 透過 Firebase Security Rules 撤銷。如此一來, 資料庫 在下列程式碼範例中,請使用 上一節

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

如要強制執行這項檢查,請設定沒有用戶端寫入權限的規則來儲存 每位使用者的撤銷時間可用時區以世界標準時間的時間戳記更新 上次撤銷時間 (如上一個範例所示):

{
  "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 Authentication 用戶端 SDK 提供的重新驗證 API。

如要初始化平台適用的 Admin SDK,請按照 設定頁面。擷取 ID 的範例 是否位於 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

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