Skip to main content

steamkit/steam/authentication/
auth_session.rs

1//! An authentication session, used to finish authenticating and get the tokens.
2//!
3//! Credentials and QR sessions share [`AuthSession`]; what differs between them
4//! lives in `SessionKind`, and the public session types wrap an [`AuthSession`].
5
6use 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
26/// What differs between a credentials session and a QR session.
27pub(crate) enum SessionKind {
28    /// A session that was started with a username and a password.
29    Credentials {
30        /// The SteamID of the account that is logging on.
31        steam_id: SteamID,
32    },
33
34    /// A session that was started with a QR code.
35    Qr {
36        /// Holds the challenge URL of the session.
37        state: Arc<QrAuthSessionState>,
38    },
39}
40
41/// An authentication session, used to finish authenticating and get the tokens.
42pub 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    /// Creates an authentication session.
68    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    /// The authenticator used for 2-factor authentication, if any.
89    pub fn authenticator(&self) -> Option<&Arc<dyn IAuthenticator>> {
90        self.authenticator.as_ref()
91    }
92
93    /// Unique id of the requestor. Also used for routing, and part of the QR code.
94    pub fn client_id(&self) -> u64 {
95        self.client_id.load(Ordering::Relaxed)
96    }
97
98    /// Request id to present when polling.
99    pub fn request_id(&self) -> &[u8] {
100        &self.request_id
101    }
102
103    /// How often to call `poll_auth_session_status`.
104    pub fn polling_interval(&self) -> Duration {
105        self.polling_interval
106    }
107
108    /// The confirmation types that can confirm the request.
109    pub fn allowed_confirmations(&self) -> &[CAuthenticationAllowedConfirmation] {
110        &self.allowed_confirmations
111    }
112
113    /// The SteamID of the account logging on. QR sessions don't have one yet,
114    /// so they report an empty SteamID.
115    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    /// Handles any 2-factor authentication and polls until authentication
123    /// succeeds.
124    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        // With an authenticator and device confirmation available, let the
147        // authenticator decide: wait for the confirmation in the app, or fall
148        // back to the next confirmation type.
149        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            // No Steam Guard.
172            EAuthSessionGuardType::KEAuthSessionGuardTypeNone => {}
173
174            // 2-factor code from the authenticator app, or sent by email.
175            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            // The prompt in the Steam mobile app.
248            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                    // An error with a Steam result code is Steam rejecting the
286                    // session, so stop. Without one it's a transport error (the
287                    // CM connection dropped mid-request). The auth session lives
288                    // on Steam's side and survives that, so just poll again.
289                    if error.result().is_some() {
290                        return Err(error.into());
291                    }
292                }
293            }
294        }
295    }
296
297    /// Polls the authentication status once. You probably want
298    /// [`AuthSession::polling_wait_for_result`] instead.
299    ///
300    /// Returns the tokens for logging on, or `None` if the session isn't
301    /// authenticated yet.
302    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    /// Polls the authentication status once, keeping the Steam result code of
307    /// a failure.
308    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    /// Handles a poll response. QR sessions also pick up a refreshed challenge
331    /// URL here (see `SessionKind::Qr`).
332    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    /// Sends a Steam Guard code for this session.
352    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
374/// Reads the confirmation type of an allowed confirmation.
375fn 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
388/// Sorts the available confirmation types by preference.
389fn 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}