oidc_test.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. use cdk::oidc_client::{verify_jwt_signature, Jwk};
  2. use js_sys::{Object, Reflect, Uint8Array};
  3. use wasm_bindgen::JsValue;
  4. use wasm_bindgen_futures::JsFuture;
  5. use wasm_bindgen_test::*;
  6. wasm_bindgen_test_configure!(run_in_browser);
  7. const TEST_ISSUER: &str = "https://test.issuer.example.com";
  8. const TEST_CLIENT_ID: &str = "test-client-123";
  9. // Pre-generated RSA key components (base64url, no padding)
  10. const RSA_N: &str = "viZMoE4GaFGF9cualbi9SMKYfL8_A1z8819SM_S38423mlGOyPZoR8gKUMMShQabPanuHAEpSooY2KXM68lAdG0UiRt__MupQWFRP5MaPg4wAkJxAZFWpVSkxuks_P8E3icce48RDjvInhNIB6ZSYQQ9VzXYLhCPOBVIo5CRuD8fXQPJU9TIAhGPUFYXaIcRnL8chECmGBXovHU16x3LxY5wZXA_CJ0HfsZ2Tu97lwyFmNb2enYBtB9NgrxHx3tFsE0uIketOCrKx7exrjWA_m6XHYxl-HB9ixI791cHjA_guSasnWp3HSVXdq8xvmbOZUtpJ0wu4LS9PwOAvvJaIw";
  11. const RSA_E: &str = "AQAB";
  12. const RSA_D: &str = "TYmIjYXDjx5PJd-UdaETbmwLijLiGxj7_LHN72nG6QXM7Jx9QO1ZsIudyTkCgEQlYYu9kKXYlJCjeRSC71LteYxRZ2dTVV4m8oYgf3AYr11RrloxgpYlYt2VI5dJxRCoh34jWy8HoWo3cF4kbRohVXZJHRrTwFT4UcI8EJaPFTXTnFWcFUS4I5wvtWeCpWwLB0IhvIvOfPSmsrEEyC0AlCm71X9WLGhln0YpPk7rEmdYp2pHTSed4pL822lUsuXQjkB3yjN7IJDq6BpJzMnsnO9QUsZhATI6ElPajeez2iYDzqSdrENAyNY7g9fiB1lJAF_eBchmg_zjmtp6MSb5IQ";
  13. const RSA_P: &str = "8A9Rg-CwLHbXvAjxhEUYqVZMWlHeIU7tCf7GiWxyr3u1eusUGWmu6eyWYZNuvOPONHA4e-f2o3ePBmSfuSG0B0Lp7MFS0BDiJTe1f7n1f0DcpcM5tnLXYdH_z5Nn27fnexgZHkNKkjIPwPwZUdoQTLVhZ2lDWv2NZK-Sy0NJKp8";
  14. const RSA_Q: &str = "ysaTpsmKokjbTWHDS6D469OFyvv1g6rSAeJufzEigD96xjOOk3rZQFQJ6WQFsZ8G4WXmLvBPNknjxv-0aKwxC0XLulw2TDKMeVWQESm-etXJmzHfPJyE9dJOCYOZ750xRJfQeYmDvWl3Gk9Uo9sIhuHpBqGUYKmbOOmU6Led5f0";
  15. const RSA_DP: &str = "xoHURToaVFpdoMbAeEDu2LBc6N8D0QVD69z67Y5483VXp3IWp8EFe7hAziUtEBNMY35cptE02Q23fnDcxykAhnSlnTprsVQUvPPpKNpsEDNhgc0Cv0UNp30QjOR2oHDdgKN3udepJWUyM8IDafTpP5VJG0snAGnkbtrkhyJ3sT8";
  16. const RSA_DQ: &str = "P22mOgHJD8Jidu4hvMJ5mqrrqvbtcWY5ksVVcwvXku5IZT8zVgaTdn_TKeJTtZ_c8xyAyCX7YSvzyAesUyGppbELbRvzEBqvvjR5gCTipGHDUnxjK_55yLskFe3IdR9ijeY_HAVb5B_dVamC_E5DeI2p6p0YYLQtDbxjC_iDt7E";
  17. const RSA_QI: &str = "hEd-TpKvFEi4PCm4845FR6D9O_Y6o8nVyODLbC6ACacA_f4rKm7BLQLphsG8aVaKteUMmJS6eQg2abM2Phmy9NrGzg3qGEGl_VDFQ6laDbfrtVgSAZBMzH-ftzoybZZO3MAFdE_x3HaafYkcZ_lcV_OMo1QGwD2cg3AGBZ3bAcM";
  18. // Pre-generated EC P-256 key components (base64url, no padding)
  19. const EC_X: &str = "_GpXxzqwAcWFrFINB4ehLPSWAbXEEt7TTgElIakGOhk";
  20. const EC_Y: &str = "r-CLBX5hepUn2PJ9tHivvDwvL_nZfwl1KrajlRQwzIc";
  21. const EC_D: &str = "RmG7VzFSrog-8-W7Yt1bbQuMxHY0ggnL9-oGaaLPWPM";
  22. // ---------------------------------------------------------------------------
  23. // Base64url encoding (no-pad) — avoids depending on bitcoin crate
  24. // ---------------------------------------------------------------------------
  25. const B64URL_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
  26. fn base64url_encode(data: &[u8]) -> String {
  27. let mut out = String::with_capacity((data.len() * 4 + 2) / 3);
  28. for chunk in data.chunks(3) {
  29. let b0 = chunk[0] as usize;
  30. let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 };
  31. let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 };
  32. let n = (b0 << 16) | (b1 << 8) | b2;
  33. out.push(B64URL_CHARS[(n >> 18) & 0x3f] as char);
  34. out.push(B64URL_CHARS[(n >> 12) & 0x3f] as char);
  35. if chunk.len() > 1 {
  36. out.push(B64URL_CHARS[(n >> 6) & 0x3f] as char);
  37. }
  38. if chunk.len() > 2 {
  39. out.push(B64URL_CHARS[n & 0x3f] as char);
  40. }
  41. }
  42. out
  43. }
  44. // ---------------------------------------------------------------------------
  45. // JS Object helpers
  46. // ---------------------------------------------------------------------------
  47. fn js_set(obj: &Object, key: &str, val: &JsValue) {
  48. Reflect::set(obj, &JsValue::from_str(key), val).unwrap();
  49. }
  50. fn js_str(s: &str) -> JsValue {
  51. JsValue::from_str(s)
  52. }
  53. fn hash_obj(name: &str) -> Object {
  54. let o = Object::new();
  55. js_set(&o, "name", &js_str(name));
  56. o
  57. }
  58. fn get_subtle() -> web_sys::SubtleCrypto {
  59. let global = js_sys::global();
  60. let crypto = Reflect::get(&global, &js_str("crypto")).unwrap();
  61. let subtle = Reflect::get(&crypto, &js_str("subtle")).unwrap();
  62. subtle.into()
  63. }
  64. // ---------------------------------------------------------------------------
  65. // JWK builders — construct JS Objects directly for Web Crypto importKey
  66. // ---------------------------------------------------------------------------
  67. fn build_rsa_private_jwk_js() -> Object {
  68. let o = Object::new();
  69. js_set(&o, "kty", &js_str("RSA"));
  70. js_set(&o, "n", &js_str(RSA_N));
  71. js_set(&o, "e", &js_str(RSA_E));
  72. js_set(&o, "d", &js_str(RSA_D));
  73. js_set(&o, "p", &js_str(RSA_P));
  74. js_set(&o, "q", &js_str(RSA_Q));
  75. js_set(&o, "dp", &js_str(RSA_DP));
  76. js_set(&o, "dq", &js_str(RSA_DQ));
  77. js_set(&o, "qi", &js_str(RSA_QI));
  78. o
  79. }
  80. fn build_ec_private_jwk_js() -> Object {
  81. let o = Object::new();
  82. js_set(&o, "kty", &js_str("EC"));
  83. js_set(&o, "crv", &js_str("P-256"));
  84. js_set(&o, "x", &js_str(EC_X));
  85. js_set(&o, "y", &js_str(EC_Y));
  86. js_set(&o, "d", &js_str(EC_D));
  87. o
  88. }
  89. fn rsa_public_jwk() -> Jwk {
  90. Jwk {
  91. kty: "RSA".into(),
  92. kid: Some("test-key-1".into()),
  93. alg: Some("RS256".into()),
  94. n: Some(RSA_N.into()),
  95. e: Some(RSA_E.into()),
  96. crv: None,
  97. x: None,
  98. y: None,
  99. extra: Default::default(),
  100. }
  101. }
  102. fn ec_public_jwk() -> Jwk {
  103. Jwk {
  104. kty: "EC".into(),
  105. kid: Some("test-key-1".into()),
  106. alg: Some("ES256".into()),
  107. n: None,
  108. e: None,
  109. crv: Some("P-256".into()),
  110. x: Some(EC_X.into()),
  111. y: Some(EC_Y.into()),
  112. extra: Default::default(),
  113. }
  114. }
  115. // ---------------------------------------------------------------------------
  116. // Algorithm builders
  117. // ---------------------------------------------------------------------------
  118. fn rsa_algo() -> Object {
  119. let algo = Object::new();
  120. js_set(&algo, "name", &js_str("RSASSA-PKCS1-v1_5"));
  121. js_set(&algo, "hash", &hash_obj("SHA-256").into());
  122. algo
  123. }
  124. fn ec_import_algo() -> Object {
  125. let algo = Object::new();
  126. js_set(&algo, "name", &js_str("ECDSA"));
  127. js_set(&algo, "namedCurve", &js_str("P-256"));
  128. algo
  129. }
  130. fn ec_sign_algo() -> Object {
  131. let algo = Object::new();
  132. js_set(&algo, "name", &js_str("ECDSA"));
  133. js_set(&algo, "hash", &hash_obj("SHA-256").into());
  134. algo
  135. }
  136. // ---------------------------------------------------------------------------
  137. // JWT signing helper using Web Crypto
  138. // ---------------------------------------------------------------------------
  139. async fn sign_jwt_web_crypto(
  140. claims_json: &str,
  141. private_jwk: &Object,
  142. alg_name: &str,
  143. kid: &str,
  144. import_algo: &Object,
  145. sign_algo: &Object,
  146. ) -> String {
  147. let subtle = get_subtle();
  148. let header = format!(r#"{{"alg":"{}","typ":"JWT","kid":"{}"}}"#, alg_name, kid);
  149. let header_b64 = base64url_encode(header.as_bytes());
  150. let payload_b64 = base64url_encode(claims_json.as_bytes());
  151. let message = format!("{}.{}", header_b64, payload_b64);
  152. // importKey("jwk", private_jwk, algo, false, ["sign"])
  153. let usages = js_sys::Array::new();
  154. usages.push(&js_str("sign"));
  155. let key_promise = subtle
  156. .import_key_with_object("jwk", private_jwk, import_algo, false, &usages)
  157. .unwrap();
  158. let key: web_sys::CryptoKey = JsFuture::from(key_promise).await.unwrap().into();
  159. // sign(algo, key, data)
  160. let msg_array = Uint8Array::from(message.as_bytes() as &[u8]);
  161. let sign_promise = subtle
  162. .sign_with_object_and_buffer_source(sign_algo, &key, &msg_array)
  163. .unwrap();
  164. let sig_buffer = JsFuture::from(sign_promise).await.unwrap();
  165. let sig_array = Uint8Array::new(&sig_buffer);
  166. let mut sig_bytes = vec![0u8; sig_array.length() as usize];
  167. sig_array.copy_to(&mut sig_bytes);
  168. let sig_b64 = base64url_encode(&sig_bytes);
  169. format!("{}.{}.{}", header_b64, payload_b64, sig_b64)
  170. }
  171. fn claims_str(issuer: &str, client_id: &str, expired: bool) -> String {
  172. let exp = if expired { 1_000_000_000u64 } else { 4_102_444_800u64 };
  173. format!(
  174. r#"{{"iss":"{}","client_id":"{}","exp":{},"sub":"test-user"}}"#,
  175. issuer, client_id, exp
  176. )
  177. }
  178. // ---------------------------------------------------------------------------
  179. // Helpers: sign a JWT for reuse across multiple tests
  180. // ---------------------------------------------------------------------------
  181. async fn make_rsa_jwt(issuer: &str, client_id: &str, expired: bool) -> String {
  182. let algo = rsa_algo();
  183. sign_jwt_web_crypto(
  184. &claims_str(issuer, client_id, expired),
  185. &build_rsa_private_jwk_js(),
  186. "RS256",
  187. "test-key-1",
  188. &algo,
  189. &algo,
  190. )
  191. .await
  192. }
  193. async fn make_ec_jwt(issuer: &str, client_id: &str, expired: bool) -> String {
  194. sign_jwt_web_crypto(
  195. &claims_str(issuer, client_id, expired),
  196. &build_ec_private_jwk_js(),
  197. "ES256",
  198. "test-key-1",
  199. &ec_import_algo(),
  200. &ec_sign_algo(),
  201. )
  202. .await
  203. }
  204. // ---------------------------------------------------------------------------
  205. // Tests
  206. // ---------------------------------------------------------------------------
  207. #[wasm_bindgen_test]
  208. async fn test_verify_jwt_rsa_rs256() {
  209. let jwt = make_rsa_jwt(TEST_ISSUER, TEST_CLIENT_ID, false).await;
  210. let result = verify_jwt_signature(&jwt, &rsa_public_jwk(), "RS256", TEST_ISSUER).await;
  211. let claims = result.unwrap();
  212. assert_eq!(claims["iss"], TEST_ISSUER);
  213. assert_eq!(claims["client_id"], TEST_CLIENT_ID);
  214. }
  215. #[wasm_bindgen_test]
  216. async fn test_verify_jwt_ec_es256() {
  217. let jwt = make_ec_jwt(TEST_ISSUER, TEST_CLIENT_ID, false).await;
  218. let result = verify_jwt_signature(&jwt, &ec_public_jwk(), "ES256", TEST_ISSUER).await;
  219. let claims = result.unwrap();
  220. assert_eq!(claims["iss"], TEST_ISSUER);
  221. assert_eq!(claims["client_id"], TEST_CLIENT_ID);
  222. }
  223. #[wasm_bindgen_test]
  224. async fn test_verify_jwt_expired() {
  225. let jwt = make_rsa_jwt(TEST_ISSUER, TEST_CLIENT_ID, true).await;
  226. let result = verify_jwt_signature(&jwt, &rsa_public_jwk(), "RS256", TEST_ISSUER).await;
  227. assert!(result.is_err(), "expected error for expired token");
  228. }
  229. #[wasm_bindgen_test]
  230. async fn test_verify_jwt_wrong_issuer() {
  231. let jwt = make_rsa_jwt(TEST_ISSUER, TEST_CLIENT_ID, false).await;
  232. let result =
  233. verify_jwt_signature(&jwt, &rsa_public_jwk(), "RS256", "https://wrong.issuer.com").await;
  234. assert!(result.is_err(), "expected error for wrong issuer");
  235. }
  236. #[wasm_bindgen_test]
  237. async fn test_verify_jwt_bad_signature() {
  238. let jwt = make_rsa_jwt(TEST_ISSUER, TEST_CLIENT_ID, false).await;
  239. // Tamper with the signature
  240. let parts: Vec<&str> = jwt.splitn(3, '.').collect();
  241. let tampered = format!("{}.{}.{}AAAA", parts[0], parts[1], parts[2]);
  242. let result = verify_jwt_signature(&tampered, &rsa_public_jwk(), "RS256", TEST_ISSUER).await;
  243. assert!(result.is_err(), "expected error for tampered signature");
  244. }
  245. #[wasm_bindgen_test]
  246. async fn test_verify_jwt_unsupported_kty() {
  247. let jwt = make_rsa_jwt(TEST_ISSUER, TEST_CLIENT_ID, false).await;
  248. let mut jwk = rsa_public_jwk();
  249. jwk.kty = "OKP".into();
  250. let result = verify_jwt_signature(&jwt, &jwk, "RS256", TEST_ISSUER).await;
  251. assert!(result.is_err(), "expected error for unsupported key type");
  252. }