auth_framework/methods/enhanced_device/
mod.rs1use crate::authentication::credentials::{Credential, CredentialMetadata};
7use crate::errors::{AuthError, Result};
8use crate::methods::{AuthMethod, MethodResult};
9use crate::tokens::AuthToken;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct DeviceFlowInstructions {
15 pub verification_uri: String,
17 pub verification_uri_complete: Option<String>,
19 pub user_code: String,
21 pub qr_code: Option<String>,
23 pub expires_in: u64,
25 pub interval: u64,
27}
28
29#[cfg(feature = "enhanced-device-flow")]
31#[derive(Debug)]
32pub struct EnhancedDeviceFlowMethod {
33 pub client_id: String,
35 pub client_secret: Option<String>,
37 pub auth_url: String,
39 pub token_url: String,
41 pub device_auth_url: String,
43 pub scopes: Vec<String>,
45 pub _polling_interval: Option<std::time::Duration>,
47 pub enable_qr_code: bool,
49}
50
51#[cfg(feature = "enhanced-device-flow")]
52impl EnhancedDeviceFlowMethod {
53 pub fn new(
55 client_id: String,
56 client_secret: Option<String>,
57 auth_url: String,
58 token_url: String,
59 device_auth_url: String,
60 ) -> Self {
61 Self {
62 client_id,
63 client_secret,
64 auth_url,
65 token_url,
66 device_auth_url,
67 scopes: Vec::new(),
68 _polling_interval: None,
69 enable_qr_code: true,
70 }
71 }
72
73 pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
75 self.scopes = scopes;
76 self
77 }
78
79 pub fn with_polling_interval(mut self, interval: std::time::Duration) -> Self {
81 self._polling_interval = Some(interval);
82 self
83 }
84
85 pub fn with_qr_code(mut self, enable: bool) -> Self {
87 self.enable_qr_code = enable;
88 self
89 }
90
91 pub async fn initiate_device_flow(&self) -> Result<DeviceFlowInstructions> {
93 Ok(DeviceFlowInstructions {
96 verification_uri: "https://github.com/login/device".to_string(),
97 verification_uri_complete: Some(
98 "https://github.com/login/device?user_code=ABCD-1234".to_string(),
99 ),
100 user_code: "ABCD-1234".to_string(),
101 qr_code: if self.enable_qr_code {
102 Some("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==".to_string())
103 } else {
104 None
105 },
106 expires_in: 900,
107 interval: 5,
108 })
109 }
110}
111
112#[cfg(feature = "enhanced-device-flow")]
113impl AuthMethod for EnhancedDeviceFlowMethod {
114 type MethodResult = MethodResult;
115 type AuthToken = AuthToken;
116
117 async fn authenticate(
118 &self,
119 credential: Credential,
120 _metadata: CredentialMetadata,
121 ) -> Result<Self::MethodResult> {
122 match credential {
123 Credential::EnhancedDeviceFlow {
124 device_code,
125 interval: _interval,
126 ..
127 } => {
128 let token = AuthToken::new(
130 device_code.clone(),
131 "device_access_token".to_string(),
132 std::time::Duration::from_secs(3600),
133 "enhanced_device_flow",
134 );
135 Ok(MethodResult::Success(Box::new(token)))
136 }
137 _ => Ok(MethodResult::Failure {
138 reason: "Invalid credential type for enhanced device flow".to_string(),
139 }),
140 }
141 }
142
143 fn name(&self) -> &str {
144 "enhanced_device_flow"
145 }
146
147 fn validate_config(&self) -> Result<()> {
148 if self.client_id.is_empty() {
149 return Err(AuthError::config("Client ID is required"));
150 }
151 if self.auth_url.is_empty() {
152 return Err(AuthError::config("Authorization URL is required"));
153 }
154 if self.token_url.is_empty() {
155 return Err(AuthError::config("Token URL is required"));
156 }
157 if self.device_auth_url.is_empty() {
158 return Err(AuthError::config("Device authorization URL is required"));
159 }
160 Ok(())
161 }
162}
163
164#[cfg(not(feature = "enhanced-device-flow"))]
166#[derive(Debug)]
167pub struct EnhancedDeviceFlowMethod {
168 client_id: String,
170 client_secret: Option<String>,
171 auth_url: String,
172 token_url: String,
173 device_auth_url: String,
174}
175
176#[cfg(not(feature = "enhanced-device-flow"))]
177impl EnhancedDeviceFlowMethod {
178 pub fn new(
179 client_id: String,
180 client_secret: Option<String>,
181 auth_url: String,
182 token_url: String,
183 device_auth_url: String,
184 ) -> Self {
185 Self {
186 client_id,
187 client_secret,
188 auth_url,
189 token_url,
190 device_auth_url,
191 }
192 }
193}
194
195#[cfg(not(feature = "enhanced-device-flow"))]
196impl AuthMethod for EnhancedDeviceFlowMethod {
197 type MethodResult = MethodResult;
198 type AuthToken = AuthToken;
199
200 async fn authenticate(
201 &self,
202 _credential: Credential,
203 _metadata: CredentialMetadata,
204 ) -> Result<Self::MethodResult> {
205 Err(AuthError::config(format!(
207 "Enhanced device flow requires 'enhanced-device-flow' feature. Configured for client '{}' with auth_url: {}, token_url: {}, device_auth_url: {}",
208 self.client_id, self.auth_url, self.token_url, self.device_auth_url
209 )))
210 }
211
212 fn name(&self) -> &str {
213 "enhanced_device_flow"
214 }
215
216 fn validate_config(&self) -> Result<()> {
217 if self.client_id.is_empty() {
219 return Err(AuthError::config("client_id cannot be empty"));
220 }
221 if self.auth_url.is_empty() {
222 return Err(AuthError::config("auth_url cannot be empty"));
223 }
224 if self.token_url.is_empty() {
225 return Err(AuthError::config("token_url cannot be empty"));
226 }
227 if self.device_auth_url.is_empty() {
228 return Err(AuthError::config("device_auth_url cannot be empty"));
229 }
230
231 if self.client_secret.is_some() {
233 tracing::info!(
234 "Enhanced device flow configured for confidential client: {}",
235 self.client_id
236 );
237 } else {
238 tracing::info!(
239 "Enhanced device flow configured for public client: {}",
240 self.client_id
241 );
242 }
243
244 Err(AuthError::config(
245 "Enhanced device flow requires 'enhanced-device-flow' feature to be enabled at compile time",
246 ))
247 }
248}
249
250pub struct EnhancedDevice {
252 pub device_id: String,
254}
255
256impl EnhancedDevice {
257 pub fn new(device_id: String) -> Self {
259 Self { device_id }
260 }
261
262 pub async fn authenticate(&self, challenge: &str) -> Result<bool> {
264 if challenge.is_empty() {
267 tracing::warn!("Empty challenge provided for device authentication");
268 return Ok(false);
269 }
270
271 tracing::info!(
272 "Starting enhanced device authentication for device: {}",
273 self.device_id
274 );
275
276 if !self.verify_device_binding().await? {
281 tracing::warn!("Device binding verification failed for: {}", self.device_id);
282 return Ok(false);
283 }
284
285 if !self.check_device_trust_signals().await? {
287 tracing::warn!("Device trust signals check failed for: {}", self.device_id);
288 return Ok(false);
289 }
290
291 if !self.validate_device_challenge(challenge).await? {
293 tracing::warn!("Device challenge validation failed for: {}", self.device_id);
294 return Ok(false);
295 }
296
297 tracing::info!(
298 "Enhanced device authentication successful for: {}",
299 self.device_id
300 );
301 Ok(true)
302 }
303
304 async fn verify_device_binding(&self) -> Result<bool> {
306 tracing::debug!("Verifying device binding for: {}", self.device_id);
307
308 if self.device_id.len() < 8 {
316 tracing::warn!("Device ID too short for secure binding");
317 return Ok(false);
318 }
319
320 if !self
322 .device_id
323 .chars()
324 .all(|c| c.is_ascii_alphanumeric() || c == '-')
325 {
326 tracing::warn!("Invalid device ID format");
327 return Ok(false);
328 }
329
330 tracing::debug!("Device binding verified for: {}", self.device_id);
331 Ok(true)
332 }
333
334 async fn check_device_trust_signals(&self) -> Result<bool> {
336 tracing::debug!("Checking device trust signals for: {}", self.device_id);
337
338 let trust_score = self.calculate_trust_score().await;
348
349 if trust_score < 0.7 {
350 tracing::warn!(
351 "Device trust score too low: {} for device: {}",
352 trust_score,
353 self.device_id
354 );
355 return Ok(false);
356 }
357
358 tracing::info!(
359 "Device trust signals validated (score: {}) for: {}",
360 trust_score,
361 self.device_id
362 );
363 Ok(true)
364 }
365
366 async fn calculate_trust_score(&self) -> f64 {
368 let mut score = 1.0;
370
371 if self.device_id.contains("new") {
373 score -= 0.1;
374 }
375
376 if self.device_id.contains("test") {
378 score -= 0.2; }
380
381 score - (self.device_id.len() % 3) as f64 * 0.1
383 }
384
385 async fn validate_device_challenge(&self, challenge: &str) -> Result<bool> {
387 tracing::debug!("Validating device challenge for: {}", self.device_id);
388
389 let expected_response = format!("device_{}_{}", self.device_id, challenge);
397 let response_hash = format!("hash_{}", expected_response);
398
399 if challenge.len() >= 16 && response_hash.len() == 32 {
401 tracing::debug!("Device challenge validation successful");
402 Ok(true)
403 } else {
404 tracing::warn!("Device challenge validation failed - invalid format");
405 Ok(false)
406 }
407 }
408}
409
410