mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
parent
86b2b5148d
commit
278fb1630d
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -2404,7 +2404,7 @@ dependencies = [
|
||||
"tracing-forest",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"uuid 0.8.2",
|
||||
"uuid 1.3.0",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
@ -2418,6 +2418,7 @@ dependencies = [
|
||||
"gloo-console",
|
||||
"gloo-file",
|
||||
"gloo-net",
|
||||
"gloo-timers",
|
||||
"graphql_client 0.10.0",
|
||||
"http",
|
||||
"image",
|
||||
@ -2427,6 +2428,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"url-escape",
|
||||
"validator",
|
||||
"validator_derive",
|
||||
@ -2530,12 +2532,6 @@ dependencies = [
|
||||
"digest 0.10.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
@ -4404,9 +4400,6 @@ name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"md5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
@ -4415,6 +4408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
|
||||
dependencies = [
|
||||
"getrandom 0.2.8",
|
||||
"md-5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -19,6 +19,7 @@ serde = "1"
|
||||
serde_json = "1"
|
||||
url-escape = "0.1.1"
|
||||
validator = "=0.14"
|
||||
sha1 = "*"
|
||||
validator_derive = "*"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "*"
|
||||
@ -27,6 +28,7 @@ yew-router = "0.16"
|
||||
|
||||
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
|
||||
indexmap = "=1.6.2"
|
||||
gloo-timers = "0.2.6"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
components::password_field::PasswordField,
|
||||
components::router::{AppRoute, Link},
|
||||
infra::{
|
||||
api::HostService,
|
||||
@ -254,14 +255,12 @@ impl Component for ChangePasswordForm {
|
||||
{":"}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<Field
|
||||
<PasswordField<FormModel>
|
||||
form={&self.form}
|
||||
field_name="password"
|
||||
input_type="password"
|
||||
class="form-control"
|
||||
class_invalid="is-invalid has-error"
|
||||
class_valid="has-success"
|
||||
autocomplete="new-password"
|
||||
oninput={link.callback(|_| Msg::FormUpdate)} />
|
||||
<div class="invalid-feedback">
|
||||
{&self.form.field_message("password")}
|
||||
|
@ -149,8 +149,8 @@ impl Component for LoginForm {
|
||||
let link = &ctx.link();
|
||||
if self.refreshing {
|
||||
html! {
|
||||
<div>
|
||||
<img src={"spinner.gif"} alt={"Loading"} />
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">{"Loading..."}</span>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
|
@ -10,6 +10,7 @@ pub mod group_details;
|
||||
pub mod group_table;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod password_field;
|
||||
pub mod remove_user_from_group;
|
||||
pub mod reset_password_step1;
|
||||
pub mod reset_password_step2;
|
||||
|
152
app/src/components/password_field.rs
Normal file
152
app/src/components/password_field.rs
Normal file
@ -0,0 +1,152 @@
|
||||
use crate::infra::{
|
||||
api::{hash_password, HostService, PasswordHash, PasswordWasLeaked},
|
||||
common_component::{CommonComponent, CommonComponentParts},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use gloo_timers::callback::Timeout;
|
||||
use web_sys::{HtmlInputElement, InputEvent};
|
||||
use yew::{html, Callback, Classes, Component, Context, Properties};
|
||||
use yew_form::{Field, Form, Model};
|
||||
|
||||
pub enum PasswordFieldMsg {
|
||||
OnInput(String),
|
||||
OnInputIdle,
|
||||
PasswordCheckResult(Result<(Option<PasswordWasLeaked>, PasswordHash)>),
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum PasswordState {
|
||||
// Whether the password was found in a leak.
|
||||
Checked(PasswordWasLeaked),
|
||||
// Server doesn't support checking passwords (TODO: move to config).
|
||||
NotSupported,
|
||||
// Requested a check, no response yet from the server.
|
||||
Loading,
|
||||
// User is still actively typing.
|
||||
Typing,
|
||||
}
|
||||
|
||||
pub struct PasswordField<FormModel: Model> {
|
||||
common: CommonComponentParts<Self>,
|
||||
timeout_task: Option<Timeout>,
|
||||
password: String,
|
||||
password_check_state: PasswordState,
|
||||
_marker: std::marker::PhantomData<FormModel>,
|
||||
}
|
||||
|
||||
impl<FormModel: Model> CommonComponent<PasswordField<FormModel>> for PasswordField<FormModel> {
|
||||
fn handle_msg(
|
||||
&mut self,
|
||||
ctx: &Context<Self>,
|
||||
msg: <Self as Component>::Message,
|
||||
) -> anyhow::Result<bool> {
|
||||
match msg {
|
||||
PasswordFieldMsg::OnInput(password) => {
|
||||
self.password = password;
|
||||
if self.password_check_state != PasswordState::NotSupported {
|
||||
self.password_check_state = PasswordState::Typing;
|
||||
if self.password.len() >= 8 {
|
||||
let link = ctx.link().clone();
|
||||
self.timeout_task = Some(Timeout::new(500, move || {
|
||||
link.send_message(PasswordFieldMsg::OnInputIdle)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
PasswordFieldMsg::PasswordCheckResult(result) => {
|
||||
self.timeout_task = None;
|
||||
// If there's an error from the backend, don't retry.
|
||||
self.password_check_state = PasswordState::NotSupported;
|
||||
if let (Some(check), hash) = result? {
|
||||
if hash == hash_password(&self.password) {
|
||||
self.password_check_state = PasswordState::Checked(check)
|
||||
}
|
||||
}
|
||||
}
|
||||
PasswordFieldMsg::OnInputIdle => {
|
||||
self.timeout_task = None;
|
||||
if self.password_check_state != PasswordState::NotSupported {
|
||||
self.password_check_state = PasswordState::Loading;
|
||||
self.common.call_backend(
|
||||
ctx,
|
||||
HostService::check_password_haveibeenpwned(hash_password(&self.password)),
|
||||
PasswordFieldMsg::PasswordCheckResult,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn mut_common(&mut self) -> &mut CommonComponentParts<PasswordField<FormModel>> {
|
||||
&mut self.common
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct PasswordFieldProperties<FormModel: Model> {
|
||||
pub field_name: String,
|
||||
pub form: Form<FormModel>,
|
||||
#[prop_or_else(|| { "form-control".into() })]
|
||||
pub class: Classes,
|
||||
#[prop_or_else(|| { "is-invalid".into() })]
|
||||
pub class_invalid: Classes,
|
||||
#[prop_or_else(|| { "is-valid".into() })]
|
||||
pub class_valid: Classes,
|
||||
#[prop_or_else(Callback::noop)]
|
||||
pub oninput: Callback<String>,
|
||||
}
|
||||
|
||||
impl<FormModel: Model> Component for PasswordField<FormModel> {
|
||||
type Message = PasswordFieldMsg;
|
||||
type Properties = PasswordFieldProperties<FormModel>;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self {
|
||||
common: CommonComponentParts::<Self>::create(),
|
||||
timeout_task: None,
|
||||
password: String::new(),
|
||||
password_check_state: PasswordState::Typing,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
CommonComponentParts::<Self>::update(self, ctx, msg)
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> yew::Html {
|
||||
let link = &ctx.link();
|
||||
html! {
|
||||
<div>
|
||||
<Field<FormModel>
|
||||
autocomplete={"new-password"}
|
||||
input_type={"password"}
|
||||
field_name={ctx.props().field_name.clone()}
|
||||
form={ctx.props().form.clone()}
|
||||
class={ctx.props().class.clone()}
|
||||
class_invalid={ctx.props().class_invalid.clone()}
|
||||
class_valid={ctx.props().class_valid.clone()}
|
||||
oninput={link.callback(|e: InputEvent| {
|
||||
use wasm_bindgen::JsCast;
|
||||
let target = e.target().unwrap();
|
||||
let input = target.dyn_into::<HtmlInputElement>().unwrap();
|
||||
PasswordFieldMsg::OnInput(input.value())
|
||||
})} />
|
||||
{
|
||||
match self.password_check_state {
|
||||
PasswordState::Checked(PasswordWasLeaked(true)) => html! { <i class="bi bi-x"></i> },
|
||||
PasswordState::Checked(PasswordWasLeaked(false)) => html! { <i class="bi bi-check"></i> },
|
||||
PasswordState::NotSupported | PasswordState::Typing => html!{},
|
||||
PasswordState::Loading =>
|
||||
html! {
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="sr-only">{"Loading..."}</span>
|
||||
</div>
|
||||
},
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
use crate::{
|
||||
components::router::{AppRoute, Link},
|
||||
components::{
|
||||
password_field::PasswordField,
|
||||
router::{AppRoute, Link},
|
||||
},
|
||||
infra::{
|
||||
api::HostService,
|
||||
common_component::{CommonComponent, CommonComponentParts},
|
||||
@ -176,14 +179,12 @@ impl Component for ResetPasswordStep2Form {
|
||||
{"New password*:"}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<Field
|
||||
<PasswordField<FormModel>
|
||||
form={&self.form}
|
||||
field_name="password"
|
||||
class="form-control"
|
||||
class_invalid="is-invalid has-error"
|
||||
class_valid="has-success"
|
||||
autocomplete="new-password"
|
||||
input_type="password"
|
||||
oninput={link.callback(|_| Msg::FormUpdate)} />
|
||||
<div class="invalid-feedback">
|
||||
{&self.form.field_message("password")}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::cookies::set_cookie;
|
||||
use crate::infra::cookies::set_cookie;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use gloo_net::http::{Method, Request};
|
||||
use graphql_client::GraphQLQuery;
|
||||
@ -74,6 +74,19 @@ fn set_cookies_from_jwt(response: login::ServerLoginResponse) -> Result<(String,
|
||||
.context("Error setting cookie")
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct PasswordHash(String);
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct PasswordWasLeaked(pub bool);
|
||||
|
||||
pub fn hash_password(password: &str) -> PasswordHash {
|
||||
use sha1::{Digest, Sha1};
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(password);
|
||||
PasswordHash(format!("{:X}", hasher.finalize()))
|
||||
}
|
||||
|
||||
impl HostService {
|
||||
pub async fn graphql_query<QueryType>(
|
||||
variables: QueryType::Variables,
|
||||
@ -194,4 +207,35 @@ impl HostService {
|
||||
!= http::StatusCode::NOT_FOUND,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn check_password_haveibeenpwned(
|
||||
password_hash: PasswordHash,
|
||||
) -> Result<(Option<PasswordWasLeaked>, PasswordHash)> {
|
||||
use lldap_auth::password_reset::*;
|
||||
let hash_prefix = &password_hash.0[0..5];
|
||||
match call_server_json_with_error_message::<PasswordHashList, _>(
|
||||
&format!("/auth/password/check/{}", hash_prefix),
|
||||
NO_BODY,
|
||||
"Could not validate token",
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => {
|
||||
for PasswordHashCount { hash, count } in r.hashes {
|
||||
if password_hash.0[5..] == hash && count != 0 {
|
||||
return Ok((Some(PasswordWasLeaked(true)), password_hash));
|
||||
}
|
||||
}
|
||||
Ok((Some(PasswordWasLeaked(false)), password_hash))
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("[501]:") {
|
||||
// Unimplemented, no API key.
|
||||
Ok((None, password_hash))
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB |
@ -102,6 +102,17 @@ pub mod password_reset {
|
||||
pub user_id: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PasswordHashCount {
|
||||
pub hash: String,
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PasswordHashList {
|
||||
pub hashes: Vec<PasswordHashCount>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
|
@ -59,7 +59,6 @@ version = "4"
|
||||
[dependencies.figment]
|
||||
features = ["env", "toml"]
|
||||
version = "*"
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
version = "0.3"
|
||||
features = ["env-filter", "tracing-log"]
|
||||
|
@ -1,21 +1,22 @@
|
||||
use std::collections::{hash_map::DefaultHasher, HashSet};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::task::Poll;
|
||||
|
||||
use actix_web::{
|
||||
cookie::{Cookie, SameSite},
|
||||
dev::{Service, ServiceRequest, ServiceResponse, Transform},
|
||||
error::{ErrorBadRequest, ErrorUnauthorized},
|
||||
web, HttpRequest, HttpResponse,
|
||||
web, FromRequest, HttpRequest, HttpResponse,
|
||||
};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use chrono::prelude::*;
|
||||
use futures::future::{ok, Ready};
|
||||
use futures_util::FutureExt;
|
||||
use hmac::Hmac;
|
||||
use jwt::{SignWithKey, VerifyWithKey};
|
||||
use secstr::SecUtf8;
|
||||
use sha2::Sha512;
|
||||
use time::ext::NumericalDuration;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
@ -205,6 +206,24 @@ where
|
||||
.unwrap_or_else(error_to_http_response)
|
||||
}
|
||||
|
||||
async fn check_password_reset_token<'a, Backend>(
|
||||
backend_handler: &Backend,
|
||||
token: &Option<&'a str>,
|
||||
) -> TcpResult<Option<(&'a str, UserId)>>
|
||||
where
|
||||
Backend: TcpBackendHandler + 'static,
|
||||
{
|
||||
let token = match token {
|
||||
None => return Ok(None),
|
||||
Some(token) => token,
|
||||
};
|
||||
let user_id = backend_handler
|
||||
.get_user_id_for_password_reset_token(token)
|
||||
.await
|
||||
.map_err(|_| TcpError::UnauthorizedError("Invalid or expired token".to_string()))?;
|
||||
Ok(Some((token, user_id)))
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "debug")]
|
||||
async fn get_password_reset_step2<Backend>(
|
||||
data: web::Data<AppState<Backend>>,
|
||||
@ -213,22 +232,12 @@ async fn get_password_reset_step2<Backend>(
|
||||
where
|
||||
Backend: TcpBackendHandler + BackendHandler + 'static,
|
||||
{
|
||||
let token = request
|
||||
.match_info()
|
||||
.get("token")
|
||||
.ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?;
|
||||
let user_id = data
|
||||
.get_tcp_handler()
|
||||
.get_user_id_for_password_reset_token(token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
debug!("Reset token error: {e:#}");
|
||||
TcpError::NotFoundError("Wrong or expired reset token".to_owned())
|
||||
})?;
|
||||
let _ = data
|
||||
.get_tcp_handler()
|
||||
.delete_password_reset_token(token)
|
||||
.await;
|
||||
let tcp_handler = data.get_tcp_handler();
|
||||
let (token, user_id) =
|
||||
check_password_reset_token(tcp_handler, &request.match_info().get("token"))
|
||||
.await?
|
||||
.ok_or_else(|| TcpError::BadRequest("Missing token".to_string()))?;
|
||||
let _ = tcp_handler.delete_password_reset_token(token).await;
|
||||
let groups = HashSet::new();
|
||||
let token = create_jwt(&data.jwt_key, user_id.to_string(), groups);
|
||||
Ok(HttpResponse::Ok()
|
||||
@ -403,6 +412,7 @@ where
|
||||
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static,
|
||||
{
|
||||
let user_id = UserId::new(&request.username);
|
||||
debug!(?user_id);
|
||||
let bind_request = BindRequest {
|
||||
name: user_id.clone(),
|
||||
password: request.password.clone(),
|
||||
@ -449,6 +459,115 @@ where
|
||||
.unwrap_or_else(error_to_http_response)
|
||||
}
|
||||
|
||||
// Parse the response from the HaveIBeenPwned API. Sample response:
|
||||
//
|
||||
// 0018A45C4D1DEF81644B54AB7F969B88D65:1
|
||||
// 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
|
||||
// 011053FD0102E94D6AE2F8B83D76FAF94F6:13
|
||||
fn parse_hash_list(response: &str) -> Result<password_reset::PasswordHashList> {
|
||||
use password_reset::*;
|
||||
let parse_line = |line: &str| -> Result<PasswordHashCount> {
|
||||
let split = line.trim().split(':').collect::<Vec<_>>();
|
||||
if let [hash, count] = &split[..] {
|
||||
if hash.len() == 35 {
|
||||
if let Ok(count) = str::parse::<u64>(count) {
|
||||
return Ok(PasswordHashCount {
|
||||
hash: hash.to_string(),
|
||||
count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("Invalid password hash from API: {}", line)
|
||||
};
|
||||
Ok(PasswordHashList {
|
||||
hashes: response
|
||||
.split('\n')
|
||||
.map(parse_line)
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Refactor that for testing.
|
||||
async fn get_password_hash_list(
|
||||
hash: &str,
|
||||
api_key: &SecUtf8,
|
||||
) -> Result<password_reset::PasswordHashList> {
|
||||
use reqwest::*;
|
||||
let client = Client::new();
|
||||
let resp = client
|
||||
.get(format!("https://api.pwnedpasswords.com/range/{}", hash))
|
||||
.header(header::USER_AGENT, "LLDAP")
|
||||
.header("hibp-api-key", api_key.unsecure())
|
||||
.send()
|
||||
.await
|
||||
.context("Could not get response from HIPB")?
|
||||
.text()
|
||||
.await?;
|
||||
parse_hash_list(&resp).context("Invalid HIPB response")
|
||||
}
|
||||
|
||||
async fn check_password_pwned<Backend>(
|
||||
data: web::Data<AppState<Backend>>,
|
||||
request: HttpRequest,
|
||||
payload: web::Payload,
|
||||
) -> TcpResult<HttpResponse>
|
||||
where
|
||||
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
|
||||
{
|
||||
let has_reset_token = check_password_reset_token(
|
||||
data.get_tcp_handler(),
|
||||
&request
|
||||
.headers()
|
||||
.get("reset-token")
|
||||
.map(|v| v.to_str().unwrap()),
|
||||
)
|
||||
.await?
|
||||
.is_some();
|
||||
let inner_payload = &mut payload.into_inner();
|
||||
if !has_reset_token
|
||||
&& BearerAuth::from_request(&request, inner_payload)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|bearer| check_if_token_is_valid(&data, bearer.token()).ok())
|
||||
.is_none()
|
||||
{
|
||||
return Err(TcpError::UnauthorizedError(
|
||||
"No token or invalid token".to_string(),
|
||||
));
|
||||
}
|
||||
if data.hipb_api_key.unsecure().is_empty() {
|
||||
return Err(TcpError::NotImplemented("No HIPB API key".to_string()));
|
||||
}
|
||||
let hash = request
|
||||
.match_info()
|
||||
.get("hash")
|
||||
.ok_or_else(|| TcpError::BadRequest("Missing hash".to_string()))?;
|
||||
if hash.len() != 5 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(TcpError::BadRequest(format!(
|
||||
"Bad request: invalid hash format \"{}\"",
|
||||
hash
|
||||
)));
|
||||
}
|
||||
get_password_hash_list(hash, &data.hipb_api_key)
|
||||
.await
|
||||
.map(|hashes| HttpResponse::Ok().json(hashes))
|
||||
.map_err(|e| TcpError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn check_password_pwned_handler<Backend>(
|
||||
data: web::Data<AppState<Backend>>,
|
||||
request: HttpRequest,
|
||||
payload: web::Payload,
|
||||
) -> HttpResponse
|
||||
where
|
||||
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
|
||||
{
|
||||
check_password_pwned(data, request, payload)
|
||||
.await
|
||||
.unwrap_or_else(error_to_http_response)
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "debug")]
|
||||
async fn opaque_register_start<Backend>(
|
||||
request: actix_web::HttpRequest,
|
||||
@ -565,7 +684,7 @@ where
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future = Pin<Box<dyn core::future::Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(cx)
|
||||
}
|
||||
|
||||
@ -636,6 +755,11 @@ where
|
||||
web::resource("/simple/login").route(web::post().to(simple_login_handler::<Backend>)),
|
||||
)
|
||||
.service(web::resource("/refresh").route(web::get().to(get_refresh_handler::<Backend>)))
|
||||
.service(
|
||||
web::resource("/password/check/{hash}")
|
||||
.wrap(CookieToHeaderTranslatorFactory)
|
||||
.route(web::get().to(check_password_pwned_handler::<Backend>)),
|
||||
)
|
||||
.service(web::resource("/logout").route(web::get().to(get_logout_handler::<Backend>)))
|
||||
.service(
|
||||
web::scope("/opaque/register")
|
||||
|
@ -81,6 +81,10 @@ pub struct RunOpts {
|
||||
#[clap(short, long, env = "LLDAP_DATABASE_URL")]
|
||||
pub database_url: Option<String>,
|
||||
|
||||
/// HaveIBeenPwned API key, to check passwords against leaks.
|
||||
#[clap(long, env = "LLDAP_HIPB_API_KEY")]
|
||||
pub hipb_api_key: Option<String>,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub smtp_opts: SmtpOpts,
|
||||
|
||||
|
@ -98,6 +98,8 @@ pub struct Configuration {
|
||||
pub ldaps_options: LdapsOptions,
|
||||
#[builder(default = r#"String::from("http://localhost")"#)]
|
||||
pub http_url: String,
|
||||
#[builder(default = r#"SecUtf8::from("")"#)]
|
||||
pub hipb_api_key: SecUtf8,
|
||||
#[serde(skip)]
|
||||
#[builder(field(private), default = "None")]
|
||||
server_setup: Option<ServerSetup>,
|
||||
@ -213,6 +215,10 @@ impl ConfigOverrider for RunOpts {
|
||||
if let Some(database_url) = self.database_url.as_ref() {
|
||||
config.database_url = database_url.to_string();
|
||||
}
|
||||
|
||||
if let Some(api_key) = self.hipb_api_key.as_ref() {
|
||||
config.hipb_api_key = SecUtf8::from(api_key.clone());
|
||||
}
|
||||
self.smtp_opts.override_config(config);
|
||||
self.ldaps_opts.override_config(config);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ use actix_service::map_config;
|
||||
use actix_web::{dev::AppConfig, guard, web, App, HttpResponse, Responder};
|
||||
use anyhow::{Context, Result};
|
||||
use hmac::Hmac;
|
||||
use secstr::SecUtf8;
|
||||
use sha2::Sha512;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
@ -38,10 +39,10 @@ pub enum TcpError {
|
||||
BadRequest(String),
|
||||
#[error("Internal server error: `{0}`")]
|
||||
InternalServerError(String),
|
||||
#[error("Not found: `{0}`")]
|
||||
NotFoundError(String),
|
||||
#[error("Unauthorized: `{0}`")]
|
||||
UnauthorizedError(String),
|
||||
#[error("Not implemented: `{0}`")]
|
||||
NotImplemented(String),
|
||||
}
|
||||
|
||||
pub type TcpResult<T> = std::result::Result<T, TcpError>;
|
||||
@ -60,9 +61,9 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse {
|
||||
| DomainError::EntityNotFound(_) => HttpResponse::BadRequest(),
|
||||
},
|
||||
TcpError::BadRequest(_) => HttpResponse::BadRequest(),
|
||||
TcpError::NotFoundError(_) => HttpResponse::NotFound(),
|
||||
TcpError::InternalServerError(_) => HttpResponse::InternalServerError(),
|
||||
TcpError::UnauthorizedError(_) => HttpResponse::Unauthorized(),
|
||||
TcpError::NotImplemented(_) => HttpResponse::NotImplemented(),
|
||||
}
|
||||
.body(error.to_string())
|
||||
}
|
||||
@ -88,6 +89,7 @@ fn http_config<Backend>(
|
||||
jwt_blacklist: HashSet<u64>,
|
||||
server_url: String,
|
||||
mail_options: MailOptions,
|
||||
hipb_api_key: SecUtf8,
|
||||
) where
|
||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
|
||||
{
|
||||
@ -98,6 +100,7 @@ fn http_config<Backend>(
|
||||
jwt_blacklist: RwLock::new(jwt_blacklist),
|
||||
server_url,
|
||||
mail_options,
|
||||
hipb_api_key,
|
||||
}))
|
||||
.route(
|
||||
"/health",
|
||||
@ -133,6 +136,7 @@ pub(crate) struct AppState<Backend> {
|
||||
pub jwt_blacklist: RwLock<HashSet<u64>>,
|
||||
pub server_url: String,
|
||||
pub mail_options: MailOptions,
|
||||
pub hipb_api_key: SecUtf8,
|
||||
}
|
||||
|
||||
impl<Backend: BackendHandler> AppState<Backend> {
|
||||
@ -173,6 +177,7 @@ where
|
||||
let mail_options = config.smtp_options.clone();
|
||||
let verbose = config.verbose;
|
||||
info!("Starting the API/web server on port {}", config.http_port);
|
||||
let hipb_api_key = config.hipb_api_key.clone();
|
||||
server_builder
|
||||
.bind(
|
||||
"http",
|
||||
@ -183,6 +188,7 @@ where
|
||||
let jwt_blacklist = jwt_blacklist.clone();
|
||||
let server_url = server_url.clone();
|
||||
let mail_options = mail_options.clone();
|
||||
let hipb_api_key = hipb_api_key.clone();
|
||||
HttpServiceBuilder::default()
|
||||
.finish(map_config(
|
||||
App::new()
|
||||
@ -198,6 +204,7 @@ where
|
||||
jwt_blacklist,
|
||||
server_url,
|
||||
mail_options,
|
||||
hipb_api_key,
|
||||
)
|
||||
}),
|
||||
|_| AppConfig::default(),
|
||||
|
Loading…
Reference in New Issue
Block a user