/ packages / auths-python / src / token.rs
token.rs
 1  use pyo3::prelude::*;
 2  use serde::{Deserialize, Serialize};
 3  use std::sync::OnceLock;
 4  
 5  use crate::runtime::runtime;
 6  
 7  static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
 8  
 9  fn http_client() -> &'static reqwest::Client {
10      HTTP_CLIENT.get_or_init(|| {
11          reqwest::Client::builder()
12              .timeout(std::time::Duration::from_secs(30))
13              .build()
14              .expect("failed to create HTTP client")
15      })
16  }
17  
18  #[derive(Serialize)]
19  struct ExchangeRequest {
20      attestation_chain: serde_json::Value,
21      root_public_key: String,
22      #[serde(skip_serializing_if = "Option::is_none")]
23      requested_capabilities: Option<Vec<String>>,
24  }
25  
26  #[derive(Deserialize)]
27  struct TokenResponse {
28      access_token: String,
29  }
30  
31  /// Exchange an attestation chain for a Bearer token via the OIDC bridge.
32  ///
33  /// Args:
34  /// * `bridge_url`: The OIDC bridge base URL.
35  /// * `chain_json`: JSON string of the attestation chain array.
36  /// * `root_public_key`: Hex-encoded Ed25519 public key.
37  /// * `capabilities`: List of requested capability strings.
38  ///
39  /// Usage:
40  /// ```ignore
41  /// let token = get_token(py, "https://bridge.example.com", "[...]", "abcd...", vec![])?;
42  /// ```
43  #[pyfunction]
44  pub fn get_token(
45      py: Python<'_>,
46      bridge_url: String,
47      chain_json: String,
48      root_public_key: String,
49      capabilities: Vec<String>,
50  ) -> PyResult<String> {
51      let chain: serde_json::Value = serde_json::from_str(&chain_json).map_err(|e| {
52          PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("invalid chain JSON: {e}"))
53      })?;
54  
55      let url = format!("{}/token", bridge_url.trim_end_matches('/'));
56      let request_body = ExchangeRequest {
57          attestation_chain: chain,
58          root_public_key,
59          requested_capabilities: if capabilities.is_empty() {
60              None
61          } else {
62              Some(capabilities)
63          },
64      };
65  
66      py.allow_threads(|| {
67          runtime().block_on(async {
68              let response = http_client()
69                  .post(&url)
70                  .json(&request_body)
71                  .send()
72                  .await
73                  .map_err(|e| {
74                      PyErr::new::<pyo3::exceptions::PyConnectionError, _>(format!(
75                          "bridge unreachable: {e}"
76                      ))
77                  })?;
78  
79              if !response.status().is_success() {
80                  let status = response.status().as_u16();
81                  let body = response.text().await.unwrap_or_default();
82                  return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
83                      "token exchange failed (HTTP {status}): {body}"
84                  )));
85              }
86  
87              let token_resp: TokenResponse = response.json().await.map_err(|e| {
88                  PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("invalid response: {e}"))
89              })?;
90  
91              Ok(token_resp.access_token)
92          })
93      })
94  }