two_factor.rs (27275B)
1 use crate::{ 2 api::EmptyResult, 3 db::{ 4 schema::{totp, webauthn}, 5 DbConn, FromDb, 6 }, 7 error::Error, 8 }; 9 use serde::ser::{Serialize, SerializeStruct, Serializer}; 10 use tokio::task; 11 use webauthn_rp::{ 12 bin::{Decode, Encode}, 13 request::{ 14 auth::AllowedCredentials, register::UserHandle, Credentials, PublicKeyCredentialDescriptor, 15 }, 16 response::{ 17 register::{ 18 AuthenticatorExtensionOutputStaticState, CompressedP256PubKey, CompressedP384PubKey, 19 CompressedPubKey, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, RsaPubKey, 20 StaticState, UncompressedPubKey, 21 }, 22 AuthTransports, CredentialId, 23 }, 24 AggErr, AuthenticatedCredential, RegisteredCredential, 25 }; 26 db_object! { 27 /// Exactly one of the following is true: 28 /// * [`Self::ed25519_key`] is `Some`. 29 /// * [`Self::p256_x`] is `Some`. 30 /// * [`Self::p384_x`] is `Some`. 31 /// * [`Self::rsa_n`] is `Some`. 32 /// 33 /// [`Self::p256_x`] is `Some` iff [`Self::p256_y_is_odd`] is `Some`. [`Self::p384_x`] is `Some` iff 34 /// [`Self::p384_y_is_odd`] is `Some`. [`Self::rsa_n`] is `Some` iff [`Self::rsa_e`] is `Some`. 35 /// 36 /// [`Self::transports`] is actually a `u8`. [`Self::rsa_e`] is actually an `Option` of `u32` such that the 37 /// contained `i32` is converted to `u32` via `as`. [`Self::cred_protect`] is actually `0`, `1`, `2`, or `3`. 38 #[derive(Insertable, Queryable)] 39 #[diesel(table_name = webauthn)] 40 pub struct WebAuthn { 41 credential_id: Vec<u8>, 42 transports: i16, 43 user_uuid: String, 44 ed25519_key: Option<[u8; 32]>, 45 p256_x: Option<[u8; 32]>, 46 p256_y_is_odd: Option<bool>, 47 p384_x: Option<[u8; 32]>, 48 p384_y_is_odd: Option<bool>, 49 rsa_n: Option<Vec<u8>>, 50 rsa_e: Option<i32>, 51 cred_protect: i16, 52 hmac_secret: Option<bool>, 53 dynamic_state: [u8; 7], 54 metadata: String, 55 id: i64, 56 name: String, 57 } 58 #[derive(Insertable, Queryable)] 59 #[diesel(table_name = totp)] 60 pub struct Totp { 61 user_uuid: String, 62 pub token: String, 63 last_used: i64, 64 } 65 } 66 impl WebAuthn { 67 pub fn new( 68 cred: &RegisteredCredential<'_, '_>, 69 user_uuid: String, 70 id: i64, 71 name: String, 72 ) -> Result<Self, Error> { 73 Ok(Self { 74 credential_id: cred.id().encode()?.to_owned(), 75 transports: i16::from(cred.transports().encode()?), 76 user_uuid, 77 ed25519_key: match cred.static_state().credential_public_key { 78 UncompressedPubKey::Ed25519(ref key) => Some(key.into_inner().try_into().unwrap_or_else(|_e| unreachable!("there is a bug in webauthn_rp::response::register::Ed25519PubKey"))), 79 UncompressedPubKey::P256(_) 80 | UncompressedPubKey::P384(_) 81 | UncompressedPubKey::Rsa(_) => None, 82 }, 83 p256_x: match cred.static_state().credential_public_key { 84 UncompressedPubKey::P256(ref key) => Some(key.x().try_into().unwrap_or_else(|_e| unreachable!("there is a bug in webauthn_rp::response::register::UncompressedP256PubKey"))), 85 UncompressedPubKey::Ed25519(_) 86 | UncompressedPubKey::P384(_) 87 | UncompressedPubKey::Rsa(_) => None, 88 }, 89 p256_y_is_odd: match cred.static_state().credential_public_key { 90 UncompressedPubKey::P256(ref key) => Some(key.y()[31] & 1 == 1), 91 UncompressedPubKey::Ed25519(_) 92 | UncompressedPubKey::P384(_) 93 | UncompressedPubKey::Rsa(_) => None, 94 }, 95 p384_x: match cred.static_state().credential_public_key { 96 UncompressedPubKey::P384(ref key) => Some(key.x().try_into().unwrap_or_else(|_e| unreachable!("there is a bug in webauthn_rp::response::register::UncompressedP384PubKey"))), 97 UncompressedPubKey::Ed25519(_) 98 | UncompressedPubKey::P256(_) 99 | UncompressedPubKey::Rsa(_) => None, 100 }, 101 p384_y_is_odd: match cred.static_state().credential_public_key { 102 UncompressedPubKey::P384(ref key) => Some(key.y()[47] & 1 == 1), 103 UncompressedPubKey::Ed25519(_) 104 | UncompressedPubKey::P256(_) 105 | UncompressedPubKey::Rsa(_) => None, 106 }, 107 rsa_n: match cred.static_state().credential_public_key { 108 UncompressedPubKey::Rsa(ref key) => Some((*key.n()).to_owned()), 109 UncompressedPubKey::Ed25519(_) 110 | UncompressedPubKey::P256(_) 111 | UncompressedPubKey::P384(_) => None, 112 }, 113 rsa_e: match cred.static_state().credential_public_key { 114 UncompressedPubKey::Rsa(ref key) => Some(key.e() as i32), 115 UncompressedPubKey::Ed25519(_) 116 | UncompressedPubKey::P256(_) 117 | UncompressedPubKey::P384(_) => None, 118 }, 119 cred_protect: match cred.static_state().extensions.cred_protect { 120 CredentialProtectionPolicy::None => 0, 121 CredentialProtectionPolicy::UserVerificationOptional => 1, 122 CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList => 2, 123 CredentialProtectionPolicy::UserVerificationRequired => 3, 124 }, 125 hmac_secret: cred.static_state().extensions.hmac_secret, 126 dynamic_state: cred.dynamic_state().encode()?, 127 metadata: cred.metadata().into_json(), 128 id, 129 name, 130 }) 131 } 132 } 133 #[derive(Queryable)] 134 pub struct WebAuthnInfo { 135 id: i64, 136 name: String, 137 } 138 impl Serialize for WebAuthnInfo { 139 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 140 where 141 S: Serializer, 142 { 143 let mut s = serializer.serialize_struct("WebAuthnInfo", 3)?; 144 s.serialize_field("id", &self.id)?; 145 s.serialize_field("name", self.name.as_str())?; 146 s.serialize_field("migrated", &false)?; 147 s.end() 148 } 149 } 150 151 #[derive(Clone, Copy)] 152 pub enum TwoFactorType { 153 Totp = 0, 154 WebAuthn = 7, 155 } 156 impl From<TwoFactorType> for i32 { 157 fn from(value: TwoFactorType) -> Self { 158 match value { 159 TwoFactorType::Totp => 0i32, 160 TwoFactorType::WebAuthn => 7i32, 161 } 162 } 163 } 164 impl TryFrom<i32> for TwoFactorType { 165 type Error = Error; 166 fn try_from(value: i32) -> Result<Self, Self::Error> { 167 match value { 168 0i32 => Ok(Self::Totp), 169 7i32 => Ok(Self::WebAuthn), 170 _ => Err(Error::from(String::from( 171 "i32 is not a valid TwoFactorType", 172 ))), 173 } 174 } 175 } 176 impl Serialize for TwoFactorType { 177 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 178 where 179 S: Serializer, 180 { 181 let mut s = serializer.serialize_struct("TwoFactorType", 3)?; 182 s.serialize_field("enabled", &true)?; 183 s.serialize_field("type", &i32::from(*self))?; 184 s.serialize_field("object", "twoFactorProvider")?; 185 s.end() 186 } 187 } 188 impl Totp { 189 pub const fn new(user_uuid: String, token: String) -> Self { 190 Self { 191 user_uuid, 192 token, 193 last_used: 0, 194 } 195 } 196 pub fn get_last_used(&self) -> u64 { 197 u64::try_from(self.last_used).expect("underflow") 198 } 199 pub fn set_last_used(&mut self, last_used: u64) { 200 self.last_used = i64::try_from(last_used).expect("overflow"); 201 } 202 } 203 impl TwoFactorType { 204 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 205 pub async fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { 206 use diesel::prelude::{Connection, ExpressionMethods, RunQueryDsl}; 207 use diesel::result; 208 let mut con_res = conn.conn.clone().lock_owned().await; 209 let con = con_res.as_mut().expect("unable to get a pooled connection"); 210 task::block_in_place(move || { 211 con.transaction(|con| { 212 diesel::delete(webauthn::table) 213 .filter(webauthn::user_uuid.eq(user_uuid)) 214 .execute(con) 215 .and_then(|_| { 216 diesel::delete(totp::table) 217 .filter(totp::user_uuid.eq(user_uuid)) 218 .execute(con) 219 .map(|_| ()) 220 }) 221 .map_err(result::Error::into) 222 }) 223 }) 224 } 225 #[allow(clippy::clone_on_ref_ptr)] 226 pub async fn delete_by_user(self, user_uuid: &str, conn: &DbConn) -> EmptyResult { 227 use diesel::prelude::{ExpressionMethods, RunQueryDsl}; 228 use diesel::result; 229 let mut con_res = conn.conn.clone().lock_owned().await; 230 let con = con_res.as_mut().expect("unable to get a pooled connection"); 231 task::block_in_place(move || { 232 match self { 233 Self::Totp => diesel::delete(totp::table) 234 .filter(totp::user_uuid.eq(user_uuid)) 235 .execute(con), 236 Self::WebAuthn => diesel::delete(webauthn::table) 237 .filter(webauthn::user_uuid.eq(user_uuid)) 238 .execute(con), 239 } 240 .map(|_| ()) 241 .map_err(result::Error::into) 242 }) 243 } 244 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 245 pub async fn has_twofactor(user_uuid: &str, conn: &DbConn) -> Result<bool, Error> { 246 use diesel::prelude::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl}; 247 use diesel::result; 248 let mut con_res = conn.conn.clone().lock_owned().await; 249 let con = con_res.as_mut().expect("unable to get a pooled connection"); 250 task::block_in_place(move || { 251 con.transaction(|con| { 252 webauthn::table 253 .count() 254 .filter(webauthn::user_uuid.eq(user_uuid)) 255 .get_result::<i64>(con) 256 .map_err(result::Error::into) 257 .and_then(|count| { 258 if count == 0 { 259 totp::table 260 .count() 261 .filter(totp::user_uuid.eq(user_uuid)) 262 .get_result::<i64>(con) 263 .map_err(result::Error::into) 264 .map(|count| count > 0) 265 } else { 266 Ok(true) 267 } 268 }) 269 }) 270 }) 271 } 272 /// The `bool` represents if WebAuthn is enabled. 273 /// The `Option` represents if TOTP is enabled; and if so, contains the secret token. 274 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 275 pub async fn get_factors( 276 user_uuid: &str, 277 conn: &DbConn, 278 ) -> Result<(bool, Option<String>), Error> { 279 use diesel::prelude::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl}; 280 use diesel::result; 281 use diesel::OptionalExtension; 282 let mut con_res = conn.conn.clone().lock_owned().await; 283 let con = con_res.as_mut().expect("unable to get a pooled connection"); 284 task::block_in_place(move || { 285 con.transaction(|con| { 286 webauthn::table 287 .count() 288 .filter(webauthn::user_uuid.eq(user_uuid)) 289 .get_result::<i64>(con) 290 .and_then(|count| { 291 let authn = count > 0; 292 totp::table 293 .select(totp::token) 294 .filter(totp::user_uuid.eq(user_uuid)) 295 .first(con) 296 .optional() 297 .map(|token| (authn, token)) 298 }) 299 .map_err(result::Error::into) 300 }) 301 }) 302 } 303 } 304 impl WebAuthnInfo { 305 #[allow(clippy::clone_on_ref_ptr)] 306 pub async fn get_all_by_user(user_uuid: &str, conn: &DbConn) -> Result<Vec<Self>, Error> { 307 use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; 308 use diesel::result; 309 let mut con_res = conn.conn.clone().lock_owned().await; 310 let con = con_res.as_mut().expect("unable to get a pooled connection"); 311 task::block_in_place(move || { 312 webauthn::table 313 .select((webauthn::id, webauthn::name)) 314 .filter(webauthn::user_uuid.eq(user_uuid)) 315 .load::<Self>(con) 316 .map_err(result::Error::into) 317 }) 318 } 319 } 320 321 impl WebAuthn { 322 #[allow(clippy::clone_on_ref_ptr)] 323 async fn get_creds<T>(user_uuid: &str, conn: &DbConn) -> Result<T, Error> 324 where 325 T: Credentials, 326 PublicKeyCredentialDescriptor<Vec<u8>>: Into<T::Credential>, 327 { 328 use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; 329 use diesel::result; 330 let mut con_res = conn.conn.clone().lock_owned().await; 331 let con = con_res.as_mut().expect("unable to get a pooled connection"); 332 task::block_in_place(move || { 333 webauthn::table 334 .select((webauthn::credential_id, webauthn::transports)) 335 .filter(webauthn::user_uuid.eq(user_uuid)) 336 .load::<(Vec<u8>, i16)>(con) 337 .map_err(result::Error::into) 338 .and_then(|rows| { 339 let len = rows.len(); 340 rows.into_iter() 341 .try_fold(T::with_capacity(len), |mut creds, parts| { 342 let id = CredentialId::decode(parts.0).map_err(AggErr::CredentialId)?; 343 let transports = 344 AuthTransports::decode(u8::try_from(parts.1).map_err(|_e| { 345 Error::from(String::from("Encoded AuthTransports is not a u8")) 346 })?) 347 .map_err(AggErr::DecodeAuthTransports)?; 348 creds.push(PublicKeyCredentialDescriptor { id, transports }.into()); 349 Ok(creds) 350 }) 351 }) 352 }) 353 } 354 pub async fn get_registered_creds( 355 user_uuid: &str, 356 conn: &DbConn, 357 ) -> Result<Vec<PublicKeyCredentialDescriptor<Vec<u8>>>, Error> { 358 Self::get_creds(user_uuid, conn).await 359 } 360 pub async fn get_allowed_creds( 361 user_uuid: &str, 362 conn: &DbConn, 363 ) -> Result<AllowedCredentials, Error> { 364 Self::get_creds(user_uuid, conn).await 365 } 366 #[allow(clippy::clone_on_ref_ptr)] 367 pub async fn insert(self, conn: &DbConn) -> EmptyResult { 368 use __sqlite_model::WebAuthnDb; 369 use diesel::prelude::RunQueryDsl; 370 use diesel::result; 371 let mut con_res = conn.conn.clone().lock_owned().await; 372 let con = con_res.as_mut().expect("unable to get a pooled connection"); 373 task::block_in_place(move || { 374 diesel::insert_into(webauthn::table) 375 .values(WebAuthnDb::to_db(&self)) 376 .execute(con) 377 .map_err(result::Error::into) 378 .and_then(|count| { 379 if count == 1 { 380 Ok(()) 381 } else { 382 Err(Error::from(String::from( 383 "exactly one row would not have been inserted into webauthn", 384 ))) 385 } 386 }) 387 }) 388 } 389 #[allow(clippy::clone_on_ref_ptr)] 390 pub async fn update( 391 id: CredentialId<&[u8]>, 392 dynamic_state: DynamicState, 393 conn: &DbConn, 394 ) -> EmptyResult { 395 use diesel::prelude::{ExpressionMethods, RunQueryDsl}; 396 use diesel::result; 397 let mut con_res = conn.conn.clone().lock_owned().await; 398 let con = con_res.as_mut().expect("unable to get a pooled connection"); 399 task::block_in_place(move || { 400 diesel::update(webauthn::table) 401 .set(webauthn::dynamic_state.eq(dynamic_state.encode()?)) 402 .filter(webauthn::credential_id.eq(id.into_inner())) 403 .execute(con) 404 .map_err(result::Error::into) 405 .and_then(|count| { 406 if count == 1 { 407 Ok(()) 408 } else { 409 Err(Error::from(String::from( 410 "exactly one webauthn row would not have been updated", 411 ))) 412 } 413 }) 414 }) 415 } 416 #[allow(clippy::clone_on_ref_ptr)] 417 pub async fn get_credential<'a, 'b>( 418 credential_id: CredentialId<&'a [u8]>, 419 user_uuid: &str, 420 user_handle: UserHandle<&'b [u8]>, 421 conn: &DbConn, 422 ) -> Result< 423 Option< 424 AuthenticatedCredential< 425 'a, 426 'b, 427 CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>, 428 >, 429 >, 430 Error, 431 > { 432 use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; 433 use diesel::result; 434 use diesel::OptionalExtension; 435 let mut con_res = conn.conn.clone().lock_owned().await; 436 let con = con_res.as_mut().expect("unable to get a pooled connection"); 437 task::block_in_place(move || { 438 webauthn::table 439 .select(( 440 webauthn::ed25519_key, 441 webauthn::p256_x, 442 webauthn::p256_y_is_odd, 443 webauthn::p384_x, 444 webauthn::p384_y_is_odd, 445 webauthn::rsa_n, 446 webauthn::rsa_e, 447 webauthn::cred_protect, 448 webauthn::hmac_secret, 449 webauthn::dynamic_state, 450 )) 451 .filter(webauthn::credential_id.eq(credential_id.into_inner())) 452 .filter(webauthn::user_uuid.eq(user_uuid)) 453 .first::<( 454 Option<Vec<u8>>, 455 Option<Vec<u8>>, 456 Option<bool>, 457 Option<Vec<u8>>, 458 Option<bool>, 459 Option<Vec<u8>>, 460 Option<i32>, 461 i16, 462 Option<bool>, 463 Vec<u8>, 464 )>(con) 465 .optional() 466 .map_err(result::Error::into) 467 .and_then(|row| { 468 row.map_or_else( 469 || Ok(None), 470 |r| { 471 let credential_public_key = r.0.map_or_else( 472 || { 473 r.1.map_or_else( 474 || { 475 r.3.map_or_else( 476 || { 477 r.5.ok_or_else(|| Error::from("Encoded RsaPubKey is invalid".to_owned())).and_then(|n| { 478 r.6.ok_or_else(|| Error::from("Encoded RsaPubKey is invalid".to_owned())).and_then(|e| { 479 RsaPubKey::try_from((n, e as u32)).map_err(|e| Error::from(e.to_string())).map(CompressedPubKey::Rsa) 480 }) 481 }) 482 }, 483 |k| { 484 if k.len() == 48 { 485 r.4.ok_or_else(|| Error::from("Encoded CompressedP384PubKey".to_owned())).map(|y_is_odd| { 486 let mut key = [0; 48]; 487 key.copy_from_slice(k.as_slice()); 488 CompressedPubKey::P384(CompressedP384PubKey::from((key, y_is_odd))) 489 }) 490 } else { 491 Err(Error::from("Encoded CompressedP384PubKey is invalid".to_owned())) 492 } 493 } 494 ) 495 }, 496 |k| { 497 if k.len() == 32 { 498 r.2.ok_or_else(|| Error::from("Encoded CompressedP256PubKey".to_owned())).map(|y_is_odd| { 499 let mut key = [0; 32]; 500 key.copy_from_slice(k.as_slice()); 501 CompressedPubKey::P256(CompressedP256PubKey::from((key, y_is_odd))) 502 }) 503 } else { 504 Err(Error::from("Encoded CompressedP256PubKey is invalid".to_owned())) 505 } 506 } 507 ) 508 }, 509 |k| { 510 if k.len() == 32 { 511 let mut key = [0; 32]; 512 key.copy_from_slice(k.as_slice()); 513 Ok(CompressedPubKey::Ed25519(Ed25519PubKey::from(key))) 514 } else { 515 Err(Error::from("Encoded Ed25519PubKey is invalid".to_owned())) 516 } 517 } 518 )?; 519 let cred_protect = match r.7 { 520 0 => Ok(CredentialProtectionPolicy::None), 521 1 => Ok(CredentialProtectionPolicy::UserVerificationOptional), 522 2 => Ok(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList), 523 3 => Ok(CredentialProtectionPolicy::UserVerificationRequired), 524 _ => Err(Error::from("Encoded AuthTransports is invalid".to_owned())), 525 }?; 526 if r.9.len() == 7 { 527 let mut dec_data = [0; 7]; 528 dec_data.copy_from_slice(r.9.as_slice()); 529 AuthenticatedCredential::new( 530 credential_id, 531 user_handle, 532 StaticState { 533 credential_public_key, 534 extensions: AuthenticatorExtensionOutputStaticState { cred_protect, hmac_secret: r.8 }, 535 }, 536 DynamicState::decode(dec_data) 537 .map_err(AggErr::DecodeDynamicState)?, 538 ) 539 .map_err(AggErr::Credential) 540 .map_err(Error::from) 541 .map(Some) 542 } else { 543 Err(Error::from("Encoded DynamicState is invalid".to_owned())) 544 } 545 }, 546 ) 547 }) 548 }) 549 } 550 #[allow(clippy::clone_on_ref_ptr)] 551 pub async fn delete_by_user_uuid_and_id( 552 user_uuid: &str, 553 id: i64, 554 conn: &DbConn, 555 ) -> EmptyResult { 556 use diesel::prelude::{ExpressionMethods, RunQueryDsl}; 557 use diesel::result; 558 let mut con_res = conn.conn.clone().lock_owned().await; 559 let con = con_res.as_mut().expect("unable to get a pooled connection"); 560 task::block_in_place(move || { 561 diesel::delete(webauthn::table) 562 .filter(webauthn::user_uuid.eq(user_uuid)) 563 .filter(webauthn::id.eq(id)) 564 .execute(con) 565 .map_err(result::Error::into) 566 .and_then(|count| { 567 if count == 1 { 568 Ok(()) 569 } else { 570 Err(Error::from(String::from( 571 "exactly one webauthn row would not have been removed for the user", 572 ))) 573 } 574 }) 575 }) 576 } 577 } 578 579 impl Totp { 580 #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] 581 pub async fn replace(self, conn: &DbConn) -> EmptyResult { 582 use __sqlite_model::TotpDb; 583 use diesel::prelude::{ExpressionMethods, RunQueryDsl}; 584 use diesel::result; 585 let mut con_res = conn.conn.clone().lock_owned().await; 586 let con = con_res.as_mut().expect("unable to get a pooled connection"); 587 task::block_in_place(move || { 588 diesel::update(totp::table) 589 .set(totp::last_used.eq(self.last_used)) 590 .filter(totp::user_uuid.eq(&self.user_uuid)) 591 .execute(con) 592 .map_err(result::Error::into) 593 .and_then(|count| { 594 if count == 1 { 595 Ok(()) 596 } else { 597 diesel::insert_into(totp::table) 598 .values(TotpDb::to_db(&self)) 599 .execute(con) 600 .map_err(result::Error::into) 601 .and_then(|count| { 602 if count == 1 { 603 Ok(()) 604 } else { 605 Err(Error::from(String::from( 606 "exactly one totp row was not inserted/updated", 607 ))) 608 } 609 }) 610 } 611 }) 612 }) 613 } 614 #[allow(clippy::clone_on_ref_ptr)] 615 pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Result<Option<Self>, Error> { 616 use __sqlite_model::TotpDb; 617 use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; 618 use diesel::result; 619 use diesel::OptionalExtension; 620 let mut con_res = conn.conn.clone().lock_owned().await; 621 let con = con_res.as_mut().expect("unable to get a pooled connection"); 622 task::block_in_place(move || { 623 totp::table 624 .filter(totp::user_uuid.eq(user_uuid)) 625 .first::<TotpDb>(con) 626 .optional() 627 .map_err(result::Error::into) 628 .map(FromDb::from_db) 629 }) 630 } 631 }