revoke.rs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. //! Per-client token revocation for RFC 7592 `DELETE`.
  2. //!
  3. //! Token records carry no per-client index (the grant set is the only
  4. //! index), so revocation scans the access and refresh keyspaces with
  5. //! `SCAN MATCH <prefix>* COUNT 100`, filters on the record's `client_id`
  6. //! field, and deletes what it owns. Refresh-owned families cascade through
  7. //! [`grants::revoke`]; owned access keys delete directly (plus their grant
  8. //! membership). Tombstoned refresh keys still carry `client_id` and are
  9. //! deleted outright. `DELETE` is an rare administrative call, so a bounded
  10. //! linear scan is proportionate; on Cluster topologies the scan covers the
  11. //! contacted node only (documented limit — Single and Sentinel scan fully).
  12. use std::collections::{HashMap, HashSet};
  13. use crate::error::AppError;
  14. use crate::state::RedisConn;
  15. use crate::store::{Store, StoreError, grants};
  16. /// How many records one revocation pass removed.
  17. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
  18. pub struct RevokedTokens {
  19. /// Access-token records deleted.
  20. pub access: u64,
  21. /// Refresh-token records (live or tombstoned) deleted.
  22. pub refresh: u64,
  23. /// Grant families cascade-revoked.
  24. pub grants: u64,
  25. }
  26. /// Revoke every access and refresh token owned by `client_id`.
  27. ///
  28. /// # Errors
  29. ///
  30. /// Returns a fail-closed store failure when Redis is unavailable.
  31. pub async fn revoke_client_tokens(
  32. conn: &mut RedisConn,
  33. store: &Store,
  34. client_id: &str,
  35. ) -> Result<RevokedTokens, AppError> {
  36. let mut summary = RevokedTokens::default();
  37. let refresh_keys = scan_keys(conn, &store.refresh_key_prefix()).await?;
  38. let mut grant_ids: HashSet<String> = HashSet::new();
  39. let mut owned_refresh: Vec<String> = Vec::new();
  40. for key in refresh_keys {
  41. let fields = read_fields(conn, &key).await?;
  42. if is_owned(&fields, client_id) {
  43. if let Some(grant) = fields.get("grant_id").cloned()
  44. && !grant.is_empty()
  45. {
  46. grant_ids.insert(grant);
  47. }
  48. owned_refresh.push(key);
  49. }
  50. }
  51. for grant_id in &grant_ids {
  52. grants::revoke(conn, store, grant_id)
  53. .await
  54. .map_err(AppError::from)?;
  55. summary.grants += 1;
  56. }
  57. for key in &owned_refresh {
  58. del_key(conn, key).await?;
  59. summary.refresh += 1;
  60. }
  61. let access_keys = scan_keys(conn, &store.access_key_prefix()).await?;
  62. for key in access_keys {
  63. let fields = read_fields(conn, &key).await?;
  64. if is_owned(&fields, client_id) {
  65. if let Some(hash) = key.strip_prefix(&store.access_key_prefix()) {
  66. if let Some(jti_hash) = hash.strip_prefix("jwt:") {
  67. del_key(conn, &store.jwt_marker_key(jti_hash)).await?;
  68. }
  69. if let Some(grant) = fields.get("grant_id") {
  70. let _: () = redis::cmd("SREM")
  71. .arg(store.grant_tokens_key(grant))
  72. .arg(hash)
  73. .query_async(conn)
  74. .await
  75. .map_err(store_mapped)?;
  76. }
  77. }
  78. del_key(conn, &key).await?;
  79. summary.access += 1;
  80. }
  81. }
  82. Ok(summary)
  83. }
  84. /// List every key under `prefix` with a bounded `SCAN` loop.
  85. async fn scan_keys(conn: &mut RedisConn, prefix: &str) -> Result<Vec<String>, AppError> {
  86. let pattern = format!("{prefix}*");
  87. let mut cursor = "0".to_owned();
  88. let mut out = Vec::new();
  89. loop {
  90. let (next, batch): (String, Vec<String>) = redis::cmd("SCAN")
  91. .arg(&cursor)
  92. .arg("MATCH")
  93. .arg(&pattern)
  94. .arg("COUNT")
  95. .arg(100)
  96. .query_async(conn)
  97. .await
  98. .map_err(store_mapped)?;
  99. out.extend(batch);
  100. if next == "0" {
  101. break;
  102. }
  103. cursor = next;
  104. }
  105. Ok(out)
  106. }
  107. /// Read one record's hash fields; a missing key reads as empty.
  108. async fn read_fields(conn: &mut RedisConn, key: &str) -> Result<HashMap<String, String>, AppError> {
  109. redis::cmd("HGETALL")
  110. .arg(key)
  111. .query_async(conn)
  112. .await
  113. .map_err(store_mapped)
  114. }
  115. /// Whether a record's `client_id` field names this client.
  116. fn is_owned(fields: &HashMap<String, String>, client_id: &str) -> bool {
  117. fields
  118. .get("client_id")
  119. .is_some_and(|owner| owner == client_id)
  120. }
  121. /// Delete one key.
  122. async fn del_key(conn: &mut RedisConn, key: &str) -> Result<(), AppError> {
  123. let _: () = redis::cmd("DEL")
  124. .arg(key)
  125. .query_async(conn)
  126. .await
  127. .map_err(store_mapped)?;
  128. Ok(())
  129. }
  130. /// Map a Redis failure onto the fail-closed store policy (transient outages
  131. /// render `503`, everything else is masked).
  132. fn store_mapped(error: redis::RedisError) -> AppError {
  133. AppError::from(StoreError::Redis(error))
  134. }