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::net::connect_sender;
9use super::Client;
10use crate::types::{LoginToken, PasswordToken, TermsOfService, User};
11use crate::utils;
12use grammers_crypto::two_factor_auth::{calculate_2fa, check_p_and_g};
13pub use grammers_mtsender::{AuthorizationError, InvocationError};
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 is not authorized, you will need to sign_in!");
68    /// } else {
69    ///     println!("Client already authorized and ready to use!")
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        let sync_state = {
93            let mut state = self.0.state.write().unwrap();
94            self.0
95                .config
96                .session
97                .set_user(user.id(), state.dc_id, user.is_bot());
98
99            state.chat_hashes.set_self_user(user.pack());
100            if let Some(us) = update_state {
101                state.message_box.set_state(us);
102                true
103            } else {
104                false
105            }
106        };
107
108        if sync_state {
109            self.sync_update_state();
110        }
111
112        Ok(user)
113    }
114
115    /// Signs in to the bot account associated with this token.
116    ///
117    /// This is the method you need to call to use the client under a bot account.
118    ///
119    /// It is recommended to save the [`Client::session()`] on successful login, and if saving
120    /// fails, it is recommended to [`Client::sign_out`]. If the session cannot be saved, then the
121    /// authorization will be "lost" in the list of logged-in clients, since it is unaccessible.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
127    /// // Note: this token is obviously fake.
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).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    /// println!("Signed in as {}!", user.first_name());
140    /// # Ok(())
141    /// # }
142    /// ```
143    pub async fn bot_sign_in(&self, token: &str) -> Result<User, AuthorizationError> {
144        let request = tl::functions::auth::ImportBotAuthorization {
145            flags: 0,
146            api_id: self.0.config.api_id,
147            api_hash: self.0.config.api_hash.clone(),
148            bot_auth_token: token.to_string(),
149        };
150
151        let result = match self.invoke(&request).await {
152            Ok(x) => x,
153            Err(InvocationError::Rpc(err)) if err.code == 303 => {
154                let dc_id = err.value.unwrap() as i32;
155                let (sender, request_tx) = connect_sender(dc_id, &self.0.config).await?;
156                {
157                    *self.0.conn.sender.lock().await = sender;
158                    *self.0.conn.request_tx.write().unwrap() = request_tx;
159                    let mut state = self.0.state.write().unwrap();
160                    state.dc_id = dc_id;
161                }
162                self.invoke(&request).await?
163            }
164            Err(e) => return Err(e.into()),
165        };
166
167        match result {
168            tl::enums::auth::Authorization::Authorization(x) => {
169                self.complete_login(x).await.map_err(Into::into)
170            }
171            tl::enums::auth::Authorization::SignUpRequired(_) => {
172                panic!("API returned SignUpRequired even though we're logging in as a bot");
173            }
174        }
175    }
176
177    /// Requests the login code for the account associated to the given phone
178    /// number via another Telegram application or SMS.
179    ///
180    /// This is the method you need to call before being able to sign in to a user account.
181    /// After you obtain the code and it's inside your program (e.g. ask the user to enter it
182    /// via the console's standard input), you will need to [`Client::sign_in`] to complete the
183    /// process.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
189    /// // Note: this phone number is obviously fake.
190    /// //       The phone used here does NOT need to be the same as the one used by the developer
191    /// //       to obtain the API ID and hash.
192    /// const PHONE: &str = "+1 415 555 0132";
193    ///
194    /// if !client.is_authorized().await? {
195    ///     // We're not logged in, so request the login code.
196    ///     client.request_login_code(PHONE).await?;
197    /// }
198    /// # Ok(())
199    /// # }
200    /// ```
201    pub async fn request_login_code(&self, phone: &str) -> Result<LoginToken, AuthorizationError> {
202        let request = tl::functions::auth::SendCode {
203            phone_number: phone.to_string(),
204            api_id: self.0.config.api_id,
205            api_hash: self.0.config.api_hash.clone(),
206            settings: tl::types::CodeSettings {
207                allow_flashcall: false,
208                current_number: false,
209                allow_app_hash: false,
210                allow_missed_call: false,
211                allow_firebase: false,
212                logout_tokens: None,
213                token: None,
214                app_sandbox: None,
215                unknown_number: false,
216            }
217            .into(),
218        };
219
220        use tl::enums::auth::SentCode as SC;
221
222        let sent_code: tl::types::auth::SentCode = match self.invoke(&request).await {
223            Ok(x) => match x {
224                SC::Code(code) => code,
225                SC::Success(_) => panic!("should not have logged in yet"),
226            },
227            Err(InvocationError::Rpc(err)) if err.code == 303 => {
228                // Since we are not logged in (we're literally requesting for
229                // the code to login now), there's no need to export the current
230                // authorization and re-import it at a different datacenter.
231                //
232                // Just connect and generate a new authorization key with it
233                // before trying again.
234                let dc_id = err.value.unwrap() as i32;
235                let (sender, request_tx) = connect_sender(dc_id, &self.0.config).await?;
236                {
237                    *self.0.conn.sender.lock().await = sender;
238                    *self.0.conn.request_tx.write().unwrap() = request_tx;
239                    let mut state = self.0.state.write().unwrap();
240                    state.dc_id = dc_id;
241                }
242                match self.invoke(&request).await? {
243                    SC::Code(code) => code,
244                    SC::Success(_) => panic!("should not have logged in yet"),
245                }
246            }
247            Err(e) => return Err(e.into()),
248        };
249
250        Ok(LoginToken {
251            phone: phone.to_string(),
252            phone_code_hash: sent_code.phone_code_hash,
253        })
254    }
255
256    /// Signs in to the user account.
257    ///
258    /// You must call [`Client::request_login_code`] before using this method in order to obtain
259    /// necessary login token, and also have asked the user for the login code.
260    ///
261    /// It is recommended to save the [`Client::session()`] on successful login, and if saving
262    /// fails, it is recommended to [`Client::sign_out`]. If the session cannot be saved, then the
263    /// authorization will be "lost" in the list of logged-in clients, since it is unaccessible.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// # use grammers_client::SignInError;
269    ///
270    ///  async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
271    /// # const PHONE: &str = "";
272    /// fn ask_code_to_user() -> String {
273    ///     unimplemented!()
274    /// }
275    ///
276    /// let token = client.request_login_code(PHONE).await?;
277    /// let code = ask_code_to_user();
278    ///
279    /// let user = match client.sign_in(&token, &code).await {
280    ///     Ok(user) => user,
281    ///     Err(SignInError::PasswordRequired(_token)) => panic!("Please provide a password"),
282    ///     Err(SignInError::SignUpRequired { terms_of_service: tos }) => panic!("Sign up required"),
283    ///     Err(err) => {
284    ///         println!("Failed to sign in as a user :(\n{}", err);
285    ///         return Err(err.into());
286    ///     }
287    /// };
288    ///
289    /// println!("Signed in as {}!", user.first_name());
290    /// # Ok(())
291    /// # }
292    /// ```
293    pub async fn sign_in(&self, token: &LoginToken, code: &str) -> Result<User, SignInError> {
294        match self
295            .invoke(&tl::functions::auth::SignIn {
296                phone_number: token.phone.clone(),
297                phone_code_hash: token.phone_code_hash.clone(),
298                phone_code: Some(code.to_string()),
299                email_verification: None,
300            })
301            .await
302        {
303            Ok(tl::enums::auth::Authorization::Authorization(x)) => {
304                self.complete_login(x).await.map_err(SignInError::Other)
305            }
306            Ok(tl::enums::auth::Authorization::SignUpRequired(x)) => {
307                Err(SignInError::SignUpRequired {
308                    terms_of_service: x.terms_of_service.map(TermsOfService::from_raw),
309                })
310            }
311            Err(err) if err.is("SESSION_PASSWORD_NEEDED") => {
312                let password_token = self.get_password_information().await;
313                match password_token {
314                    Ok(token) => Err(SignInError::PasswordRequired(token)),
315                    Err(e) => Err(SignInError::Other(e)),
316                }
317            }
318            Err(err) if err.is("PHONE_CODE_*") => Err(SignInError::InvalidCode),
319            Err(error) => Err(SignInError::Other(error)),
320        }
321    }
322
323    /// Extract information needed for the two-factor authentication
324    /// It's called automatically when we get SESSION_PASSWORD_NEEDED error during sign in.
325    async fn get_password_information(&self) -> Result<PasswordToken, InvocationError> {
326        let request = tl::functions::account::GetPassword {};
327
328        let password: tl::types::account::Password = self.invoke(&request).await?.into();
329
330        Ok(PasswordToken::new(password))
331    }
332
333    /// Sign in using two-factor authentication (user password).
334    ///
335    /// [`PasswordToken`] can be obtained from [`SignInError::PasswordRequired`] error after the
336    /// [`Client::sign_in`] method fails.
337    ///
338    /// # Examples
339    ///
340    /// ```
341    /// use grammers_client::SignInError;
342    ///
343    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
344    /// # const PHONE: &str = "";
345    /// fn get_user_password(hint: &str) -> Vec<u8> {
346    ///     unimplemented!()
347    /// }
348    ///
349    /// # let token = client.request_login_code(PHONE).await?;
350    /// # let code = "";
351    ///
352    /// // ... enter phone number, request login code ...
353    ///
354    /// let user = match client.sign_in(&token, &code).await {
355    ///     Err(SignInError::PasswordRequired(password_token) ) => {
356    ///         let mut password = get_user_password(password_token.hint().unwrap());
357    ///
358    ///         client
359    ///             .check_password(password_token, password)
360    ///             .await.unwrap()
361    ///     }
362    ///     Ok(user) => user,
363    ///     Ok(_) => panic!("Sign in required"),
364    ///     Err(err) => {
365    ///         panic!("Failed to sign in as a user :(\n{err}");
366    ///     }
367    /// };
368    /// # Ok(())
369    /// # }
370    /// ```
371    pub async fn check_password(
372        &self,
373        password_token: PasswordToken,
374        password: impl AsRef<[u8]>,
375    ) -> Result<User, SignInError> {
376        let mut password_info = password_token.password;
377        let current_algo = password_info.current_algo.unwrap();
378        let mut params = utils::extract_password_parameters(&current_algo);
379
380        // Telegram sent us incorrect parameters, trying to get them again
381        if !check_p_and_g(params.2, params.3) {
382            password_info = self
383                .get_password_information()
384                .await
385                .map_err(SignInError::Other)?
386                .password;
387            params =
388                utils::extract_password_parameters(password_info.current_algo.as_ref().unwrap());
389            if !check_p_and_g(params.2, params.3) {
390                panic!("Failed to get correct password information from Telegram")
391            }
392        }
393
394        let (salt1, salt2, p, g) = params;
395
396        let g_b = password_info.srp_b.unwrap();
397        let a: Vec<u8> = password_info.secure_random;
398
399        let (m1, g_a) = calculate_2fa(salt1, salt2, p, g, g_b, a, password);
400
401        let check_password = tl::functions::auth::CheckPassword {
402            password: tl::enums::InputCheckPasswordSrp::Srp(tl::types::InputCheckPasswordSrp {
403                srp_id: password_info.srp_id.unwrap(),
404                a: g_a.to_vec(),
405                m1: m1.to_vec(),
406            }),
407        };
408
409        match self.invoke(&check_password).await {
410            Ok(tl::enums::auth::Authorization::Authorization(x)) => {
411                self.complete_login(x).await.map_err(SignInError::Other)
412            }
413            Ok(tl::enums::auth::Authorization::SignUpRequired(_x)) => panic!("Unexpected result"),
414            Err(err) if err.is("PASSWORD_HASH_INVALID") => Err(SignInError::InvalidPassword),
415            Err(error) => Err(SignInError::Other(error)),
416        }
417    }
418
419    /// Signs out of the account authorized by this client's session.
420    ///
421    /// If the client was not logged in, this method returns false.
422    ///
423    /// The client is not disconnected after signing out.
424    ///
425    /// Note that after using this method you will have to sign in again. If all you want to do
426    /// is disconnect, simply [`drop`] the [`Client`] instance.
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// # async fn f(client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
432    /// if client.sign_out().await.is_ok() {
433    ///     println!("Signed out successfully!");
434    /// } else {
435    ///     println!("No user was signed in, so nothing has changed...");
436    /// }
437    /// # Ok(())
438    /// # }
439    /// ```
440    pub async fn sign_out(&self) -> Result<tl::enums::auth::LoggedOut, InvocationError> {
441        self.invoke(&tl::functions::auth::LogOut {}).await
442    }
443
444    /// Synchronize all state to the session file and provide mutable access to it.
445    ///
446    /// You can use this to temporarily access the session and save it wherever you want to.
447    ///
448    /// Panics if the type parameter does not match the actual session type.
449    pub fn session(&self) -> &grammers_session::Session {
450        self.sync_update_state();
451        &self.0.config.session
452    }
453
454    /// Calls [`Client::sign_out`] and disconnects.
455    ///
456    /// The client will be disconnected even if signing out fails.
457    pub async fn sign_out_disconnect(&self) -> Result<(), InvocationError> {
458        let _res = self.invoke(&tl::functions::auth::LogOut {}).await;
459        panic!("disconnect now only works via dropping");
460    }
461}