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