Get auth challenge
This commit is contained in:
		@@ -28,6 +28,7 @@ Features :
 | 
				
			|||||||
  * [x] TOTP (authenticator app)
 | 
					  * [x] TOTP (authenticator app)
 | 
				
			||||||
  * [ ] Using a security key
 | 
					  * [ ] Using a security key
 | 
				
			||||||
* [ ] Fully responsive webui
 | 
					* [ ] Fully responsive webui
 | 
				
			||||||
 | 
					* [ ] `robots.txt` file to prevent indexing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Compiling
 | 
					## Compiling
 | 
				
			||||||
You will need the Rust toolchain to compile this project. To build it for production, just run:
 | 
					You will need the Rust toolchain to compile this project. To build it for production, just run:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ use crate::data::login_redirect::LoginRedirect;
 | 
				
			|||||||
use crate::data::remote_ip::RemoteIP;
 | 
					use crate::data::remote_ip::RemoteIP;
 | 
				
			||||||
use crate::data::session_identity::{SessionIdentity, SessionStatus};
 | 
					use crate::data::session_identity::{SessionIdentity, SessionStatus};
 | 
				
			||||||
use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User};
 | 
					use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User};
 | 
				
			||||||
 | 
					use crate::data::webauthn_manager::WebAuthManagerReq;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct BaseLoginPage<'a> {
 | 
					struct BaseLoginPage<'a> {
 | 
				
			||||||
    danger: Option<String>,
 | 
					    danger: Option<String>,
 | 
				
			||||||
@@ -49,6 +50,15 @@ struct LoginWithOTPTemplate<'a> {
 | 
				
			|||||||
    factor: &'a TwoFactor,
 | 
					    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)]
 | 
					#[derive(serde::Deserialize)]
 | 
				
			||||||
pub struct LoginRequestBody {
 | 
					pub struct LoginRequestBody {
 | 
				
			||||||
@@ -327,4 +337,68 @@ pub async fn login_with_otp(id: Identity, query: web::Query<LoginWithOTPQuery>,
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        factor,
 | 
					        factor,
 | 
				
			||||||
    }.render().unwrap())
 | 
					    }.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<LoginWithWebauthnQuery>,
 | 
				
			||||||
 | 
					                                 manager: WebAuthManagerReq,
 | 
				
			||||||
 | 
					                                 users: web::Data<Addr<UsersActor>>) -> 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())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -2,8 +2,8 @@ use std::io::ErrorKind;
 | 
				
			|||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use actix_web::web;
 | 
					use actix_web::web;
 | 
				
			||||||
use webauthn_rs::{RegistrationState, Webauthn, WebauthnConfig};
 | 
					use webauthn_rs::{AuthenticationState, RegistrationState, Webauthn, WebauthnConfig};
 | 
				
			||||||
use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential};
 | 
					use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential, RequestChallengeResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::APP_NAME;
 | 
					use crate::constants::APP_NAME;
 | 
				
			||||||
use crate::data::app_config::AppConfig;
 | 
					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)]
 | 
					#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
pub struct WebauthnPubKey {
 | 
					pub struct WebauthnPubKey {
 | 
				
			||||||
    creds: Credential,
 | 
					    creds: Credential,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct RegisterKeyRequest {
 | 
				
			||||||
 | 
					    pub opaque_state: String,
 | 
				
			||||||
 | 
					    pub creation_challenge: CreationChallengeResponse,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
 | 
					#[derive(Debug, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
struct RegisterKeyOpaqueData {
 | 
					struct RegisterKeyOpaqueData {
 | 
				
			||||||
    registration_state: RegistrationState,
 | 
					    registration_state: RegistrationState,
 | 
				
			||||||
    user_id: UserID,
 | 
					    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<Arc<WebAuthManager>>;
 | 
					pub type WebAuthManagerReq = web::Data<Arc<WebAuthManager>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct WebAuthManager {
 | 
					pub struct WebAuthManager {
 | 
				
			||||||
@@ -97,4 +109,18 @@ impl WebAuthManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Ok(WebauthnPubKey { creds: res.0 })
 | 
					        Ok(WebauthnPubKey { creds: res.0 })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn start_authentication(&self, user_id: &UserID, key: &WebauthnPubKey) -> Res<AuthRequest> {
 | 
				
			||||||
 | 
					        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,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -119,6 +119,7 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
            .route("/2fa_auth", web::get().to(login_controller::choose_2fa_method))
 | 
					            .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::get().to(login_controller::login_with_otp))
 | 
				
			||||||
            .route("/2fa_otp", web::post().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
 | 
					            // Logout page
 | 
				
			||||||
            .route("/logout", web::get().to(login_controller::logout_route))
 | 
					            .route("/logout", web::get().to(login_controller::logout_route))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								templates/login/webauthn_input.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								templates/login/webauthn_input.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					{% extends "base_login_page.html" %}
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
					    <p>Please insert now your security key <i>{{ factor.name }}</i>, and accept authentication request.</p>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div style="margin-top: 10px;">
 | 
				
			||||||
 | 
					    <a href="/2fa_auth?force_display=true&redirect={{ _p.redirect_uri.get_encoded() }}">Sign in using another factor</a><br/>
 | 
				
			||||||
 | 
					    <a href="/logout">Sign out</a>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					    const OPAQUE_STATE = "{{ opaque_state }}";
 | 
				
			||||||
 | 
					    const AUTH_CHALLENGE = JSON.parse(decodeURIComponent("{{ challenge_json }}"));
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock content %}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user