Add webauthn #8
@ -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:
|
||||
|
@ -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<String>,
|
||||
@ -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 {
|
||||
@ -328,3 +338,67 @@ pub async fn login_with_otp(id: Identity, query: web::Query<LoginWithOTPQuery>,
|
||||
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<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 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<Arc<WebAuthManager>>;
|
||||
|
||||
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<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_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))
|
||||
|
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 %}
|
Loading…
Reference in New Issue
Block a user