edge.rs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. //! Request mechanics for the `tests/e2e_edge_matrix.rs` rejection matrix.
  2. //!
  3. //! Each function owns one row's wire setup against the live router; the
  4. //! suite owns the table and the row-level assertions. Device rows reuse the
  5. //! shared `e2e/device.rs` driver so `/device` and `/token` are exercised
  6. //! exactly as the RFC 8628 flow does.
  7. use std::time::Duration;
  8. use d_oauth::{
  9. crypto::tokens::{KeyId as TokenKeyId, mint_token, token_hash},
  10. db::TokenEndpointAuthMethod,
  11. store::dcr::{self, InitialAccessRecord},
  12. };
  13. use reqwest::StatusCode;
  14. use secrecy::ExposeSecret as _;
  15. use serde_json::Value;
  16. use super::{
  17. browser, device,
  18. device::{
  19. Client, DEVICE_GRANT, Decision, PUBLIC_CLIENT, Poll, SCOPE, decide, issue, poll,
  20. seed_public_client,
  21. },
  22. dpop::DpopKey,
  23. harness::{TestError, World, ensure, fail, json_body, open_rate_limit, seed_client},
  24. };
  25. /// User provisioned for the device approval flow.
  26. const DEVICE_EMAIL: &str = "edge-device@example.test";
  27. /// Password for [`DEVICE_EMAIL`].
  28. const DEVICE_PASSWORD: &str = "e2e-edge-device-password-1a2b3c";
  29. /// Seed an IAT exactly as `protocol::dcr::tests::issue_iat` does: the storage
  30. /// key is the active pepper's keyed hash, not a plain `sha256_hex`.
  31. async fn seed_iat(world: &World) -> Result<String, TestError> {
  32. let raw = mint_token().map_err(|error| fail(format!("mint IAT failed: {error}")))?;
  33. let (key_id, pepper) = world
  34. .state
  35. .config
  36. .keys
  37. .active_pepper()
  38. .ok_or_else(|| fail("config has no active token pepper"))?;
  39. let hash = token_hash(
  40. &TokenKeyId::new(key_id.as_str()),
  41. pepper.expose_secret().as_bytes(),
  42. &raw,
  43. )
  44. .to_storage_string();
  45. let mut conn = world.state.redis.clone();
  46. dcr::issue(
  47. &mut conn,
  48. &world.store,
  49. &hash,
  50. &InitialAccessRecord {
  51. uses_remaining: 1,
  52. label: "edge".to_owned(),
  53. },
  54. Duration::from_secs(600),
  55. )
  56. .await
  57. .map_err(|error| fail(format!("seed IAT failed: {error}")))?;
  58. Ok(raw)
  59. }
  60. /// One DCR rejection: exact code, no client in the body, no client row.
  61. pub async fn dcr_rejects(body: Value, expected: &str) -> Result<(), TestError> {
  62. let world = World::start(open_rate_limit()).await?;
  63. let iat = seed_iat(&world).await?;
  64. let response = world
  65. .client
  66. .post(format!("{}/register", world.base))
  67. .bearer_auth(&iat)
  68. .json(&body)
  69. .send()
  70. .await?;
  71. let status = response.status();
  72. let text = response.text().await?;
  73. ensure_error(status, &text, StatusCode::BAD_REQUEST, expected)?;
  74. ensure(
  75. json_body(&text)?.get("client_id").is_none(),
  76. format!("a rejected registration must mint no client: {text}"),
  77. )?;
  78. let clients: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM oauth_clients")
  79. .fetch_one(&world.pool)
  80. .await?;
  81. ensure(
  82. clients == 0,
  83. format!("a rejected registration must persist no client row, found {clients}"),
  84. )
  85. }
  86. /// Assert an exact OAuth error response that minted nothing.
  87. pub fn ensure_error(
  88. status: StatusCode,
  89. body: &str,
  90. expected_status: StatusCode,
  91. expected: &str,
  92. ) -> Result<(), TestError> {
  93. ensure(
  94. status == expected_status,
  95. format!("expected {expected_status} {expected}, got {status}: {body}"),
  96. )?;
  97. let parsed = json_body(body)?;
  98. ensure(
  99. parsed["error"].as_str() == Some(expected),
  100. format!("expected {expected}, got {body}"),
  101. )?;
  102. ensure(
  103. parsed.get("access_token").is_none() && parsed.get("refresh_token").is_none(),
  104. format!("a rejection must mint no token: {body}"),
  105. )
  106. }
  107. /// A device authorization wider than the client registration is `invalid_scope`.
  108. pub async fn device_over_registered_scope() -> Result<(), TestError> {
  109. let world = World::start(open_rate_limit()).await?;
  110. seed_client(
  111. &world.pool,
  112. &world.state.keys,
  113. PUBLIC_CLIENT,
  114. None,
  115. &[DEVICE_GRANT],
  116. SCOPE,
  117. TokenEndpointAuthMethod::None,
  118. )
  119. .await?;
  120. let (status, _, body) = world
  121. .post_form(
  122. "/device_authorization",
  123. &[("client_id", PUBLIC_CLIENT), ("scope", "read write")],
  124. None,
  125. &[],
  126. )
  127. .await?;
  128. ensure_error(status, &body, StatusCode::BAD_REQUEST, "invalid_scope")?;
  129. ensure(
  130. json_body(&body)?.get("device_code").is_none(),
  131. format!("an over-scope request must mint no device code: {body}"),
  132. )
  133. }
  134. /// An approved device interaction both consumed-code rows observe.
  135. struct ApprovedDevice {
  136. /// Live world owned by the flow.
  137. world: World,
  138. /// Authenticated session id.
  139. session: String,
  140. /// Opaque device code.
  141. device_code: String,
  142. /// Display user code.
  143. user_code: String,
  144. /// Polling proof key.
  145. key: DpopKey,
  146. /// Polling client.
  147. client: Client<'static>,
  148. }
  149. /// Issue a device code, approve it through the real UI, and return the state
  150. /// both consumed-code rows observe.
  151. async fn approve_device() -> Result<ApprovedDevice, TestError> {
  152. let world = World::start(open_rate_limit()).await?;
  153. seed_public_client(&world).await?;
  154. let browser = browser::login_session(world, DEVICE_EMAIL, DEVICE_PASSWORD).await?;
  155. let session = browser
  156. .session
  157. .clone()
  158. .ok_or_else(|| fail("login_session must authenticate"))?;
  159. let key = DpopKey::generate()?;
  160. let client = Client {
  161. id: PUBLIC_CLIENT,
  162. secret: None,
  163. scope: SCOPE,
  164. };
  165. let (device_code, user_code) = issue(&browser.world, client, &key).await?;
  166. let page = device::review(&browser.world, &session, &user_code).await?;
  167. let csrf = browser::hidden_input(&page, "csrf")?;
  168. decide(
  169. &browser.world,
  170. Decision {
  171. session: &session,
  172. user_code: &user_code,
  173. csrf: &csrf,
  174. action: "approve",
  175. },
  176. )
  177. .await?;
  178. Ok(ApprovedDevice {
  179. world: browser.world,
  180. session,
  181. device_code,
  182. user_code,
  183. key,
  184. client,
  185. })
  186. }
  187. /// A consumed user code at `/device` is the generic 200 miss, never
  188. /// `expired_token`.
  189. pub async fn device_user_code_consumed() -> Result<(), TestError> {
  190. let approved = approve_device().await?;
  191. let cookie = device::session_cookie(&approved.session);
  192. let (status, _, body) = approved
  193. .world
  194. .post_form(
  195. "/device",
  196. &[("user_code", approved.user_code.as_str())],
  197. None,
  198. &[("cookie", cookie.as_str())],
  199. )
  200. .await?;
  201. ensure(
  202. status == StatusCode::OK,
  203. format!("a consumed user_code must be a 200 miss, got {status}: {body}"),
  204. )?;
  205. ensure(
  206. !body.contains("expired_token")
  207. && !body.contains(PUBLIC_CLIENT)
  208. && !body.contains("access_token"),
  209. format!("the /device miss must be generic: {body}"),
  210. )
  211. }
  212. /// A consumed device code at `/token` is `400 expired_token` and mints nothing.
  213. pub async fn device_code_consumed_at_token() -> Result<(), TestError> {
  214. let approved = approve_device().await?;
  215. let request = Poll {
  216. client: approved.client,
  217. device_code: &approved.device_code,
  218. };
  219. let (status, body) = poll(&approved.world, request, &approved.key).await?;
  220. ensure(
  221. status == StatusCode::OK,
  222. format!("the approved poll must mint: {status} {body}"),
  223. )?;
  224. ensure(
  225. json_body(&body)?["access_token"].as_str().is_some(),
  226. "the first poll must mint an access token",
  227. )?;
  228. let (status, body) = poll(&approved.world, request, &approved.key).await?;
  229. ensure_error(status, &body, StatusCode::BAD_REQUEST, "expired_token")
  230. }