steamkit/steam/authentication/
auth_session.rs1use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Weak};
8use std::time::Duration;
9
10use steamkit_protos::steam::{
11 CAuthenticationAllowedConfirmation, CAuthenticationPollAuthSessionStatusResponse,
12 EAuthSessionGuardType,
13};
14use tokio_util::sync::CancellationToken;
15
16use crate::base::generated::steam_language::EResult;
17use crate::error::SteamError;
18use crate::steam::steam_client::steam_client::SteamClient;
19use crate::types::steam_id::SteamID;
20
21use super::auth_poll_result::AuthPollResult;
22use super::authentication_exception::AuthenticationException;
23use super::i_authenticator::IAuthenticator;
24use super::qr_auth_session::QrAuthSessionState;
25
26pub(crate) enum SessionKind {
28 Credentials {
30 steam_id: SteamID,
32 },
33
34 Qr {
36 state: Arc<QrAuthSessionState>,
38 },
39}
40
41pub struct AuthSession {
43 client: Weak<SteamClient>,
44 authenticator: Option<Arc<dyn IAuthenticator>>,
45 client_id: AtomicU64,
46 request_id: Vec<u8>,
47 allowed_confirmations: Vec<CAuthenticationAllowedConfirmation>,
48 polling_interval: Duration,
49 kind: SessionKind,
50}
51
52impl std::fmt::Debug for AuthSession {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("AuthSession")
55 .field("client_id", &self.client_id())
56 .field("request_id", &self.request_id)
57 .field("polling_interval", &self.polling_interval)
58 .field(
59 "allowed_confirmations",
60 &self.allowed_confirmations.len(),
61 )
62 .finish()
63 }
64}
65
66impl AuthSession {
67 pub(crate) fn new(
69 client: Weak<SteamClient>,
70 authenticator: Option<Arc<dyn IAuthenticator>>,
71 client_id: u64,
72 request_id: Vec<u8>,
73 allowed_confirmations: Vec<CAuthenticationAllowedConfirmation>,
74 polling_interval: f32,
75 kind: SessionKind,
76 ) -> Self {
77 AuthSession {
78 client,
79 authenticator,
80 client_id: AtomicU64::new(client_id),
81 request_id,
82 allowed_confirmations: sort_confirmations(allowed_confirmations),
83 polling_interval: Duration::from_secs_f64(f64::from(polling_interval)),
84 kind,
85 }
86 }
87
88 pub fn authenticator(&self) -> Option<&Arc<dyn IAuthenticator>> {
90 self.authenticator.as_ref()
91 }
92
93 pub fn client_id(&self) -> u64 {
95 self.client_id.load(Ordering::Relaxed)
96 }
97
98 pub fn request_id(&self) -> &[u8] {
100 &self.request_id
101 }
102
103 pub fn polling_interval(&self) -> Duration {
105 self.polling_interval
106 }
107
108 pub fn allowed_confirmations(&self) -> &[CAuthenticationAllowedConfirmation] {
110 &self.allowed_confirmations
111 }
112
113 pub(crate) fn steam_id(&self) -> SteamID {
116 match &self.kind {
117 SessionKind::Credentials { steam_id } => *steam_id,
118 SessionKind::Qr { .. } => SteamID::new(),
119 }
120 }
121
122 pub async fn polling_wait_for_result(
125 &self,
126 cancellation: CancellationToken,
127 ) -> Result<AuthPollResult, SteamError> {
128 let mut poll_loop = false;
129
130 let Some(first_confirmation) = self.allowed_confirmations.first().cloned() else {
131 return Err(SteamError::InvalidArgument(
132 "There are no allowed confirmations".to_string(),
133 ));
134 };
135
136 let mut preferred_confirmation = first_confirmation;
137
138 if confirmation_type(&preferred_confirmation)
139 == EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown
140 {
141 return Err(SteamError::InvalidArgument(
142 "There are no allowed confirmations".to_string(),
143 ));
144 }
145
146 if let Some(authenticator) = self.authenticator.as_ref() {
150 if confirmation_type(&preferred_confirmation)
151 == EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation
152 {
153 let prefers_to_poll_for_confirmation =
154 authenticator.accept_device_confirmation().await?;
155
156 if !prefers_to_poll_for_confirmation {
157 if self.allowed_confirmations.len() <= 1 {
158 return Err(SteamError::InvalidArgument(
159 "AcceptDeviceConfirmation returned false which indicates a fallback to another confirmation type, but there are no other confirmation types available.".to_string(),
160 ));
161 }
162
163 preferred_confirmation = self.allowed_confirmations[1].clone();
164 }
165 }
166 }
167
168 let guard_type = confirmation_type(&preferred_confirmation);
169
170 match guard_type {
171 EAuthSessionGuardType::KEAuthSessionGuardTypeNone => {}
173
174 EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode
176 | EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode => {
177 let Some(authenticator) = self.authenticator.as_ref() else {
178 return Err(SteamError::InvalidArgument(
179 "This account requires an authenticator for login, but none was provided in AuthSessionDetails.".to_string(),
180 ));
181 };
182
183 let expected_invalid_code_result = match guard_type {
184 EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode => {
185 EResult::InvalidLoginAuthCode
186 }
187 EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode => {
188 EResult::TwoFactorCodeMismatch
189 }
190 other => {
191 return Err(SteamError::InvalidArgument(format!(
192 "Unsupported confirmation type {other:?}."
193 )))
194 }
195 };
196
197 let mut previous_code_was_incorrect = false;
198 let mut waiting_for_valid_code = true;
199
200 while waiting_for_valid_code {
201 if cancellation.is_cancelled() { return Err(SteamError::Cancelled); }
202
203 let code = match guard_type {
204 EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode => {
205 let email = preferred_confirmation
206 .associated_message
207 .as_deref()
208 .unwrap_or_default();
209
210 authenticator
211 .get_email_code(email, previous_code_was_incorrect)
212 .await?
213 }
214 EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode => {
215 authenticator
216 .get_device_code(previous_code_was_incorrect)
217 .await?
218 }
219 other => {
220 return Err(SteamError::InvalidArgument(format!(
221 "Unsupported confirmation type {other:?}."
222 )))
223 }
224 };
225
226 if cancellation.is_cancelled() { return Err(SteamError::Cancelled); }
227
228 if code.is_empty() {
229 return Err(SteamError::InvalidArgument(
230 "No code was provided by the authenticator.".to_string(),
231 ));
232 }
233
234 match self.send_steam_guard_code(&code, guard_type).await {
235 Ok(()) => waiting_for_valid_code = false,
236 Err(error) => {
237 if error.result() == Some(expected_invalid_code_result) {
238 previous_code_was_incorrect = true;
239 } else {
240 return Err(error.into());
241 }
242 }
243 }
244 }
245 }
246
247 EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation => {
249 poll_loop = true;
250 }
251
252 other => {
253 return Err(SteamError::InvalidArgument(format!(
254 "Unsupported confirmation type {other:?}."
255 )))
256 }
257 }
258
259 if !poll_loop {
260 if cancellation.is_cancelled() { return Err(SteamError::Cancelled); }
261
262 let poll_response = self
263 .poll_auth_session_status_raw()
264 .await?
265 .ok_or_else(|| {
266 SteamError::Authentication(
267 AuthenticationException::failed("Authentication failed", EResult::Fail)
268 .to_string(),
269 )
270 })?;
271
272 return Ok(poll_response);
273 }
274
275 loop {
276 tokio::select! {
277 _ = cancellation.cancelled() => return Err(SteamError::Cancelled),
278 _ = tokio::time::sleep(self.polling_interval) => {}
279 }
280
281 match self.poll_auth_session_status_raw().await {
282 Ok(Some(poll_response)) => return Ok(poll_response),
283 Ok(None) => {}
284 Err(error) => {
285 if error.result().is_some() {
290 return Err(error.into());
291 }
292 }
293 }
294 }
295 }
296
297 pub async fn poll_auth_session_status(&self) -> Result<Option<AuthPollResult>, SteamError> {
303 self.poll_auth_session_status_raw().await.map_err(Into::into)
304 }
305
306 pub(crate) async fn poll_auth_session_status_raw(
309 &self,
310 ) -> Result<Option<AuthPollResult>, AuthenticationException> {
311 let client = self.client.upgrade().ok_or_else(|| {
312 AuthenticationException::other("The SteamClient instance has been dropped.")
313 })?;
314
315 let response = client
316 .authentication()
317 .poll_auth_session_status(self.client_id(), self.request_id.clone())
318 .await?;
319
320 self.handle_poll_auth_session_status_response(&response);
321
322 let refresh_token = response.refresh_token.clone().unwrap_or_default();
323 if !refresh_token.is_empty() {
324 return Ok(Some(AuthPollResult::from_response(&response)));
325 }
326
327 Ok(None)
328 }
329
330 pub fn handle_poll_auth_session_status_response(
333 &self,
334 response: &CAuthenticationPollAuthSessionStatusResponse,
335 ) {
336 if let Some(new_client_id) = response.new_client_id {
337 if new_client_id != 0 {
338 self.client_id.store(new_client_id, Ordering::Relaxed);
339 }
340 }
341
342 if let SessionKind::Qr { state } = &self.kind {
343 if let Some(challenge_url) = &response.new_challenge_url {
344 if !challenge_url.is_empty() {
345 state.set_challenge_url(challenge_url.clone());
346 }
347 }
348 }
349 }
350
351 pub(crate) async fn send_steam_guard_code(
353 &self,
354 code: &str,
355 code_type: EAuthSessionGuardType,
356 ) -> Result<(), AuthenticationException> {
357 let SessionKind::Credentials { steam_id } = &self.kind else {
358 return Err(AuthenticationException::other(format!(
359 "Got {code_type:?} confirmation type in a session that is not CredentialsAuthSession."
360 )));
361 };
362
363 let client = self.client.upgrade().ok_or_else(|| {
364 AuthenticationException::other("The SteamClient instance has been dropped.")
365 })?;
366
367 client
368 .authentication()
369 .send_steam_guard_code(self.client_id(), *steam_id, code, code_type)
370 .await
371 }
372}
373
374fn confirmation_type(confirmation: &CAuthenticationAllowedConfirmation) -> EAuthSessionGuardType {
376 match confirmation.confirmation_type {
377 Some(1) => EAuthSessionGuardType::KEAuthSessionGuardTypeNone,
378 Some(2) => EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
379 Some(3) => EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode,
380 Some(4) => EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation,
381 Some(5) => EAuthSessionGuardType::KEAuthSessionGuardTypeEmailConfirmation,
382 Some(6) => EAuthSessionGuardType::KEAuthSessionGuardTypeMachineToken,
383 Some(7) => EAuthSessionGuardType::KEAuthSessionGuardTypeLegacyMachineAuth,
384 _ => EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown,
385 }
386}
387
388fn sort_confirmations(
390 confirmations: Vec<CAuthenticationAllowedConfirmation>,
391) -> Vec<CAuthenticationAllowedConfirmation> {
392 let preferred_confirmation_types = [
393 EAuthSessionGuardType::KEAuthSessionGuardTypeNone,
394 EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation,
395 EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode,
396 EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
397 EAuthSessionGuardType::KEAuthSessionGuardTypeEmailConfirmation,
398 EAuthSessionGuardType::KEAuthSessionGuardTypeMachineToken,
399 EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown,
400 ];
401
402 let mut sorted = confirmations;
403 sorted.sort_by_key(|confirmation| {
404 let confirmation_type = confirmation_type(confirmation);
405
406 preferred_confirmation_types
407 .iter()
408 .position(|preferred| *preferred == confirmation_type)
409 .unwrap_or(usize::MAX)
410 });
411
412 sorted
413}