Add webauthn #8

Merged
pierre merged 10 commits from webauthn into master 2022-04-23 18:25:16 +00:00
5 changed files with 129 additions and 7 deletions
Showing only changes of commit 1d69ea536f - Show all commits

View File

@ -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:

View File

@ -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())
} }

View File

@ -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,
})
}
} }

View File

@ -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))

View 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 %}