Skip to main content

txtx_cloud/auth/
mod.rs

1pub mod jwt;
2
3use serde::{Deserialize, Serialize};
4use std::io::{Read, Write};
5use txtx_core::kit::reqwest;
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct AuthConfig {
10    pub access_token: String,
11    pub exp: u64,
12    pub refresh_token: String,
13    pub pat: Option<String>,
14    pub user: AuthUser,
15}
16
17impl AuthConfig {
18    pub fn new(
19        access_token: String,
20        exp: u64,
21        refresh_token: String,
22        pat: Option<String>,
23        user: AuthUser,
24    ) -> Self {
25        Self { access_token, exp, refresh_token, pat, user }
26    }
27
28    async fn from_refresh_session_response(
29        id_service_url: &str,
30        RefreshSessionResponse { access_token, refresh_token, user }: &RefreshSessionResponse,
31        pat: &Option<String>,
32    ) -> Result<Self, String> {
33        let jwt_manager = jwt::JwtManager::initialize(id_service_url)
34            .await
35            .map_err(|e| format!("Failed to initialize JWT manager: {}", e))?;
36
37        let access_token_claims = jwt_manager
38            .decode_jwt(access_token, true)
39            .map_err(|e| format!("Failed to decode access token: {}", e))?;
40
41        Ok(Self {
42            access_token: access_token.clone(),
43            exp: access_token_claims.exp,
44            refresh_token: refresh_token.clone(),
45            pat: pat.clone(),
46            user: user.clone(),
47        })
48    }
49
50    /// Write auth config to system data directory.
51    pub fn write_to_system_config(&self) -> Result<(), String> {
52        let data_dir = dirs::data_dir().ok_or("Failed to get system data directory")?;
53
54        std::fs::create_dir_all(data_dir.join("txtx"))
55            .map_err(|e| format!("Failed to create data directory: {}", e))?;
56
57        let path = data_dir.join("txtx/auth.toml");
58
59        let mut file = std::fs::File::create(&path)
60            .map_err(|e| format!("Failed to create config file: {}", e))?;
61
62        let toml = toml::to_string(&self)
63            .map_err(|e| format!("Failed to serialize auth config: {}", e))?;
64
65        file.write_all(toml.as_bytes())
66            .map_err(|e| format!("Failed to write auth config: {}", e))?;
67        Ok(())
68    }
69
70    /// Read auth config from system data directory.
71    pub fn read_from_system_config() -> Result<Option<Self>, String> {
72        let data_dir = dirs::data_dir().ok_or("Failed to get system data directory")?;
73        let path = data_dir.join("txtx/auth.toml");
74
75        if !path.exists() {
76            return Ok(None);
77        }
78
79        let mut file =
80            std::fs::File::open(&path).map_err(|e| format!("Failed to open config file: {}", e))?;
81        let mut buf = String::new();
82
83        file.read_to_string(&mut buf).map_err(|e| format!("Failed to read config file: {}", e))?;
84
85        let config =
86            toml::from_str(&buf).map_err(|e| format!("Failed to parse auth config file: {}", e))?;
87        Ok(Some(config))
88    }
89
90    pub async fn refresh_session_if_needed(&mut self, id_service_url: &str) -> Result<(), String> {
91        if self.is_access_token_expired() {
92            let refreshed_auth_config = self.get_refreshed_session(id_service_url).await.map_err(|e| {
93                format!("Failed to refresh session. Run `txtx cloud login` to log in again. Downstream error: {e}")
94            })?;
95            self.access_token = refreshed_auth_config.access_token;
96            self.exp = refreshed_auth_config.exp;
97            self.refresh_token = refreshed_auth_config.refresh_token;
98            self.write_to_system_config()
99                .map_err(|e| format!("Failed to write refreshed session to config: {}", e))?;
100            return Ok(());
101        }
102        Ok(())
103    }
104
105    /// Get a new access token by sending a POST request to the auth service with the refresh token.
106    /// If the request is successful, the new auth config is written to the system config.
107    async fn get_refreshed_session(&self, id_service_url: &str) -> Result<AuthConfig, String> {
108        let client = reqwest::Client::new();
109        let res = client
110            .post(&format!("{id_service_url}/token"))
111            .json(&serde_json::json!({
112                "refreshToken": &self.refresh_token,
113            }))
114            .send()
115            .await
116            .map_err(|e| format!("Failed to send request to refresh session: {}", e))?;
117
118        if res.status().is_success() {
119            let res = res
120                .json::<RefreshSessionResponse>()
121                .await
122                .map_err(|e| format!("Failed to parse response: {}", e))?;
123
124            let auth_config =
125                AuthConfig::from_refresh_session_response(id_service_url, &res, &self.pat)
126                    .await
127                    .map_err(|e| format!("Failed to parse refresh session response: {e}"))?;
128
129            auth_config
130                .write_to_system_config()
131                .map_err(|e| format!("Failed to write refreshed session to config: {}", e))?;
132            return Ok(auth_config);
133        } else {
134            let err = res.text().await.unwrap_or_else(|_| "Unknown error".to_string());
135            return Err(format!("Received error from refresh session request: {}", err));
136        }
137    }
138
139    pub fn is_access_token_expired(&self) -> bool {
140        let now = std::time::SystemTime::now()
141            .duration_since(std::time::UNIX_EPOCH)
142            .expect("SystemTime before UNIX EPOCH")
143            .as_secs() as i64;
144
145        self.exp < now as u64
146    }
147}
148
149#[derive(Debug, Clone, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct RefreshSessionResponse {
152    pub access_token: String,
153    pub refresh_token: String,
154    pub user: AuthUser,
155}
156
157#[derive(Debug, Clone, Deserialize, Serialize)]
158#[serde(rename_all = "camelCase")]
159pub struct AuthUser {
160    pub id: String,
161    pub email: Option<String>,
162    pub display_name: String,
163}