管理使用者工作階段

Firebase 驗證工作階段的效期較長。使用者每次登入時,系統會將使用者憑證傳送至 Firebase 驗證後端,然後交換 Firebase ID 權杖 (JWT) 和更新權杖。Firebase ID 權杖的效期很短,且會持續一小時;更新權杖可用來擷取新的 ID 權杖。只有在發生下列其中一種情況時,重新整理權杖才會失效:

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

Firebase Admin SDK 可讓您撤銷指定使用者的更新憑證。此外,您也可以使用 API 查看可撤銷 ID 權杖撤銷情況的 API。藉助這些功能,您可以進一步掌控使用者工作階段。SDK 可讓您加入限制,防止工作階段在可疑情況下被使用,以及從潛在權杖遭竊復原的機制。

撤銷更新權杖

當使用者回報遺失或遭竊的裝置時,您可以撤銷使用者現有的更新權杖。同樣地,如果您發現一般的安全漏洞,或懷疑有效權杖大規模外洩,可以使用 listUsers API 查詢所有使用者,並撤銷指定專案的權杖。

密碼重設也會撤銷使用者現有的權杖,但 Firebase 驗證後端會自動處理撤銷作業。撤銷時,系統會將使用者登出,並提示重新驗證。

以下實作範例使用 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 驗證後端要求權杖狀態,才能判斷憑證是否已撤銷。因此,在伺服器執行這項檢查會耗費大量資源,且需要額外的網路來回行程。您可以設定 Firebase 安全性規則來檢查撤銷 (而非使用 Admin SDK 檢查),避免發出這項網路要求。

在 Firebase 安全性規則中偵測 ID 權杖撤銷作業

為了能夠使用安全性規則偵測 ID 權杖撤銷情況,我們必須先儲存一些使用者專屬的中繼資料。

更新 Firebase 即時資料庫中的使用者專屬中繼資料。

儲存更新權杖撤銷時間戳記。如此一來,才能透過 Firebase 安全性規則追蹤 ID 憑證撤銷作業。這有助於在資料庫內有效率地檢查。在下列程式碼範例中,請使用上一節取得的 uid 和撤銷時間。

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

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

{
  "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,請按照設定頁面中的指示操作。verifyIdToken 區段提供擷取 ID 符記的範例。

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