grammers_client/client/
auth.rs

1// Copyright 2020 - developers of the `grammers` project.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8use super::Client;
9use crate::types::{LoginToken, PasswordToken, TermsOfService, User};
10use crate::utils;
11use grammers_crypto::two_factor_auth::{calculate_2fa, check_p_and_g};
12pub use grammers_mtsender::InvocationError;
13use grammers_session::defs::{PeerInfo, UpdateState, UpdatesState};
14use grammers_tl_types as tl;
15use std::fmt;
16
17/// The error type which is returned when signing in fails.
18#[derive(Debug)]
19#[allow(clippy::large_enum_variant)]
20pub enum SignInError {
21    SignUpRequired {
22        terms_of_service: Option<TermsOfService>,
23    },
24    PasswordRequired(PasswordToken),
25    InvalidCode,
26    InvalidPassword,
27    Other(InvocationError),
28}
29
30impl fmt::Display for SignInError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        use SignInError::*;
33        match self {
34            SignUpRequired {
35                terms_of_service: tos,
36            } => write!(
37                f,
38                "sign in error: sign up with official client required: {tos:?}"
39            ),
40            PasswordRequired(_password) => write!(f, "2fa password required"),
41            InvalidCode => write!(f, "sign in error: invalid code"),
42            InvalidPassword => write!(f, "invalid password"),
43            Other(e) => write!(f, "sign in error: {e}"),
44        }
45    }
46}
47
48impl std::error::Error for SignInError {}
49
50/// Method implementations related with the authentication of the user into the API.
51///
52/// Most requests to the API require the user to have authorized their key, stored in the session,
53/// before being able to use them.
54impl Client {
55    /// Returns `true` if the current account is authorized. Otherwise,
56    /// logging in will be required before being able to invoke requests.
57    ///
58    /// This will likely be the first method you want to call on a connected [`Client`]. After you
59    /// determine if the account is authorized or not, you will likely want to use either
60    /// [`Client::bot_sign_in`] or [`Client::request_login_code`].
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
66    /// if client.is_authorized().await? {
67    ///     println!("Client already authorized and ready to use!");
68    /// } else {
69    ///     println!("Client is not authorized, you will need to sign_in!");
70    /// }
71    /// # Ok(())
72    /// # }
73    /// ```
74    pub async fn is_authorized(&self) -> Result<bool, InvocationError> {
75        match self.invoke(&tl::functions::updates::GetState {}).await {
76            Ok(_) => Ok(true),
77            Err(InvocationError::Rpc(e)) if e.code == 401 => Ok(false),
78            Err(err) => Err(err),
79        }
80    }
81
82    async fn complete_login(
83        &self,
84        auth: tl::types::auth::Authorization,
85    ) -> Result<User, InvocationError> {
86        // In the extremely rare case where `Err` happens, there's not much we can do.
87        // `message_box` will try to correct its state as updates arrive.
88        let update_state = self.invoke(&tl::functions::updates::GetState {}).await.ok();
89
90        let user = User::from_raw(auth.user);
91
92        self.0.session.cache_peer(&PeerInfo::User {
93            id: user.bare_id(),
94            auth: Some(user.auth()),
95            bot: Some(user.is_bot()),
96            is_self: Some(true),
97        });
98        if let Some(tl::enums::updates::State::State(state)) = update_state {
99            self.0
100                .session
101                .set_update_state(UpdateState::All(UpdatesState {
102                    pts: state.pts,
103                    qts: state.qts,
104                    date: state.date,
105                    seq: state.seq,
106                    channels: Vec::new(),
107                }));
108        }
109
110        Ok(user)
111    }
112
113    /// Signs in to the bot account associated with this token.
114    ///
115    /// This is the method you need to call to use the client under a bot account.
116    ///
117    /// It is recommended to save the session on successful login, and if saving
118    /// fails, it is recommended to [`Client::sign_out`]. If the session cannot be saved, then the
119    /// authorization will be "lost" in the list of logged-in clients, since it is unaccessible.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
125    /// // Note: these values are obviously fake.
126    /// //       Obtain your own with the developer's phone at https://my.telegram.org.
127    /// const API_HASH: &str = "514727c32270b9eb8cc16daf17e21e57";
128    /// //       Obtain your own by talking to @BotFather via a Telegram app.
129    /// const TOKEN: &str = "776609994:AAFXAy5-PawQlnYywUlZ_b_GOXgarR3ah_yq";
130    ///
131    /// let user = match client.bot_sign_in(TOKEN, API_HASH).await {
132    ///     Ok(user) => user,
133    ///     Err(err) => {
134    ///         println!("Failed to sign in as a bot :(\n{}", err);
135    ///         return Err(err.into());
136    ///     }
137    /// };
138    ///
139    /// if let Some(first_name) = user.first_name() {
140    ///     println!("Signed in as {}!", first_name);
141    /// } else {
142    ///     println!("Signed in!");
143    /// }
144    ///
145    /// # Ok(())
146    /// # }
147    /// ```
148    pub async fn bot_sign_in(&self, token: &str, api_hash: &str) -> Result<User, InvocationError> {
149        let request = tl::functions::auth::ImportBotAuthorization {
150            flags: 0,
151            api_id: self.0.api_id,
152            api_hash: api_hash.to_string(),
153            bot_auth_token: token.to_string(),
154        };
155
156        let result = match self.invoke(&request).await {
157            Ok(x) => x,
158            Err(InvocationError::Rpc(err)) if err.code == 303 => {
159                let old_dc_id = self.0.session.home_dc_id();
160                let new_dc_id = err.value.unwrap() as i32;
161                // Disconnect from current DC to cull the now-unused connection.
162                // This also gives a chance for the new home DC to export its authorization
163                // if there's a need to connect back to the old DC after having logged in.
164                self.0.handle.disconnect_from_dc(old_dc_id);
165                self.0.session.set_home_dc_id(new_dc_id);
166                self.invoke(&request).await?
167            }
168            Err(e) => return Err(e.into()),
169        };
170
171        match result {
172            tl::enums::auth::Authorization::Authorization(x) => {
173                self.complete_login(x).await.map_err(Into::into)
174            }
175            tl::enums::auth::Authorization::SignUpRequired(_) => {
176                panic!("API returned SignUpRequired even though we're logging in as a bot");
177            }
178        }
179    }
180
181    /// Requests the login code for the account associated to the given phone
182    /// number via another Telegram application or SMS.
183    ///
184    /// This is the method you need to call before being able to sign in to a user account.
185    /// After you obtain the code and it's inside your program (e.g. ask the user to enter it
186    /// via the console's standard input), you will need to [`Client::sign_in`] to complete the
187    /// process.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
193    /// // Note: these values are obviously fake.
194    /// //       Obtain your own with the developer's phone at https://my.telegram.org.
195    /// const API_HASH: &str = "514727c32270b9eb8cc16daf17e21e57";
196    /// //       The phone used here does NOT need to be the same as the one used by the developer
197    /// //       to obtain the API ID and hash.
198    /// const PHONE: &str = "+1 415 555 0132";
199    ///
200    /// if !client.is_authorized().await? {
201    ///     // We're not logged in, so request the login code.
202    ///     client.request_login_code(PHONE, API_HASH).await?;
203    /// }
204    /// # Ok(())
205    /// # }
206    /// ```
207    pub async fn request_login_code(
208        &self,
209        phone: &str,
210        api_hash: &str,
211    ) -> Result<LoginToken, InvocationError> {
212        let request = tl::functions::auth::SendCode {
213            phone_number: phone.to_string(),
214            api_id: self.0.api_id,
215            api_hash: api_hash.to_string(),
216            settings: tl::types::CodeSettings {
217                allow_flashcall: false,
218                current_number: false,
219                allow_app_hash: false,
220                allow_missed_call: false,
221                allow_firebase: false,
222                logout_tokens: None,
223                token: None,
224                app_sandbox: None,
225                unknown_number: false,
226            }
227            .into(),
228        };
229
230        use tl::enums::auth::SentCode as SC;
231
232        let sent_code: tl::types::auth::SentCode = match self.invoke(&request).await {
233            Ok(x) => match x {
234                SC::Code(code) => code,
235                SC::Success(_) => panic!("should not have logged in yet"),
236                SC::PaymentRequired(_) => todo!(),
237            },
238            Err(InvocationError::Rpc(err)) if err.code == 303 => {
239                let old_dc_id = self.0.session.home_dc_id();
240                let new_dc_id = err.value.unwrap() as i32;
241                // Disconnect from current DC to cull the now-unused connection.
242                // This also gives a chance for the new home DC to export its authorization
243                // if there's a need to connect back to the old DC after having logged in.
244                self.0.handle.disconnect_from_dc(old_dc_id);
245                self.0.session.set_home_dc_id(new_dc_id);
246                match self.invoke(&request).await? {
247                    SC::Code(code) => code,
248                    SC::Success(_) => panic!("should not have logged in yet"),
249                    SC::PaymentRequired(_) => todo!(),
250                }
251            }
252            Err(e) => return Err(e.into()),
253        };
254
255        Ok(LoginToken {
256            phone: phone.to_string(),
257            phone_code_hash: sent_code.phone_code_hash,
258        })
259    }
260
261    /// Signs in to the user account.
262    ///
263    /// You must call [`Client::request_login_code`] before using this method in order to obtain
264    /// necessary login token, and also have asked the user for the login code.
265    ///
266    /// It is recommended to save the session on successful login, and if saving
267    /// fails, it is recommended to [`Client::sign_out`]. If the session cannot be saved, then the
268    /// authorization will be "lost" in the list of logged-in clients, since it is unaccessible.
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// # use grammers_client::SignInError;
274    ///
275    ///  async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
276    /// # const API_HASH: &str = "";
277    /// # const PHONE: &str = "";
278    /// fn ask_code_to_user() -> String {
279    ///     unimplemented!()
280    /// }
281    ///
282    /// let token = client.request_login_code(PHONE, API_HASH).await?;
283    /// let code = ask_code_to_user();
284    ///
285    /// let user = match client.sign_in(&token, &code).await {
286    ///     Ok(user) => user,
287    ///     Err(SignInError::PasswordRequired(_token)) => panic!("Please provide a password"),
288    ///     Err(SignInError::SignUpRequired { terms_of_service: tos }) => panic!("Sign up required"),
289    ///     Err(err) => {
290    ///         println!("Failed to sign in as a user :(\n{}", err);
291    ///         return Err(err.into());
292    ///     }
293    /// };
294    ///
295    /// if let Some(first_name) = user.first_name() {
296    ///     println!("Signed in as {}!", first_name);
297    /// } else {
298    ///   println!("Signed in!");
299    /// }
300    /// # Ok(())
301    /// # }
302    /// ```
303    pub async fn sign_in(&self, token: &LoginToken, code: &str) -> Result<User, SignInError> {
304        match self
305            .invoke(&tl::functions::auth::SignIn {
306                phone_number: token.phone.clone(),
307                phone_code_hash: token.phone_code_hash.clone(),
308                phone_code: Some(code.to_string()),
309                email_verification: None,
310            })
311            .await
312        {
313            Ok(tl::enums::auth::Authorization::Authorization(x)) => {
314                self.complete_login(x).await.map_err(SignInError::Other)
315            }
316            Ok(tl::enums::auth::Authorization::SignUpRequired(x)) => {
317                Err(SignInError::SignUpRequired {
318                    terms_of_service: x.terms_of_service.map(TermsOfService::from_raw),
319                })
320            }
321            Err(err) if err.is("SESSION_PASSWORD_NEEDED") => {
322                let password_token = self.get_password_information().await;
323                match password_token {
324                    Ok(token) => Err(SignInError::PasswordRequired(token)),
325                    Err(e) => Err(SignInError::Other(e)),
326                }
327            }
328            Err(err) if err.is("PHONE_CODE_*") => Err(SignInError::InvalidCode),
329            Err(error) => Err(SignInError::Other(error)),
330        }
331    }
332
333    /// Extract information needed for the two-factor authentication
334    /// It's called automatically when we get SESSION_PASSWORD_NEEDED error during sign in.
335    async fn get_password_information(&self) -> Result<PasswordToken, InvocationError> {
336        let request = tl::functions::account::GetPassword {};
337
338        let password: tl::types::account::Password = self.invoke(&request).await?.into();
339
340        Ok(PasswordToken::new(password))
341    }
342
343    /// Sign in using two-factor authentication (user password).
344    ///
345    /// [`PasswordToken`] can be obtained from [`SignInError::PasswordRequired`] error after the
346    /// [`Client::sign_in`] method fails.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use grammers_client::SignInError;
352    ///
353    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
354    /// # const API_HASH: &str = "";
355    /// # const PHONE: &str = "";
356    /// fn get_user_password(hint: &str) -> Vec<u8> {
357    ///     unimplemented!()
358    /// }
359    ///
360    /// # let token = client.request_login_code(PHONE, API_HASH).await?;
361    /// # let code = "";
362    ///
363    /// // ... enter phone number, request login code ...
364    ///
365    /// let user = match client.sign_in(&token, &code).await {
366    ///     Err(SignInError::PasswordRequired(password_token) ) => {
367    ///         let mut password = get_user_password(password_token.hint().unwrap());
368    ///
369    ///         client
370    ///             .check_password(password_token, password)
371    ///             .await.unwrap()
372    ///     }
373    ///     Ok(user) => user,
374    ///     Ok(_) => panic!("Sign in required"),
375    ///     Err(err) => {
376    ///         panic!("Failed to sign in as a user :(\n{err}");
377    ///     }
378    /// };
379    /// # Ok(())
380    /// # }
381    /// ```
382    pub async fn check_password(
383        &self,
384        password_token: PasswordToken,
385        password: impl AsRef<[u8]>,
386    ) -> Result<User, SignInError> {
387        let mut password_info = password_token.password;
388        let current_algo = password_info.current_algo.unwrap();
389        let mut params = utils::extract_password_parameters(&current_algo);
390
391        // Telegram sent us incorrect parameters, trying to get them again
392        if !check_p_and_g(params.2, params.3) {
393            password_info = self
394                .get_password_information()
395                .await
396                .map_err(SignInError::Other)?
397                .password;
398            params =
399                utils::extract_password_parameters(password_info.current_algo.as_ref().unwrap());
400            if !check_p_and_g(params.2, params.3) {
401                panic!("Failed to get correct password information from Telegram")
402            }
403        }
404
405        let (salt1, salt2, p, g) = params;
406
407        let g_b = password_info.srp_b.unwrap();
408        let a: Vec<u8> = password_info.secure_random;
409
410        let (m1, g_a) = calculate_2fa(salt1, salt2, p, g, g_b, a, password);
411
412        let check_password = tl::functions::auth::CheckPassword {
413            password: tl::enums::InputCheckPasswordSrp::Srp(tl::types::InputCheckPasswordSrp {
414                srp_id: password_info.srp_id.unwrap(),
415                a: g_a.to_vec(),
416                m1: m1.to_vec(),
417            }),
418        };
419
420        match self.invoke(&check_password).await {
421            Ok(tl::enums::auth::Authorization::Authorization(x)) => {
422                self.complete_login(x).await.map_err(SignInError::Other)
423            }
424            Ok(tl::enums::auth::Authorization::SignUpRequired(_x)) => panic!("Unexpected result"),
425            Err(err) if err.is("PASSWORD_HASH_INVALID") => Err(SignInError::InvalidPassword),
426            Err(error) => Err(SignInError::Other(error)),
427        }
428    }
429
430    /// Signs out of the account authorized by this client's session.
431    ///
432    /// If the client was not logged in, this method returns false.
433    ///
434    /// The client is not disconnected after signing out.
435    ///
436    /// Note that after using this method you will have to sign in again. If all you want to do
437    /// is disconnect, simply [`drop`] the [`Client`] instance.
438    ///
439    /// # Examples
440    ///
441    /// ```
442    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
443    /// if client.sign_out().await.is_ok() {
444    ///     println!("Signed out successfully!");
445    /// } else {
446    ///     println!("No user was signed in, so nothing has changed...");
447    /// }
448    /// # Ok(())
449    /// # }
450    /// ```
451    pub async fn sign_out(&self) -> Result<tl::enums::auth::LoggedOut, InvocationError> {
452        self.invoke(&tl::functions::auth::LogOut {}).await
453    }
454
455    /// Signals all clients sharing the same sender pool to disconnect.
456    pub fn disconnect(&self) {
457        self.0.handle.quit();
458    }
459}