Skip to main content

steam_auth/
auth_client.rs

1//! Authentication client for Steam's auth API.
2
3use std::collections::HashMap;
4
5use prost::Message;
6use steam_protos::{
7    CAuthenticationAccessTokenGenerateForAppRequest, CAuthenticationAccessTokenGenerateForAppResponse, CAuthenticationBeginAuthSessionViaCredentialsRequest, CAuthenticationBeginAuthSessionViaCredentialsResponse, CAuthenticationBeginAuthSessionViaQRRequest, CAuthenticationBeginAuthSessionViaQRResponse, CAuthenticationDeviceDetails, CAuthenticationGetAuthSessionInfoRequest, CAuthenticationGetAuthSessionInfoResponse, CAuthenticationGetPasswordRSAPublicKeyRequest, CAuthenticationGetPasswordRSAPublicKeyResponse, CAuthenticationPollAuthSessionStatusRequest,
8    CAuthenticationPollAuthSessionStatusResponse, CAuthenticationUpdateAuthSessionWithMobileConfirmationRequest, CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest, EAuthSessionGuardType, EAuthTokenPlatformType, ESessionPersistence, ETokenRenewalType,
9};
10
11use crate::{
12    crypto::rsa_encrypt_password,
13    error::SessionError,
14    helpers::{default_user_agent, get_spoofed_hostname},
15    transport::{ApiRequest, Transport},
16    types::{AllowedConfirmation, DeviceDetails, PlatformData, StartAuthSessionResponse},
17};
18
19/// Result of RSA key fetch.
20#[derive(Debug, Clone)]
21pub struct RsaKeyResponse {
22    pub public_key_mod: String,
23    pub public_key_exp: String,
24    pub timestamp: u64,
25}
26
27/// Result of password encryption.
28#[derive(Debug, Clone)]
29pub struct EncryptedPassword {
30    pub encrypted_password: String,
31    pub key_timestamp: u64,
32}
33
34/// Poll response from auth status.
35#[derive(Debug, Clone)]
36pub struct PollLoginStatusResponse {
37    pub new_client_id: Option<u64>,
38    pub new_challenge_url: Option<String>,
39    pub refresh_token: Option<String>,
40    pub access_token: Option<String>,
41    pub had_remote_interaction: bool,
42    pub account_name: Option<String>,
43    pub new_steam_guard_machine_auth: Option<String>,
44}
45
46/// Authentication client for communicating with Steam's auth service.
47pub struct AuthenticationClient {
48    transport: Transport,
49    platform_type: EAuthTokenPlatformType,
50    web_user_agent: String,
51    machine_id: Option<Vec<u8>>,
52    client_friendly_name: Option<String>,
53}
54
55impl AuthenticationClient {
56    /// Create a new authentication client.
57    pub fn new(transport: Transport, platform_type: EAuthTokenPlatformType, machine_id: Option<Vec<u8>>, client_friendly_name: Option<String>) -> Self {
58        Self { transport, platform_type, web_user_agent: default_user_agent(), machine_id, client_friendly_name }
59    }
60
61    /// Get RSA public key for password encryption.
62    pub async fn get_rsa_key(&self, account_name: &str) -> Result<RsaKeyResponse, SessionError> {
63        let request = CAuthenticationGetPasswordRSAPublicKeyRequest { account_name: Some(account_name.to_string()) };
64
65        let response: CAuthenticationGetPasswordRSAPublicKeyResponse = self.send_request("Authentication", "GetPasswordRSAPublicKey", 1, &request, None).await?;
66
67        Ok(RsaKeyResponse {
68            public_key_mod: response.publickey_mod.unwrap_or_default(),
69            public_key_exp: response.publickey_exp.unwrap_or_default(),
70            timestamp: response.timestamp.unwrap_or(0),
71        })
72    }
73
74    /// Encrypt a password using RSA.
75    ///
76    /// This method fetches Steam's RSA public key and uses it to encrypt the
77    /// password. The actual encryption is performed by the pure function
78    /// [`rsa_encrypt_password`].
79    pub async fn encrypt_password(&self, account_name: &str, password: &str) -> Result<EncryptedPassword, SessionError> {
80        let rsa_info = self.get_rsa_key(account_name).await?;
81
82        // Use the pure encryption function from crypto module
83        let encrypted_password = rsa_encrypt_password(password, &rsa_info.public_key_mod, &rsa_info.public_key_exp)?;
84
85        Ok(EncryptedPassword { encrypted_password, key_timestamp: rsa_info.timestamp })
86    }
87
88    /// Start an auth session with credentials.
89    pub async fn start_session_with_credentials(&self, account_name: &str, encrypted_password: &str, key_timestamp: u64, persistence: ESessionPersistence, steam_guard_machine_token: Option<&str>) -> Result<StartAuthSessionResponse, SessionError> {
90        let platform_data = self.get_platform_data();
91
92        let device_details = CAuthenticationDeviceDetails {
93            device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
94            platform_type: Some(self.platform_type as i32),
95            os_type: platform_data.device_details.os_type,
96            gaming_device_type: platform_data.device_details.gaming_device_type,
97            client_count: None,
98            machine_id: platform_data.device_details.machine_id.clone(),
99            app_type: None,
100        };
101
102        let mut request = CAuthenticationBeginAuthSessionViaCredentialsRequest {
103            account_name: Some(account_name.to_string()),
104            encrypted_password: Some(encrypted_password.to_string()),
105            encryption_timestamp: Some(key_timestamp),
106            remember_login: Some(persistence == ESessionPersistence::KESessionPersistencePersistent),
107            persistence: Some(persistence as i32),
108            website_id: Some(platform_data.website_id.clone()),
109            device_details: Some(device_details),
110            device_friendly_name: None,
111            platform_type: Some(self.platform_type as i32),
112            guard_data: None,
113            language: None,
114            qos_level: Some(2),
115        };
116
117        // Add machine token if provided
118        if let Some(token) = steam_guard_machine_token {
119            request.guard_data = Some(token.to_string());
120        }
121
122        let response: CAuthenticationBeginAuthSessionViaCredentialsResponse = self.send_request("Authentication", "BeginAuthSessionViaCredentials", 1, &request, None).await?;
123
124        Ok(StartAuthSessionResponse {
125            client_id: response.client_id.unwrap_or(0),
126            request_id: response.request_id.unwrap_or_default(),
127            poll_interval: response.interval.unwrap_or(5.0),
128            allowed_confirmations: response
129                .allowed_confirmations
130                .into_iter()
131                .map(|c| AllowedConfirmation {
132                    confirmation_type: EAuthSessionGuardType::try_from(c.confirmation_type.unwrap_or(0)).unwrap_or(EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown),
133                    message: c.associated_message,
134                })
135                .collect(),
136            steam_id: response.steamid,
137            weak_token: response.weak_token,
138            challenge_url: None,
139            version: None,
140        })
141    }
142
143    /// Start a QR code auth session.
144    pub async fn start_session_with_qr(&self) -> Result<StartAuthSessionResponse, SessionError> {
145        let platform_data = self.get_platform_data();
146
147        let device_details = CAuthenticationDeviceDetails {
148            device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
149            platform_type: Some(self.platform_type as i32),
150            os_type: platform_data.device_details.os_type,
151            gaming_device_type: platform_data.device_details.gaming_device_type,
152            client_count: None,
153            machine_id: platform_data.device_details.machine_id.clone(),
154            app_type: None,
155        };
156
157        let request = CAuthenticationBeginAuthSessionViaQRRequest {
158            device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
159            platform_type: Some(self.platform_type as i32),
160            device_details: Some(device_details),
161            website_id: Some("Unknown".to_string()),
162        };
163
164        let response: CAuthenticationBeginAuthSessionViaQRResponse = self.send_request("Authentication", "BeginAuthSessionViaQR", 1, &request, None).await?;
165
166        Ok(StartAuthSessionResponse {
167            client_id: response.client_id.unwrap_or(0),
168            request_id: response.request_id.unwrap_or_default(),
169            poll_interval: response.interval.unwrap_or(5.0),
170            allowed_confirmations: response
171                .allowed_confirmations
172                .into_iter()
173                .map(|c| AllowedConfirmation {
174                    confirmation_type: EAuthSessionGuardType::try_from(c.confirmation_type.unwrap_or(0)).unwrap_or(EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown),
175                    message: c.associated_message,
176                })
177                .collect(),
178            steam_id: None,
179            weak_token: None,
180            challenge_url: response.challenge_url,
181            version: response.version,
182        })
183    }
184
185    /// Submit a Steam Guard code.
186    pub async fn submit_steam_guard_code(&self, client_id: u64, steam_id: u64, code: &str, code_type: EAuthSessionGuardType) -> Result<(), SessionError> {
187        let request = CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest {
188            client_id: Some(client_id),
189            steamid: Some(steam_id),
190            code: Some(code.to_string()),
191            code_type: Some(code_type as i32),
192        };
193
194        let _: () = self.send_request_no_response("Authentication", "UpdateAuthSessionWithSteamGuardCode", 1, &request, None).await?;
195
196        Ok(())
197    }
198
199    /// Poll the auth session status.
200    pub async fn poll_login_status(&self, client_id: u64, request_id: &[u8]) -> Result<PollLoginStatusResponse, SessionError> {
201        let request = CAuthenticationPollAuthSessionStatusRequest { client_id: Some(client_id), request_id: Some(request_id.to_vec()), token_to_revoke: None };
202
203        let response: CAuthenticationPollAuthSessionStatusResponse = self.send_request("Authentication", "PollAuthSessionStatus", 1, &request, None).await?;
204
205        Ok(PollLoginStatusResponse {
206            new_client_id: response.new_client_id,
207            new_challenge_url: response.new_challenge_url,
208            refresh_token: response.refresh_token,
209            access_token: response.access_token,
210            had_remote_interaction: response.had_remote_interaction.unwrap_or(false),
211            account_name: response.account_name,
212            new_steam_guard_machine_auth: response.new_guard_data,
213        })
214    }
215
216    /// Generate an access token from a refresh token.
217    pub async fn generate_access_token(&self, refresh_token: &str, steam_id: u64, renew: bool) -> Result<(String, Option<String>), SessionError> {
218        let request = CAuthenticationAccessTokenGenerateForAppRequest {
219            refresh_token: Some(refresh_token.to_string()),
220            steamid: Some(steam_id),
221            renewal_type: Some(if renew { ETokenRenewalType::KETokenRenewalTypeAllow as i32 } else { ETokenRenewalType::KETokenRenewalTypeNone as i32 }),
222        };
223
224        let response: CAuthenticationAccessTokenGenerateForAppResponse = self.send_request("Authentication", "GenerateAccessTokenForApp", 1, &request, None).await?;
225
226        Ok((response.access_token.unwrap_or_default(), response.refresh_token))
227    }
228
229    /// Get information about an auth session (for QR approval).
230    pub async fn get_auth_session_info(&self, access_token: &str, client_id: u64) -> Result<CAuthenticationGetAuthSessionInfoResponse, SessionError> {
231        let request = CAuthenticationGetAuthSessionInfoRequest { client_id: Some(client_id) };
232
233        self.send_request("Authentication", "GetAuthSessionInfo", 1, &request, Some(access_token)).await
234    }
235
236    /// Submit mobile confirmation to approve/deny a login.
237    #[allow(clippy::too_many_arguments)]
238    pub async fn submit_mobile_confirmation(&self, access_token: &str, version: i32, client_id: u64, steam_id: u64, signature: &[u8], confirm: bool, persistence: ESessionPersistence) -> Result<(), SessionError> {
239        let request = CAuthenticationUpdateAuthSessionWithMobileConfirmationRequest {
240            version: Some(version),
241            client_id: Some(client_id),
242            steamid: Some(steam_id),
243            signature: Some(signature.to_vec()),
244            confirm: Some(confirm),
245            persistence: Some(persistence as i32),
246        };
247
248        self.send_request_no_response("Authentication", "UpdateAuthSessionWithMobileConfirmation", 1, &request, Some(access_token)).await
249    }
250
251    /// Send a protobuf request and decode the response.
252    async fn send_request<Req: Message, Resp: Message + Default>(&self, interface: &str, method: &str, version: u32, request: &Req, access_token: Option<&str>) -> Result<Resp, SessionError> {
253        let platform_data = self.get_platform_data();
254        let request_data = request.encode_to_vec();
255
256        let api_request = ApiRequest {
257            api_interface: interface.to_string(),
258            api_method: method.to_string(),
259            api_version: version,
260            access_token: access_token.map(String::from),
261            request_data: Some(request_data),
262            headers: platform_data.headers,
263        };
264
265        let response = self.transport.send_request(api_request).await?;
266
267        // Check for errors
268        if let Some(result) = response.result {
269            if result != 1 {
270                // EResult::OK = 1
271                return Err(SessionError::from_eresult(result, response.error_message));
272            }
273        }
274
275        // Decode response
276        let response_data = response.response_data.unwrap_or_default();
277        Ok(Resp::decode(response_data.as_slice())?)
278    }
279
280    /// Send a protobuf request with no response body.
281    async fn send_request_no_response<Req: Message>(&self, interface: &str, method: &str, version: u32, request: &Req, access_token: Option<&str>) -> Result<(), SessionError> {
282        let platform_data = self.get_platform_data();
283        let request_data = request.encode_to_vec();
284
285        let api_request = ApiRequest {
286            api_interface: interface.to_string(),
287            api_method: method.to_string(),
288            api_version: version,
289            access_token: access_token.map(String::from),
290            request_data: Some(request_data),
291            headers: platform_data.headers,
292        };
293
294        let response = self.transport.send_request(api_request).await?;
295
296        // Check for errors
297        if let Some(result) = response.result {
298            if result != 1 {
299                return Err(SessionError::from_eresult(result, response.error_message));
300            }
301        }
302
303        Ok(())
304    }
305
306    /// Get platform-specific data for requests.
307    fn get_platform_data(&self) -> PlatformData {
308        match self.platform_type {
309            EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient => {
310                let machine_name = self.client_friendly_name.clone().unwrap_or_else(get_spoofed_hostname);
311
312                PlatformData {
313                    website_id: "Unknown".to_string(),
314                    headers: HashMap::from([("user-agent".to_string(), "Mozilla/5.0 (Windows; U; Windows NT 10.0; en-US; Valve Steam Client/default/1665786434; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36".to_string()), ("origin".to_string(), "https://steamloopback.host".to_string())]),
315                    device_details: DeviceDetails {
316                        device_friendly_name: machine_name,
317                        platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient,
318                        os_type: Some(20), // EOSType::Win11
319                        gaming_device_type: Some(1),
320                        machine_id: self.machine_id.clone(),
321                    },
322                }
323            }
324            EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser => PlatformData {
325                website_id: "Community".to_string(),
326                headers: HashMap::from([("user-agent".to_string(), self.web_user_agent.clone()), ("origin".to_string(), "https://steamcommunity.com".to_string()), ("referer".to_string(), "https://steamcommunity.com".to_string())]),
327                device_details: DeviceDetails {
328                    device_friendly_name: self.web_user_agent.clone(),
329                    platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser,
330                    os_type: None,
331                    gaming_device_type: None,
332                    machine_id: None,
333                },
334            },
335            EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp => PlatformData {
336                website_id: "Mobile".to_string(),
337                headers: HashMap::from([("user-agent".to_string(), "okhttp/4.9.2".to_string()), ("cookie".to_string(), "mobileClient=android; mobileClientVersion=777777 3.10.3".to_string())]),
338                device_details: DeviceDetails {
339                    device_friendly_name: "Galaxy S25".to_string(),
340                    platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp,
341                    os_type: Some(-500), // EOSType::AndroidUnknown
342                    gaming_device_type: Some(528),
343                    machine_id: None,
344                },
345            },
346            _ => PlatformData {
347                website_id: "Community".to_string(),
348                headers: HashMap::new(),
349                device_details: DeviceDetails {
350                    device_friendly_name: "Unknown".to_string(),
351                    platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeUnknown,
352                    os_type: None,
353                    gaming_device_type: None,
354                    machine_id: None,
355                },
356            },
357        }
358    }
359}