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(¤t_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}