use webauthn_rs::prelude::{RegisterPublicKeyCredential, Uuid}; use actix_web::{error, HttpResponse, http::header::{ContentType, self}, web, cookie::{Cookie, SameSite}, HttpRequest}; use crate::{models::{AppState, User, PendingRegistration}, templates}; pub async fn landing_page (app_state: web::Data>) -> HttpResponse { let content: String = templates::landing_page(app_state.hbs.clone()).unwrap(); HttpResponse::Ok() .content_type(ContentType::html()) .body(content) } /// Page you get after clicking a register-user link. /// /// The path contains the registration Uuid segment. pub async fn webauthn_registration(app_state: web::Data>, path: web::Path) -> HttpResponse { let requested_uuid = match Uuid::parse_str(&path.into_inner()) { Ok(p) => p, Err(_) => return HttpResponse::from_error(error::ErrorBadRequest("This registration token is invalid: invalid UUID")), }; let registration_link = app_state.retrieve_registration_link(requested_uuid).await; let user = match registration_link { Ok(Some(user)) => user, Ok(None) => return HttpResponse::from_error(error::ErrorBadRequest("This registration token is invalid: cannot find it in the database")), Err(e) => return HttpResponse::from_error(error::ErrorInternalServerError(e.to_string())), }; let response = templates::register_user_start(app_state.hbs.clone(), requested_uuid, user.user_name).unwrap(); HttpResponse::Ok() .content_type(ContentType::html()) .body(response) } /// First phase of the webauthn key enrolling. /// /// For now, we don't save anything to the DB. We just generate the /// challenge, the UUID and store everything in the session hashmap /// server-side, in the response and cookie on the client-side. pub async fn start_webauthn_registration(app_state: web::Data>, user: web::Json) -> HttpResponse { let uuid = Uuid::new_v4(); let (creation_challenge_response, passkey_registration) = app_state.webauthn.start_passkey_registration(uuid, &user.user_name, &user.user_name, None).unwrap(); let uuid_str = uuid.to_string(); { let mut user_registrations = app_state.session.user_registrations.write().await; user_registrations.insert(uuid, PendingRegistration{ user: user.into_inner(), registration: passkey_registration }); } let res = serde_json::to_string(&creation_challenge_response).unwrap(); let cookie = Cookie::build("uuid", &uuid_str) .secure(true) .same_site(SameSite::Strict) .finish(); HttpResponse::Ok() .content_type(ContentType::json()) .insert_header((header::SET_COOKIE, cookie.encoded().to_string())) .body(res) } /// Final phase of the webauthn key enrolling. /// /// We verify the key enrolling challenge, then store the new key in the DB. pub async fn finish_webauthn_registration(req: HttpRequest, app_state: web::Data>, register: web::Json) -> impl actix_web::Responder { let cook = req.cookie("uuid"); let uuid = Uuid::parse_str(cook.unwrap().value()).unwrap(); let pending_registration: Option = { let user_registrations = app_state.session.user_registrations.read().await; user_registrations.get(&uuid).cloned() }; // 1. Check registration. // 2. Save user. // 3. Save key. match pending_registration { Some(PendingRegistration { user, registration }) => { let registration_result = app_state.webauthn.finish_passkey_registration(®ister, ®istration); match registration_result { Ok(passkey) => { let _ = app_state.save_user(&user).await; let _ = app_state.save_user_key(&uuid, &passkey).await; HttpResponse::Ok() .body("ok") }, Err(_) => HttpResponse::from_error(error::ErrorUnauthorized("Webauthn challenge failed")) } }, None => { return HttpResponse::from_error(error::ErrorInternalServerError("Session expired")) } } }