webauthn.rs (6376B)
1 use crate::{ 2 api::{EmptyResult, JsonResult, PasswordOrOtpData}, 3 auth::Headers, 4 config, 5 db::{ 6 models::{WebAuthn, WebAuthnAuth, WebAuthnChallenge, WebAuthnInfo, WebAuthnReg}, 7 DbConn, 8 }, 9 error::Error, 10 }; 11 use rocket::serde::json::Json; 12 use rocket::Route; 13 use url::Url; 14 use webauthn_rs::prelude::{ 15 PublicKeyCredential, RegisterPublicKeyCredential, Uuid, Webauthn, WebauthnBuilder, 16 WebauthnError, 17 }; 18 19 pub fn routes() -> Vec<Route> { 20 routes![ 21 activate_webauthn, 22 activate_webauthn_put, 23 delete_webauthn, 24 generate_webauthn_challenge, 25 get_webauthn, 26 ] 27 } 28 fn build_webauthn() -> Result<Webauthn, WebauthnError> { 29 WebauthnBuilder::new( 30 config::get_config().domain(), 31 &Url::parse(&config::get_config().domain_origin()).expect("a valid URL"), 32 )? 33 .build() 34 } 35 36 #[post("/two-factor/get-webauthn", data = "<data>")] 37 async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult { 38 let data: PasswordOrOtpData = data.into_inner(); 39 let user = headers.user; 40 data.validate(&user)?; 41 let keys = WebAuthnInfo::get_all_by_user(&user.uuid, &conn).await?; 42 Ok(Json(json!({ 43 "enabled": !keys.is_empty(), 44 "keys": keys, 45 "object": "twoFactorWebAuthn" 46 }))) 47 } 48 #[post("/two-factor/get-webauthn-challenge", data = "<data>")] 49 async fn generate_webauthn_challenge( 50 data: Json<PasswordOrOtpData>, 51 headers: Headers, 52 conn: DbConn, 53 ) -> JsonResult { 54 let data: PasswordOrOtpData = data.into_inner(); 55 let user = headers.user; 56 data.validate(&user)?; 57 let (challenge, registration) = build_webauthn()?.start_securitykey_registration( 58 Uuid::try_parse(user.uuid.as_str()).expect("unable to create UUID"), 59 user.email.as_str(), 60 user.name.as_str(), 61 Some(WebAuthn::get_all_credentials_by_user(&user.uuid, &conn).await?), 62 None, 63 None, 64 )?; 65 WebAuthnChallenge::Reg(WebAuthnReg::new(user.uuid, ®istration)?) 66 .replace(&conn) 67 .await?; 68 let mut challenge_value = serde_json::to_value(challenge.public_key)?; 69 challenge_value["status"] = "ok".into(); 70 challenge_value["errorMessage"] = "".into(); 71 Ok(Json(challenge_value)) 72 } 73 74 #[derive(Deserialize)] 75 #[serde(rename_all = "camelCase")] 76 struct EnableWebauthnData { 77 id: i64, 78 name: String, 79 device_response: RegisterPublicKeyCredential, 80 master_password_hash: String, 81 } 82 83 #[post("/two-factor/webauthn", data = "<data>")] 84 async fn activate_webauthn( 85 data: Json<EnableWebauthnData>, 86 headers: Headers, 87 conn: DbConn, 88 ) -> JsonResult { 89 let data = data.into_inner(); 90 let user = headers.user; 91 PasswordOrOtpData { 92 master_password_hash: Some(data.master_password_hash), 93 otp: None, 94 } 95 .validate(&user)?; 96 // Retrieve and delete the saved challenge state 97 let chall = WebAuthnReg::find_by_user(&user.uuid, &conn) 98 .await? 99 .ok_or_else(|| Error::from(String::from("no webauthn challenge")))?; 100 let registration = chall.security_key_reg()?; 101 WebAuthnChallenge::Reg(chall).delete(&conn).await?; 102 // Verify the credentials with the saved state 103 let security_key = 104 build_webauthn()?.finish_securitykey_registration(&data.device_response, ®istration)?; 105 WebAuthn::new(user.uuid.clone(), data.id, data.name, &security_key)? 106 .insert(&conn) 107 .await?; 108 let keys = WebAuthnInfo::get_all_by_user(user.uuid.as_str(), &conn).await?; 109 Ok(Json(json!({ 110 "enabled": !keys.is_empty(), 111 "keys": keys, 112 "object": "twoFactorWebAuthn" 113 }))) 114 } 115 116 #[put("/two-factor/webauthn", data = "<data>")] 117 async fn activate_webauthn_put( 118 data: Json<EnableWebauthnData>, 119 headers: Headers, 120 conn: DbConn, 121 ) -> JsonResult { 122 activate_webauthn(data, headers, conn).await 123 } 124 125 #[derive(Deserialize)] 126 #[serde(rename_all = "camelCase")] 127 struct DeleteU2FData { 128 id: i64, 129 master_password_hash: String, 130 } 131 132 #[delete("/two-factor/webauthn", data = "<data>")] 133 async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult { 134 if !headers 135 .user 136 .check_valid_password(&data.master_password_hash) 137 { 138 err!("Invalid password"); 139 } 140 WebAuthn::delete_by_user_uuid_and_id(&headers.user.uuid, data.id, &conn).await?; 141 let keys = WebAuthnInfo::get_all_by_user(&headers.user.uuid, &conn).await?; 142 Ok(Json(json!({ 143 "enabled": !keys.is_empty(), 144 "keys": keys, 145 "object": "twoFactorWebAuthn" 146 }))) 147 } 148 149 pub async fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult { 150 let keys = WebAuthn::get_all_security_keys(user_uuid, conn).await?; 151 if keys.is_empty() { 152 err!("No WebAuthn devices registered") 153 } 154 let (challenge, auth) = build_webauthn()?.start_securitykey_authentication(keys.as_slice())?; 155 WebAuthnChallenge::Auth(WebAuthnAuth::new(user_uuid.to_owned(), &auth)?) 156 .replace(conn) 157 .await?; 158 Ok(Json(serde_json::to_value(challenge.public_key)?)) 159 } 160 161 pub async fn validate_webauthn_login( 162 user_uuid: &str, 163 response: &str, 164 conn: &DbConn, 165 ) -> EmptyResult { 166 let chall = WebAuthnAuth::find_by_user(user_uuid, conn) 167 .await? 168 .ok_or_else(|| Error::from(String::from("no webauthn challenge")))?; 169 let security_key_authentication = chall.security_key_auth()?; 170 WebAuthnChallenge::Auth(chall).delete(conn).await?; 171 let resp = serde_json::from_str::<PublicKeyCredential>(response)?; 172 let auth = 173 build_webauthn()?.finish_securitykey_authentication(&resp, &security_key_authentication)?; 174 let mut web = WebAuthn::get_by_cred_id(&resp.id, conn) 175 .await? 176 .ok_or_else(|| Error::from(String::from("no matching webauthn entry")))?; 177 if auth.needs_update() { 178 let mut sec_key = web.security_key()?; 179 if let Some(update) = sec_key.update_credential(&auth) { 180 if update { 181 web.set_security_key(&sec_key)?; 182 web.update(conn).await?; 183 Ok(()) 184 } else { 185 unreachable!("webauthn credential no longer needs to be updated") 186 } 187 } else { 188 unreachable!("webauthn credential no longer matches challenge") 189 } 190 } else { 191 Ok(()) 192 } 193 }