Skip to main content

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