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