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