1use steam_protos::{EAuthSessionGuardType, EAuthTokenPlatformType};
7use steamid::SteamID;
8
9use crate::{
10 error::SessionError,
11 helpers::decode_jwt,
12 types::{AllowedConfirmation, ValidAction},
13};
14
15#[derive(Debug, Clone)]
17pub struct ValidatedRefreshToken {
18 pub steam_id: SteamID,
20 pub token: String,
22}
23
24pub fn validate_refresh_token(token: &str, platform_type: EAuthTokenPlatformType) -> Result<ValidatedRefreshToken, SessionError> {
34 let decoded = decode_jwt(token)?;
35
36 if !decoded.aud.contains(&"derive".to_string()) {
38 return Err(SessionError::TokenError("Provided token is an access token, not a refresh token".into()));
39 }
40
41 let required_audience = match platform_type {
43 EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient => "client",
44 EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp => "mobile",
45 EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser => "web",
46 _ => "unknown",
47 };
48
49 if !decoded.aud.contains(&required_audience.to_string()) {
50 return Err(SessionError::TokenError(format!("Token platform type mismatch (required audience \"{}\")", required_audience)));
51 }
52
53 let steam_id64: u64 = decoded.sub.parse().map_err(|_| SessionError::TokenError("Invalid SteamID in token".into()))?;
55
56 Ok(ValidatedRefreshToken { steam_id: SteamID::from(steam_id64), token: token.to_string() })
57}
58
59pub fn validate_access_token(token: &str, existing_steam_id: Option<&SteamID>) -> Result<String, SessionError> {
69 let decoded = decode_jwt(token)?;
70
71 if decoded.aud.contains(&"derive".to_string()) {
73 return Err(SessionError::TokenError("Provided token is a refresh token, not an access token".into()));
74 }
75
76 if let Some(existing) = existing_steam_id {
78 let steam_id64: u64 = decoded.sub.parse().map_err(|_| SessionError::TokenError("Invalid SteamID in token".into()))?;
79 if existing.steam_id64() != steam_id64 {
80 return Err(SessionError::TokenError("Token belongs to a different account".into()));
81 }
82 }
83
84 Ok(token.to_string())
85}
86
87pub fn determine_valid_actions(confirmations: &[AllowedConfirmation]) -> Option<Vec<ValidAction>> {
96 let mut valid_actions = Vec::new();
97
98 for confirmation in confirmations {
99 match confirmation.confirmation_type {
100 EAuthSessionGuardType::KEAuthSessionGuardTypeNone => {
101 return None;
103 }
104 EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode | EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode => {
105 valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: confirmation.message.clone() });
106 }
107 EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation | EAuthSessionGuardType::KEAuthSessionGuardTypeEmailConfirmation => {
108 valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: None });
109 }
110 _ => {}
111 }
112 }
113
114 if valid_actions.is_empty() {
115 None
116 } else {
117 Some(valid_actions)
118 }
119}
120
121pub fn generate_session_id(random_bytes: &[u8]) -> String {
129 random_bytes.iter().take(24).map(|b| format!("{:x}", b % 16)).collect()
130}
131
132#[derive(Debug, Clone)]
137pub struct ProcessConfirmationsResult {
138 pub requires_action: bool,
140 pub valid_actions: Option<Vec<ValidAction>>,
142 pub should_submit_presupplied_code: bool,
144 pub qr_challenge_url: Option<String>,
146}
147
148pub fn process_confirmations(confirmations: &[AllowedConfirmation], challenge_url: Option<String>, has_presupplied_code: bool) -> ProcessConfirmationsResult {
175 let mut valid_actions = Vec::new();
176 let mut should_submit_presupplied_code = false;
177
178 for confirmation in confirmations {
179 match confirmation.confirmation_type {
180 EAuthSessionGuardType::KEAuthSessionGuardTypeNone => {
181 return ProcessConfirmationsResult { requires_action: false, valid_actions: None, should_submit_presupplied_code: false, qr_challenge_url: None };
183 }
184 EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode | EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode => {
185 if has_presupplied_code {
187 should_submit_presupplied_code = true;
188 }
189 valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: confirmation.message.clone() });
190 }
191 EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation | EAuthSessionGuardType::KEAuthSessionGuardTypeEmailConfirmation => {
192 valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: None });
193 }
194 _ => {}
195 }
196 }
197
198 if valid_actions.is_empty() {
199 ProcessConfirmationsResult {
201 requires_action: true,
202 valid_actions: None,
203 should_submit_presupplied_code: false,
204 qr_challenge_url: challenge_url,
205 }
206 } else {
207 ProcessConfirmationsResult {
208 requires_action: !should_submit_presupplied_code,
209 valid_actions: Some(valid_actions),
210 should_submit_presupplied_code,
211 qr_challenge_url: challenge_url,
212 }
213 }
214}
215
216pub fn determine_required_code_type(confirmations: &[AllowedConfirmation]) -> Option<EAuthSessionGuardType> {
241 let needs_email = confirmations.iter().any(|c| c.confirmation_type == EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode);
242 let needs_totp = confirmations.iter().any(|c| c.confirmation_type == EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode);
243
244 if needs_email {
245 Some(EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode)
246 } else if needs_totp {
247 Some(EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode)
248 } else {
249 None
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
256
257 use super::*;
258
259 #[derive(serde::Serialize)]
260 struct JwtPayload<'a> {
261 sub: &'a str,
262 aud: &'a [&'a str],
263 }
264
265 fn make_test_jwt(sub: &str, audiences: &[&str]) -> String {
267 let header = URL_SAFE_NO_PAD.encode(r#"{"typ":"JWT","alg":"EdDSA"}"#);
268
269 let payload = JwtPayload { sub, aud: audiences };
270
271 let payload_json = serde_json::to_string(&payload).unwrap();
274 let payload_encoded = URL_SAFE_NO_PAD.encode(payload_json);
275 format!("{}.{}.fake_signature", header, payload_encoded)
276 }
277
278 #[test]
279 fn test_validate_refresh_token_valid() {
280 let token = make_test_jwt("76561198000000000", &["derive", "client"]);
281 let result = validate_refresh_token(&token, EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient);
282
283 assert!(result.is_ok());
284 let validated = result.unwrap();
285 assert_eq!(validated.steam_id.steam_id64(), 76561198000000000);
286 }
287
288 #[test]
289 fn test_validate_refresh_token_rejects_access_token() {
290 let token = make_test_jwt("76561198000000000", &["client"]); let result = validate_refresh_token(&token, EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient);
292
293 assert!(result.is_err());
294 assert!(matches!(result.unwrap_err(), SessionError::TokenError(_)));
295 }
296
297 #[test]
298 fn test_validate_refresh_token_platform_mismatch() {
299 let token = make_test_jwt("76561198000000000", &["derive", "web"]);
300 let result = validate_refresh_token(
301 &token,
302 EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient, );
304
305 assert!(result.is_err());
306 }
307
308 #[test]
309 fn test_validate_access_token_valid() {
310 let token = make_test_jwt("76561198000000000", &["client"]);
311 let result = validate_access_token(&token, None);
312
313 assert!(result.is_ok());
314 }
315
316 #[test]
317 fn test_validate_access_token_rejects_refresh_token() {
318 let token = make_test_jwt("76561198000000000", &["derive", "client"]);
319 let result = validate_access_token(&token, None);
320
321 assert!(result.is_err());
322 }
323
324 #[test]
325 fn test_validate_access_token_steam_id_mismatch() {
326 let token = make_test_jwt("76561198000000001", &["client"]);
327 let existing = SteamID::from(76561198000000000u64);
328 let result = validate_access_token(&token, Some(&existing));
329
330 assert!(result.is_err());
331 }
332
333 #[test]
334 fn test_validate_access_token_steam_id_matches() {
335 let token = make_test_jwt("76561198000000000", &["client"]);
336 let existing = SteamID::from(76561198000000000u64);
337 let result = validate_access_token(&token, Some(&existing));
338
339 assert!(result.is_ok());
340 }
341
342 #[test]
343 fn test_determine_valid_actions_none_guard() {
344 let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeNone, message: None }];
345
346 let result = determine_valid_actions(&confirmations);
347 assert!(result.is_none());
348 }
349
350 #[test]
351 fn test_determine_valid_actions_email_code() {
352 let confirmations = vec![AllowedConfirmation {
353 confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
354 message: Some("test@example.com".to_string()),
355 }];
356
357 let result = determine_valid_actions(&confirmations);
358 assert!(result.is_some());
359 let actions = result.unwrap();
360 assert_eq!(actions.len(), 1);
361 assert_eq!(actions[0].guard_type, EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode);
362 }
363
364 #[test]
365 fn test_generate_session_id() {
366 let bytes: Vec<u8> = (0..24).collect();
367 let session_id = generate_session_id(&bytes);
368
369 assert_eq!(session_id.len(), 24);
370 assert_eq!(&session_id[..16], "0123456789abcdef");
372 }
373
374 #[test]
375 fn test_generate_session_id_deterministic() {
376 let bytes = vec![0x0a, 0x0b, 0x0c, 0x0d];
377 let id1 = generate_session_id(&bytes);
378 let id2 = generate_session_id(&bytes);
379
380 assert_eq!(id1, id2);
381 }
382
383 #[test]
386 fn test_process_confirmations_no_guard_required() {
387 let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeNone, message: None }];
388
389 let result = process_confirmations(&confirmations, None, false);
390
391 assert!(!result.requires_action);
392 assert!(result.valid_actions.is_none());
393 assert!(!result.should_submit_presupplied_code);
394 assert!(result.qr_challenge_url.is_none());
395 }
396
397 #[test]
398 fn test_process_confirmations_email_code_required() {
399 let confirmations = vec![AllowedConfirmation {
400 confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
401 message: Some("t***@example.com".to_string()),
402 }];
403
404 let result = process_confirmations(&confirmations, None, false);
405
406 assert!(result.requires_action);
407 assert!(result.valid_actions.is_some());
408 let actions = result.valid_actions.unwrap();
409 assert_eq!(actions.len(), 1);
410 assert_eq!(actions[0].guard_type, EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode);
411 assert_eq!(actions[0].detail, Some("t***@example.com".to_string()));
412 assert!(!result.should_submit_presupplied_code);
413 }
414
415 #[test]
416 fn test_process_confirmations_with_presupplied_code() {
417 let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None }];
418
419 let result = process_confirmations(&confirmations, None, true);
420
421 assert!(!result.requires_action);
423 assert!(result.valid_actions.is_some());
424 assert!(result.should_submit_presupplied_code);
425 }
426
427 #[test]
428 fn test_process_confirmations_device_confirmation() {
429 let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation, message: None }];
430
431 let result = process_confirmations(&confirmations, Some("https://qr.example.com".to_string()), false);
432
433 assert!(result.requires_action);
434 let actions = result.valid_actions.unwrap();
435 assert_eq!(actions.len(), 1);
436 assert_eq!(actions[0].guard_type, EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation);
437 assert_eq!(result.qr_challenge_url, Some("https://qr.example.com".to_string()));
438 }
439
440 #[test]
441 fn test_process_confirmations_multiple_options() {
442 let confirmations = vec![
443 AllowedConfirmation {
444 confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
445 message: Some("t***@example.com".to_string()),
446 },
447 AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None },
448 ];
449
450 let result = process_confirmations(&confirmations, None, false);
451
452 assert!(result.requires_action);
453 let actions = result.valid_actions.unwrap();
454 assert_eq!(actions.len(), 2);
455 }
456
457 #[test]
458 fn test_process_confirmations_empty_list() {
459 let confirmations: Vec<AllowedConfirmation> = vec![];
460
461 let result = process_confirmations(&confirmations, None, false);
462
463 assert!(result.requires_action);
465 assert!(result.valid_actions.is_none());
466 }
467
468 #[test]
471 fn test_determine_required_code_type_email() {
472 let confirmations = vec![AllowedConfirmation {
473 confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
474 message: Some("t***@example.com".to_string()),
475 }];
476
477 let result = determine_required_code_type(&confirmations);
478 assert_eq!(result, Some(EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode));
479 }
480
481 #[test]
482 fn test_determine_required_code_type_totp() {
483 let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None }];
484
485 let result = determine_required_code_type(&confirmations);
486 assert_eq!(result, Some(EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode));
487 }
488
489 #[test]
490 fn test_determine_required_code_type_email_priority() {
491 let confirmations = vec![
493 AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None },
494 AllowedConfirmation {
495 confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
496 message: Some("t***@example.com".to_string()),
497 },
498 ];
499
500 let result = determine_required_code_type(&confirmations);
501 assert_eq!(result, Some(EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode));
502 }
503
504 #[test]
505 fn test_determine_required_code_type_none() {
506 let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeNone, message: None }];
507
508 let result = determine_required_code_type(&confirmations);
509 assert!(result.is_none());
510 }
511
512 #[test]
513 fn test_determine_required_code_type_device_confirmation_not_code() {
514 let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation, message: None }];
516
517 let result = determine_required_code_type(&confirmations);
518 assert!(result.is_none());
519 }
520}