Get auth challenge
This commit is contained in:
parent
f09a62f8df
commit
1d69ea536f
@ -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 %}
|
Loading…
Reference in New Issue
Block a user