use steam_protos::{EAuthSessionGuardType, EAuthTokenPlatformType};
use steamid::SteamID;
use crate::{
error::SessionError,
helpers::decode_jwt,
types::{AllowedConfirmation, ValidAction},
};
#[derive(Debug, Clone)]
pub struct ValidatedRefreshToken {
pub steam_id: SteamID,
pub token: String,
}
pub fn validate_refresh_token(token: &str, platform_type: EAuthTokenPlatformType) -> Result<ValidatedRefreshToken, SessionError> {
let decoded = decode_jwt(token)?;
if !decoded.aud.contains(&"derive".to_string()) {
return Err(SessionError::TokenError("Provided token is an access token, not a refresh token".into()));
}
let required_audience = match platform_type {
EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient => "client",
EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp => "mobile",
EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser => "web",
_ => "unknown",
};
if !decoded.aud.contains(&required_audience.to_string()) {
return Err(SessionError::TokenError(format!("Token platform type mismatch (required audience \"{}\")", required_audience)));
}
let steam_id64: u64 = decoded.sub.parse().map_err(|_| SessionError::TokenError("Invalid SteamID in token".into()))?;
Ok(ValidatedRefreshToken { steam_id: SteamID::from(steam_id64), token: token.to_string() })
}
pub fn validate_access_token(token: &str, existing_steam_id: Option<&SteamID>) -> Result<String, SessionError> {
let decoded = decode_jwt(token)?;
if decoded.aud.contains(&"derive".to_string()) {
return Err(SessionError::TokenError("Provided token is a refresh token, not an access token".into()));
}
if let Some(existing) = existing_steam_id {
let steam_id64: u64 = decoded.sub.parse().map_err(|_| SessionError::TokenError("Invalid SteamID in token".into()))?;
if existing.steam_id64() != steam_id64 {
return Err(SessionError::TokenError("Token belongs to a different account".into()));
}
}
Ok(token.to_string())
}
pub fn determine_valid_actions(confirmations: &[AllowedConfirmation]) -> Option<Vec<ValidAction>> {
let mut valid_actions = Vec::new();
for confirmation in confirmations {
match confirmation.confirmation_type {
EAuthSessionGuardType::KEAuthSessionGuardTypeNone => {
return None;
}
EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode | EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode => {
valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: confirmation.message.clone() });
}
EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation | EAuthSessionGuardType::KEAuthSessionGuardTypeEmailConfirmation => {
valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: None });
}
_ => {}
}
}
if valid_actions.is_empty() {
None
} else {
Some(valid_actions)
}
}
pub fn generate_session_id(random_bytes: &[u8]) -> String {
random_bytes.iter().take(24).map(|b| format!("{:x}", b % 16)).collect()
}
#[derive(Debug, Clone)]
pub struct ProcessConfirmationsResult {
pub requires_action: bool,
pub valid_actions: Option<Vec<ValidAction>>,
pub should_submit_presupplied_code: bool,
pub qr_challenge_url: Option<String>,
}
pub fn process_confirmations(confirmations: &[AllowedConfirmation], challenge_url: Option<String>, has_presupplied_code: bool) -> ProcessConfirmationsResult {
let mut valid_actions = Vec::new();
let mut should_submit_presupplied_code = false;
for confirmation in confirmations {
match confirmation.confirmation_type {
EAuthSessionGuardType::KEAuthSessionGuardTypeNone => {
return ProcessConfirmationsResult { requires_action: false, valid_actions: None, should_submit_presupplied_code: false, qr_challenge_url: None };
}
EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode | EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode => {
if has_presupplied_code {
should_submit_presupplied_code = true;
}
valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: confirmation.message.clone() });
}
EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation | EAuthSessionGuardType::KEAuthSessionGuardTypeEmailConfirmation => {
valid_actions.push(ValidAction { guard_type: confirmation.confirmation_type, detail: None });
}
_ => {}
}
}
if valid_actions.is_empty() {
ProcessConfirmationsResult {
requires_action: true,
valid_actions: None,
should_submit_presupplied_code: false,
qr_challenge_url: challenge_url,
}
} else {
ProcessConfirmationsResult {
requires_action: !should_submit_presupplied_code,
valid_actions: Some(valid_actions),
should_submit_presupplied_code,
qr_challenge_url: challenge_url,
}
}
}
pub fn determine_required_code_type(confirmations: &[AllowedConfirmation]) -> Option<EAuthSessionGuardType> {
let needs_email = confirmations.iter().any(|c| c.confirmation_type == EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode);
let needs_totp = confirmations.iter().any(|c| c.confirmation_type == EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode);
if needs_email {
Some(EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode)
} else if needs_totp {
Some(EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode)
} else {
None
}
}
#[cfg(test)]
mod tests {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use super::*;
#[derive(serde::Serialize)]
struct JwtPayload<'a> {
sub: &'a str,
aud: &'a [&'a str],
}
fn make_test_jwt(sub: &str, audiences: &[&str]) -> String {
let header = URL_SAFE_NO_PAD.encode(r#"{"typ":"JWT","alg":"EdDSA"}"#);
let payload = JwtPayload { sub, aud: audiences };
let payload_json = serde_json::to_string(&payload).unwrap();
let payload_encoded = URL_SAFE_NO_PAD.encode(payload_json);
format!("{}.{}.fake_signature", header, payload_encoded)
}
#[test]
fn test_validate_refresh_token_valid() {
let token = make_test_jwt("76561198000000000", &["derive", "client"]);
let result = validate_refresh_token(&token, EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient);
assert!(result.is_ok());
let validated = result.unwrap();
assert_eq!(validated.steam_id.steam_id64(), 76561198000000000);
}
#[test]
fn test_validate_refresh_token_rejects_access_token() {
let token = make_test_jwt("76561198000000000", &["client"]); let result = validate_refresh_token(&token, EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SessionError::TokenError(_)));
}
#[test]
fn test_validate_refresh_token_platform_mismatch() {
let token = make_test_jwt("76561198000000000", &["derive", "web"]);
let result = validate_refresh_token(
&token,
EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient, );
assert!(result.is_err());
}
#[test]
fn test_validate_access_token_valid() {
let token = make_test_jwt("76561198000000000", &["client"]);
let result = validate_access_token(&token, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_access_token_rejects_refresh_token() {
let token = make_test_jwt("76561198000000000", &["derive", "client"]);
let result = validate_access_token(&token, None);
assert!(result.is_err());
}
#[test]
fn test_validate_access_token_steam_id_mismatch() {
let token = make_test_jwt("76561198000000001", &["client"]);
let existing = SteamID::from(76561198000000000u64);
let result = validate_access_token(&token, Some(&existing));
assert!(result.is_err());
}
#[test]
fn test_validate_access_token_steam_id_matches() {
let token = make_test_jwt("76561198000000000", &["client"]);
let existing = SteamID::from(76561198000000000u64);
let result = validate_access_token(&token, Some(&existing));
assert!(result.is_ok());
}
#[test]
fn test_determine_valid_actions_none_guard() {
let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeNone, message: None }];
let result = determine_valid_actions(&confirmations);
assert!(result.is_none());
}
#[test]
fn test_determine_valid_actions_email_code() {
let confirmations = vec![AllowedConfirmation {
confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
message: Some("test@example.com".to_string()),
}];
let result = determine_valid_actions(&confirmations);
assert!(result.is_some());
let actions = result.unwrap();
assert_eq!(actions.len(), 1);
assert_eq!(actions[0].guard_type, EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode);
}
#[test]
fn test_generate_session_id() {
let bytes: Vec<u8> = (0..24).collect();
let session_id = generate_session_id(&bytes);
assert_eq!(session_id.len(), 24);
assert_eq!(&session_id[..16], "0123456789abcdef");
}
#[test]
fn test_generate_session_id_deterministic() {
let bytes = vec![0x0a, 0x0b, 0x0c, 0x0d];
let id1 = generate_session_id(&bytes);
let id2 = generate_session_id(&bytes);
assert_eq!(id1, id2);
}
#[test]
fn test_process_confirmations_no_guard_required() {
let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeNone, message: None }];
let result = process_confirmations(&confirmations, None, false);
assert!(!result.requires_action);
assert!(result.valid_actions.is_none());
assert!(!result.should_submit_presupplied_code);
assert!(result.qr_challenge_url.is_none());
}
#[test]
fn test_process_confirmations_email_code_required() {
let confirmations = vec![AllowedConfirmation {
confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
message: Some("t***@example.com".to_string()),
}];
let result = process_confirmations(&confirmations, None, false);
assert!(result.requires_action);
assert!(result.valid_actions.is_some());
let actions = result.valid_actions.unwrap();
assert_eq!(actions.len(), 1);
assert_eq!(actions[0].guard_type, EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode);
assert_eq!(actions[0].detail, Some("t***@example.com".to_string()));
assert!(!result.should_submit_presupplied_code);
}
#[test]
fn test_process_confirmations_with_presupplied_code() {
let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None }];
let result = process_confirmations(&confirmations, None, true);
assert!(!result.requires_action);
assert!(result.valid_actions.is_some());
assert!(result.should_submit_presupplied_code);
}
#[test]
fn test_process_confirmations_device_confirmation() {
let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation, message: None }];
let result = process_confirmations(&confirmations, Some("https://qr.example.com".to_string()), false);
assert!(result.requires_action);
let actions = result.valid_actions.unwrap();
assert_eq!(actions.len(), 1);
assert_eq!(actions[0].guard_type, EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation);
assert_eq!(result.qr_challenge_url, Some("https://qr.example.com".to_string()));
}
#[test]
fn test_process_confirmations_multiple_options() {
let confirmations = vec![
AllowedConfirmation {
confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
message: Some("t***@example.com".to_string()),
},
AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None },
];
let result = process_confirmations(&confirmations, None, false);
assert!(result.requires_action);
let actions = result.valid_actions.unwrap();
assert_eq!(actions.len(), 2);
}
#[test]
fn test_process_confirmations_empty_list() {
let confirmations: Vec<AllowedConfirmation> = vec![];
let result = process_confirmations(&confirmations, None, false);
assert!(result.requires_action);
assert!(result.valid_actions.is_none());
}
#[test]
fn test_determine_required_code_type_email() {
let confirmations = vec![AllowedConfirmation {
confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
message: Some("t***@example.com".to_string()),
}];
let result = determine_required_code_type(&confirmations);
assert_eq!(result, Some(EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode));
}
#[test]
fn test_determine_required_code_type_totp() {
let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None }];
let result = determine_required_code_type(&confirmations);
assert_eq!(result, Some(EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode));
}
#[test]
fn test_determine_required_code_type_email_priority() {
let confirmations = vec![
AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode, message: None },
AllowedConfirmation {
confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
message: Some("t***@example.com".to_string()),
},
];
let result = determine_required_code_type(&confirmations);
assert_eq!(result, Some(EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode));
}
#[test]
fn test_determine_required_code_type_none() {
let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeNone, message: None }];
let result = determine_required_code_type(&confirmations);
assert!(result.is_none());
}
#[test]
fn test_determine_required_code_type_device_confirmation_not_code() {
let confirmations = vec![AllowedConfirmation { confirmation_type: EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation, message: None }];
let result = determine_required_code_type(&confirmations);
assert!(result.is_none());
}
}