scope.rs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. //! Scope narrowing: narrower is kept, wider is rejected.
  2. use super::{
  3. TestError, TestResult, dpop_proof, input, is_invalid_scope, mint_grant, now_unix, service,
  4. test_redis,
  5. };
  6. use crate::protocol::{
  7. scope::ScopeSet,
  8. token::{
  9. ClientKind,
  10. grants::refresh_token::{RefreshInput, refresh},
  11. },
  12. };
  13. /// Scope narrowing succeeds and the narrowed scope sticks.
  14. #[tokio::test]
  15. async fn grant_refresh_scope_narrow_ok() -> TestResult {
  16. let (_container, mut conn, store, _url) = test_redis().await?;
  17. let service = service()?;
  18. let client = super::unique("narrow-client");
  19. let proof = dpop_proof(&super::unique("narrow-jkt"));
  20. let minted = mint_grant(
  21. &service,
  22. &mut conn,
  23. &store,
  24. &client,
  25. ClientKind::Public,
  26. "read write",
  27. proof.clone(),
  28. )
  29. .await?;
  30. let enabled = ScopeSet::parse("read write")?;
  31. let narrowed = refresh(
  32. &mut conn,
  33. &store,
  34. &service,
  35. &input(
  36. &client,
  37. ClientKind::Public,
  38. &minted.refresh_token,
  39. Some("read"),
  40. &proof,
  41. &enabled,
  42. ),
  43. )
  44. .await?;
  45. if narrowed.scope != "read" {
  46. return Err(TestError::Unexpected(format!(
  47. "scope not narrowed: {}",
  48. narrowed.scope
  49. )));
  50. }
  51. Ok(())
  52. }
  53. /// Scope widening is rejected with `invalid_scope`.
  54. #[tokio::test]
  55. async fn grant_refresh_scope_widen_rejected() -> TestResult {
  56. let (_container, mut conn, store, _url) = test_redis().await?;
  57. let service = service()?;
  58. let client = super::unique("widen-client");
  59. let proof = dpop_proof(&super::unique("widen-jkt"));
  60. let minted = mint_grant(
  61. &service,
  62. &mut conn,
  63. &store,
  64. &client,
  65. ClientKind::Public,
  66. "read",
  67. proof.clone(),
  68. )
  69. .await?;
  70. let enabled = ScopeSet::parse("admin read")?;
  71. let widened = refresh(
  72. &mut conn,
  73. &store,
  74. &service,
  75. &input(
  76. &client,
  77. ClientKind::Public,
  78. &minted.refresh_token,
  79. Some("admin read"),
  80. &proof,
  81. &enabled,
  82. ),
  83. )
  84. .await;
  85. let error = match widened {
  86. Ok(_) => return Err(TestError::Unexpected("widened scope rotated".to_owned())),
  87. Err(error) => error,
  88. };
  89. if !is_invalid_scope(&error) {
  90. return Err(TestError::Unexpected(format!(
  91. "widen code: {}",
  92. error.code()
  93. )));
  94. }
  95. Ok(())
  96. }
  97. /// Narrowing on refresh keeps the new refresh token at the presented scope
  98. /// (draft-ietf-oauth-v2-1-16 §4.3.3) while the access token narrows
  99. /// (§4.3.1): the response reports the narrowed access scope, the stored
  100. /// refresh record keeps the full scope, and the next rotation can still use
  101. /// the full scope.
  102. #[tokio::test]
  103. async fn grant_refresh_narrow_keeps_refresh_scope() -> TestResult {
  104. let (_container, mut conn, store, _url) = test_redis().await?;
  105. let service = service()?;
  106. let client = super::unique("rt-kept-client");
  107. let proof = dpop_proof(&super::unique("rt-kept-jkt"));
  108. let minted = mint_grant(
  109. &service,
  110. &mut conn,
  111. &store,
  112. &client,
  113. ClientKind::Public,
  114. "read write",
  115. proof.clone(),
  116. )
  117. .await?;
  118. let enabled = ScopeSet::parse("read write")?;
  119. let narrowed = refresh(
  120. &mut conn,
  121. &store,
  122. &service,
  123. &input(
  124. &client,
  125. ClientKind::Public,
  126. &minted.refresh_token,
  127. Some("read"),
  128. &proof,
  129. &enabled,
  130. ),
  131. )
  132. .await?;
  133. if narrowed.scope != "read" {
  134. return Err(TestError::Unexpected(format!(
  135. "response scope not narrowed: {}",
  136. narrowed.scope
  137. )));
  138. }
  139. let live_refresh = service
  140. .find_refresh(&mut conn, &store, &narrowed.refresh_token)
  141. .await?;
  142. let refresh_scope = match live_refresh {
  143. Some(found) => found.record.scope,
  144. None => {
  145. return Err(TestError::Unexpected(
  146. "rotated refresh token not found".to_owned(),
  147. ));
  148. }
  149. };
  150. if refresh_scope != "read write" {
  151. return Err(TestError::Unexpected(format!(
  152. "refresh scope narrowed: {refresh_scope}"
  153. )));
  154. }
  155. let live_access = service
  156. .find_access(&mut conn, &store, &narrowed.access_token)
  157. .await?;
  158. let access_scope = match live_access {
  159. Some(found) => found.record.scope,
  160. None => {
  161. return Err(TestError::Unexpected(
  162. "rotated access token not found".to_owned(),
  163. ));
  164. }
  165. };
  166. if access_scope != "read" {
  167. return Err(TestError::Unexpected(format!(
  168. "access scope not narrowed: {access_scope}"
  169. )));
  170. }
  171. // The retained refresh scope still authorizes the full grant on the next
  172. // rotation with an omitted scope.
  173. let restored = refresh(
  174. &mut conn,
  175. &store,
  176. &service,
  177. &input(
  178. &client,
  179. ClientKind::Public,
  180. &narrowed.refresh_token,
  181. None,
  182. &proof,
  183. &enabled,
  184. ),
  185. )
  186. .await?;
  187. if restored.scope != "read write" {
  188. return Err(TestError::Unexpected(format!(
  189. "retained scope lost: {}",
  190. restored.scope
  191. )));
  192. }
  193. Ok(())
  194. }
  195. /// A revoked entitlement still drops from the retained refresh scope, while
  196. /// an explicit narrower request narrows only the access token.
  197. #[tokio::test]
  198. async fn grant_refresh_narrow_drops_revoked_from_refresh_scope() -> TestResult {
  199. let (_container, mut conn, store, _url) = test_redis().await?;
  200. let service = service()?;
  201. let client = super::unique("rt-revoked-client");
  202. let proof = dpop_proof(&super::unique("rt-revoked-jkt"));
  203. let minted = mint_grant(
  204. &service,
  205. &mut conn,
  206. &store,
  207. &client,
  208. ClientKind::Public,
  209. "read write",
  210. proof.clone(),
  211. )
  212. .await?;
  213. let enabled = ScopeSet::parse("read write")?;
  214. let registered = ScopeSet::parse("read write")?;
  215. // `write` was revoked from the user after issuance.
  216. let entitled = ScopeSet::parse("read")?;
  217. let narrowed = refresh(
  218. &mut conn,
  219. &store,
  220. &service,
  221. &RefreshInput {
  222. client_id: &client,
  223. client_kind: ClientKind::Public,
  224. refresh_token: &minted.refresh_token,
  225. requested_scope: Some("read"),
  226. proof: &proof,
  227. enabled: &enabled,
  228. client_registered: &registered,
  229. user_entitled: &entitled,
  230. now_unix: now_unix(),
  231. },
  232. )
  233. .await?;
  234. if narrowed.scope != "read" {
  235. return Err(TestError::Unexpected(format!(
  236. "response scope not narrowed: {}",
  237. narrowed.scope
  238. )));
  239. }
  240. let live_refresh = service
  241. .find_refresh(&mut conn, &store, &narrowed.refresh_token)
  242. .await?;
  243. let refresh_scope = match live_refresh {
  244. Some(found) => found.record.scope,
  245. None => {
  246. return Err(TestError::Unexpected(
  247. "rotated refresh token not found".to_owned(),
  248. ));
  249. }
  250. };
  251. if refresh_scope != "read" {
  252. return Err(TestError::Unexpected(format!(
  253. "revoked scope survived on refresh token: {refresh_scope}"
  254. )));
  255. }
  256. Ok(())
  257. }