From 1d69ea536f501866a5166943f28633fa043a1187 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 23 Apr 2022 18:56:14 +0200 Subject: [PATCH] Get auth challenge --- README.md | 1 + src/controllers/login_controller.rs | 74 +++++++++++++++++++++++++++++ src/data/webauthn_manager.rs | 40 +++++++++++++--- src/main.rs | 1 + templates/login/webauthn_input.html | 20 ++++++++ 5 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 templates/login/webauthn_input.html diff --git a/README.md b/README.md index 86287da..d8265a3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Features : * [x] TOTP (authenticator app) * [ ] Using a security key * [ ] Fully responsive webui +* [ ] `robots.txt` file to prevent indexing ## Compiling You will need the Rust toolchain to compile this project. To build it for production, just run: diff --git a/src/controllers/login_controller.rs b/src/controllers/login_controller.rs index 4d9c718..0ccdac8 100644 --- a/src/controllers/login_controller.rs +++ b/src/controllers/login_controller.rs @@ -12,6 +12,7 @@ use crate::data::login_redirect::LoginRedirect; use crate::data::remote_ip::RemoteIP; use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; +use crate::data::webauthn_manager::WebAuthManagerReq; struct BaseLoginPage<'a> { danger: Option, @@ -49,6 +50,15 @@ struct LoginWithOTPTemplate<'a> { factor: &'a TwoFactor, } +#[derive(Template)] +#[template(path = "login/webauthn_input.html")] +struct LoginWithWebauthnTemplate<'a> { + _p: BaseLoginPage<'a>, + factor: &'a TwoFactor, + opaque_state: String, + challenge_json: String, +} + #[derive(serde::Deserialize)] pub struct LoginRequestBody { @@ -327,4 +337,68 @@ pub async fn login_with_otp(id: Identity, query: web::Query, }, factor, }.render().unwrap()) +} + +#[derive(serde::Deserialize)] +pub struct LoginWithWebauthnQuery { + #[serde(default)] + redirect: LoginRedirect, + id: FactorID, +} + + +/// Login with Webauthn +pub async fn login_with_webauthn(id: Identity, query: web::Query, + manager: WebAuthManagerReq, + users: web::Data>) -> impl Responder { + if !SessionIdentity(&id).need_2fa_auth() { + return redirect_user_for_login(query.redirect.get()); + } + + let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id())) + .await.unwrap().0.expect("Could not find user!"); + + let factor = match user.find_factor(&query.id) { + Some(f) => f, + None => return HttpResponse::Ok() + .body(FatalErrorPage { message: "Factor not found!" }.render().unwrap()) + }; + + let key = match &factor.kind { + TwoFactorType::WEBAUTHN(key) => key, + _ => { + return HttpResponse::Ok() + .body(FatalErrorPage { message: "Factor is not a Webauthn key!" }.render().unwrap()); + } + }; + + let challenge = match manager.start_authentication(&user.uid, key) { + Ok(c) => c, + Err(e) => { + log::error!("Failed to generate webauthn challenge! {:?}", e); + return HttpResponse::InternalServerError() + .body(FatalErrorPage { message: "Failed to generate webauthn challenge" }.render().unwrap()); + } + }; + + let challenge_json = match serde_json::to_string(&challenge.login_challenge) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to serialize challenge! {:?}", e); + return HttpResponse::InternalServerError().body("Failed to serialize challenge!"); + } + }; + + HttpResponse::Ok().body(LoginWithWebauthnTemplate { + _p: BaseLoginPage { + danger: None, + success: None, + page_title: "Two-Factor Auth", + app_name: APP_NAME, + redirect_uri: &query.redirect, + }, + factor, + opaque_state: challenge.opaque_state, + challenge_json: urlencoding::encode(&challenge_json).to_string(), + }.render().unwrap()) } \ No newline at end of file diff --git a/src/data/webauthn_manager.rs b/src/data/webauthn_manager.rs index 69e320c..55f2c42 100644 --- a/src/data/webauthn_manager.rs +++ b/src/data/webauthn_manager.rs @@ -2,8 +2,8 @@ use std::io::ErrorKind; use std::sync::Arc; use actix_web::web; -use webauthn_rs::{RegistrationState, Webauthn, WebauthnConfig}; -use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential}; +use webauthn_rs::{AuthenticationState, RegistrationState, Webauthn, WebauthnConfig}; +use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential, RequestChallengeResponse}; use crate::constants::APP_NAME; use crate::data::app_config::AppConfig; @@ -31,22 +31,34 @@ impl WebauthnConfig for WebAuthnAppConfig { } } -pub struct RegisterKeyRequest { - pub opaque_state: String, - pub creation_challenge: CreationChallengeResponse, -} - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct WebauthnPubKey { creds: Credential, } +pub struct RegisterKeyRequest { + pub opaque_state: String, + pub creation_challenge: CreationChallengeResponse, +} + #[derive(Debug, serde::Serialize, serde::Deserialize)] struct RegisterKeyOpaqueData { registration_state: RegistrationState, user_id: UserID, } +pub struct AuthRequest { + pub opaque_state: String, + pub login_challenge: RequestChallengeResponse, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct AuthStateOpaqueData { + authentication_state: AuthenticationState, + user_id: UserID, +} + + pub type WebAuthManagerReq = web::Data>; pub struct WebAuthManager { @@ -97,4 +109,18 @@ impl WebAuthManager { Ok(WebauthnPubKey { creds: res.0 }) } + + pub fn start_authentication(&self, user_id: &UserID, key: &WebauthnPubKey) -> Res { + let (login_challenge, authentication_state) = self.core.generate_challenge_authenticate(vec![ + key.creds.clone() + ])?; + + Ok(AuthRequest { + opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData { + authentication_state, + user_id: user_id.clone(), + })?, + login_challenge, + }) + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1806614..8cb1004 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,6 +119,7 @@ async fn main() -> std::io::Result<()> { .route("/2fa_auth", web::get().to(login_controller::choose_2fa_method)) .route("/2fa_otp", web::get().to(login_controller::login_with_otp)) .route("/2fa_otp", web::post().to(login_controller::login_with_otp)) + .route("/2fa_webauthn", web::get().to(login_controller::login_with_webauthn)) // Logout page .route("/logout", web::get().to(login_controller::logout_route)) diff --git a/templates/login/webauthn_input.html b/templates/login/webauthn_input.html new file mode 100644 index 0000000..a70ad66 --- /dev/null +++ b/templates/login/webauthn_input.html @@ -0,0 +1,20 @@ +{% extends "base_login_page.html" %} +{% block content %} + + +
+

Please insert now your security key {{ factor.name }}, and accept authentication request.

+
+ + + + + + +{% endblock content %} \ No newline at end of file