管理使用者工作階段

Firebase Authentication 工作階段的存續時間較長。每當使用者登入時,系統會將使用者憑證傳送至 Firebase Authentication 後端,並換取 Firebase ID 權杖 (JWT) 和重新整理權杖。Firebase ID 權杖的效期很短,只有一小時;您可以使用重新整理權杖擷取新的 ID 權杖。只有在發生下列任一情況時,重新整理權杖才會過期:

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

Firebase Admin SDK 可撤銷指定使用者的重新整理權杖。此外,我們也提供 API,可檢查 ID 權杖是否已遭撤銷。有了這些功能,您就能進一步控管使用者工作階段。SDK 可新增限制,防止工作階段在可疑情況下遭到使用,並提供機制,在權杖可能遭竊時進行復原。

撤銷更新權杖

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

密碼重設也會撤銷使用者現有的權杖,但 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 中更新使用者專屬中繼資料。

儲存更新權杖撤銷時間戳記。這是透過 Firebase Security Rules 追蹤 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 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!' })
      });
    }
  });
});