cascade.rs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. //! Revocation semantics: access-only revoke, refresh cascade, no-op cases.
  2. use axum::http::StatusCode;
  3. use http_body_util::BodyExt as _;
  4. use serde_json::Value;
  5. use super::{TestError, TestResult, mint_request, now_unix, service, test_redis, unique};
  6. use crate::{
  7. protocol::revoke::{
  8. HINT_ACCESS, HINT_REFRESH, OwnedToken, RevokeOutcome, outcome_response, revoke_owned,
  9. },
  10. store::grants,
  11. };
  12. /// Revoking an access token removes it but leaves its sibling refresh token.
  13. #[tokio::test]
  14. async fn revoke_access_token_removes_only_it() -> TestResult {
  15. let (mut conn, store, _prefixes) = test_redis().await?;
  16. let service = service()?;
  17. let client = unique("client");
  18. let minted = service
  19. .mint(
  20. &mut conn,
  21. &store,
  22. &mint_request(&client, &unique("grant")),
  23. now_unix(),
  24. )
  25. .await?;
  26. let outcome = revoke_owned(
  27. &mut conn,
  28. &store,
  29. &service,
  30. &OwnedToken {
  31. owner_client_id: &client,
  32. token: &minted.access_token,
  33. token_type_hint: Some(HINT_ACCESS),
  34. },
  35. None,
  36. )
  37. .await?;
  38. if outcome != RevokeOutcome::Revoked {
  39. return Err(TestError::Unexpected(
  40. "owned access token must be revoked".to_owned(),
  41. ));
  42. }
  43. if service
  44. .find_access(&mut conn, &store, &minted.access_token)
  45. .await?
  46. .is_some()
  47. {
  48. return Err(TestError::Unexpected(
  49. "revoked access token must no longer resolve".to_owned(),
  50. ));
  51. }
  52. if service
  53. .find_refresh(&mut conn, &store, &minted.refresh_token)
  54. .await?
  55. .is_none()
  56. {
  57. return Err(TestError::Unexpected(
  58. "access-only revocation must leave the refresh token".to_owned(),
  59. ));
  60. }
  61. Ok(())
  62. }
  63. /// Revoking a refresh token cascades to the whole grant family.
  64. #[tokio::test]
  65. async fn revoke_refresh_token_cascades_to_grant_chain() -> TestResult {
  66. let (mut conn, store, _prefixes) = test_redis().await?;
  67. let service = service()?;
  68. let client = unique("client");
  69. let grant = unique("grant");
  70. let minted = service
  71. .mint(
  72. &mut conn,
  73. &store,
  74. &mint_request(&client, &grant),
  75. now_unix(),
  76. )
  77. .await?;
  78. let outcome = revoke_owned(
  79. &mut conn,
  80. &store,
  81. &service,
  82. &OwnedToken {
  83. owner_client_id: &client,
  84. token: &minted.refresh_token,
  85. token_type_hint: Some(HINT_REFRESH),
  86. },
  87. None,
  88. )
  89. .await?;
  90. if outcome != RevokeOutcome::Revoked {
  91. return Err(TestError::Unexpected(
  92. "owned refresh token must be revoked".to_owned(),
  93. ));
  94. }
  95. if service
  96. .find_access(&mut conn, &store, &minted.access_token)
  97. .await?
  98. .is_some()
  99. || service
  100. .find_refresh(&mut conn, &store, &minted.refresh_token)
  101. .await?
  102. .is_some()
  103. {
  104. return Err(TestError::Unexpected(
  105. "refresh revocation must kill the chain".to_owned(),
  106. ));
  107. }
  108. if !grants::members(&mut conn, &store, &grant).await?.is_empty() {
  109. return Err(TestError::Unexpected(
  110. "refresh revocation must drop the token index".to_owned(),
  111. ));
  112. }
  113. Ok(())
  114. }
  115. /// An unknown token is a silent `200` no-op.
  116. #[tokio::test]
  117. async fn revoke_unknown_token_is_noop_200() -> TestResult {
  118. let (mut conn, store, _prefixes) = test_redis().await?;
  119. let service = service()?;
  120. let outcome = revoke_owned(
  121. &mut conn,
  122. &store,
  123. &service,
  124. &OwnedToken {
  125. owner_client_id: "client-a",
  126. token: "no-such-token",
  127. token_type_hint: None,
  128. },
  129. None,
  130. )
  131. .await?;
  132. if outcome != RevokeOutcome::NoOp {
  133. return Err(TestError::Unexpected(
  134. "unknown token must be a no-op".to_owned(),
  135. ));
  136. }
  137. if outcome_response(outcome).status() != StatusCode::OK {
  138. return Err(TestError::Unexpected("no-op must render 200".to_owned()));
  139. }
  140. Ok(())
  141. }
  142. /// Another client's token is a no-op and is never revoked (no cross-client).
  143. #[tokio::test]
  144. async fn revoke_foreign_token_is_noop_and_untouched() -> TestResult {
  145. let (mut conn, store, _prefixes) = test_redis().await?;
  146. let service = service()?;
  147. let owner = unique("owner");
  148. let attacker = unique("attacker");
  149. let minted = service
  150. .mint(
  151. &mut conn,
  152. &store,
  153. &mint_request(&owner, &unique("grant")),
  154. now_unix(),
  155. )
  156. .await?;
  157. let outcome = revoke_owned(
  158. &mut conn,
  159. &store,
  160. &service,
  161. &OwnedToken {
  162. owner_client_id: &attacker,
  163. token: &minted.access_token,
  164. token_type_hint: Some(HINT_ACCESS),
  165. },
  166. None,
  167. )
  168. .await?;
  169. if outcome != RevokeOutcome::NoOp {
  170. return Err(TestError::Unexpected(
  171. "a foreign token must be a no-op".to_owned(),
  172. ));
  173. }
  174. if service
  175. .find_access(&mut conn, &store, &minted.access_token)
  176. .await?
  177. .is_none()
  178. {
  179. return Err(TestError::Unexpected(
  180. "a foreign client must never revoke another client's token".to_owned(),
  181. ));
  182. }
  183. Ok(())
  184. }
  185. /// An unsupported `token_type_hint` is `400 unsupported_token_type`.
  186. #[tokio::test]
  187. async fn revoke_unsupported_hint_is_rejected() -> TestResult {
  188. let (mut conn, store, _prefixes) = test_redis().await?;
  189. let service = service()?;
  190. let outcome = revoke_owned(
  191. &mut conn,
  192. &store,
  193. &service,
  194. &OwnedToken {
  195. owner_client_id: "client-a",
  196. token: "some-token",
  197. token_type_hint: Some("password"),
  198. },
  199. None,
  200. )
  201. .await?;
  202. if outcome != RevokeOutcome::UnsupportedHint {
  203. return Err(TestError::Unexpected(
  204. "unknown hint must be unsupported".to_owned(),
  205. ));
  206. }
  207. let response = outcome_response(outcome);
  208. if response.status() != StatusCode::BAD_REQUEST {
  209. return Err(TestError::Unexpected(
  210. "unsupported hint must render 400".to_owned(),
  211. ));
  212. }
  213. let bytes = response.into_body().collect().await?.to_bytes();
  214. let body: Value = serde_json::from_slice(&bytes)?;
  215. if body.get("error").and_then(Value::as_str) != Some("unsupported_token_type") {
  216. return Err(TestError::Unexpected(
  217. "error code must be unsupported_token_type".to_owned(),
  218. ));
  219. }
  220. Ok(())
  221. }
  222. /// A `refresh_token` hint on an access token still revokes it: the hint only
  223. /// guides the lookup (RFC 7009 §2.1), and the located token's real type
  224. /// decides the scope of revocation (revoke.rs: an access token revokes only
  225. /// itself, so the sibling refresh token survives).
  226. #[tokio::test]
  227. async fn revoke_refresh_hint_on_access_token_still_revokes_it() -> TestResult {
  228. let (mut conn, store, _prefixes) = test_redis().await?;
  229. let service = service()?;
  230. let client = unique("client");
  231. let minted = service
  232. .mint(
  233. &mut conn,
  234. &store,
  235. &mint_request(&client, &unique("grant")),
  236. now_unix(),
  237. )
  238. .await?;
  239. let outcome = revoke_owned(
  240. &mut conn,
  241. &store,
  242. &service,
  243. &OwnedToken {
  244. owner_client_id: &client,
  245. token: &minted.access_token,
  246. token_type_hint: Some(HINT_REFRESH),
  247. },
  248. None,
  249. )
  250. .await?;
  251. if outcome != RevokeOutcome::Revoked {
  252. return Err(TestError::Unexpected(
  253. "the misplaced hint must not block revocation".to_owned(),
  254. ));
  255. }
  256. if service
  257. .find_access(&mut conn, &store, &minted.access_token)
  258. .await?
  259. .is_some()
  260. {
  261. return Err(TestError::Unexpected(
  262. "the access token must be gone after revocation".to_owned(),
  263. ));
  264. }
  265. if service
  266. .find_refresh(&mut conn, &store, &minted.refresh_token)
  267. .await?
  268. .is_none()
  269. {
  270. return Err(TestError::Unexpected(
  271. "the hint must not extend revocation beyond an access token".to_owned(),
  272. ));
  273. }
  274. Ok(())
  275. }
  276. /// Revoking another client's refresh token is a silent no-op: `NoOp`, the
  277. /// refresh token stays resolvable, and its access sibling is untouched too.
  278. #[tokio::test]
  279. async fn revoke_foreign_refresh_token_is_noop_and_untouched() -> TestResult {
  280. let (mut conn, store, _prefixes) = test_redis().await?;
  281. let service = service()?;
  282. let owner = unique("owner");
  283. let attacker = unique("attacker");
  284. let minted = service
  285. .mint(
  286. &mut conn,
  287. &store,
  288. &mint_request(&owner, &unique("grant")),
  289. now_unix(),
  290. )
  291. .await?;
  292. let outcome = revoke_owned(
  293. &mut conn,
  294. &store,
  295. &service,
  296. &OwnedToken {
  297. owner_client_id: &attacker,
  298. token: &minted.refresh_token,
  299. token_type_hint: Some(HINT_REFRESH),
  300. },
  301. None,
  302. )
  303. .await?;
  304. if outcome != RevokeOutcome::NoOp {
  305. return Err(TestError::Unexpected(
  306. "a foreign refresh token must be a no-op".to_owned(),
  307. ));
  308. }
  309. if service
  310. .find_refresh(&mut conn, &store, &minted.refresh_token)
  311. .await?
  312. .is_none()
  313. {
  314. return Err(TestError::Unexpected(
  315. "a foreign client must never revoke another client's refresh token".to_owned(),
  316. ));
  317. }
  318. if service
  319. .find_access(&mut conn, &store, &minted.access_token)
  320. .await?
  321. .is_none()
  322. {
  323. return Err(TestError::Unexpected(
  324. "the sibling access token must also survive a foreign attempt".to_owned(),
  325. ));
  326. }
  327. Ok(())
  328. }