FEA: matrix authentication

This commit is contained in:
Keanu D?lle 2021-06-11 12:01:26 +02:00
parent d2cbfa3cb5
commit 5dfcf27339
11 changed files with 249 additions and 43 deletions

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
alter table users drop column username;
drop table login_attempts_usernames;

View File

@ -0,0 +1,15 @@
-- Your SQL goes here
alter table users
add username text;
create unique index users_username_uindex
on users (username);
create table login_attempts_usernames
(
id uuid default uuid_generate_v1() not null
constraint login_attempts_usernames_pkey
primary key,
username text not null,
timestamp timestamp default now() not null
);

View File

@ -1 +1 @@
v0.2-3-g1b1d458
v0.2-5-gd2cbfa3

View File

@ -32,6 +32,30 @@ pub fn login_attempts_exceeded(settings: &State<Settings>, email: String) -> Res
}
}
/// Checks if maximum login attempts for username exceeded. Locks account if exceeded
/// Returns:
/// * Ok(true) if login attempts exceeded and account got locked
/// * Ok(false) if login attempts not exceeded
/// * Err(diesel::result::Error) if database error occured
pub fn login_attempts_usernames_exceeded(settings: &State<Settings>, username: String) -> Result<bool, diesel::result::Error>{
let connection = establish_connection(settings);
let result : Result<LoginAttemptsResult, diesel::result::Error> = sql_query("SELECT COUNT(*) AS count, MAX(timestamp) AS last_timestamp FROM login_attempts_usernames WHERE username=$1 AND timestamp > (now() - interval '1800 seconds')").bind::<Text, _>(username.clone()).get_result(&connection);
let result = match result{
Ok(res) => res,
Err(e) => {
error!("Couldn't check for login attempts: {}", e);
return Err(e)
}
};
if result.count > settings.application.max_login_attempts as i64 {
Ok(true)
}else{
Ok(false)
}
}
pub(crate) fn add_login_attempt(settings: &State<Settings>, email2: String) -> Result<(), diesel::result::Error>{
use crate::schema::login_attempts::dsl::{login_attempts, email};
let connection = establish_connection(settings);
@ -43,4 +67,17 @@ pub(crate) fn add_login_attempt(settings: &State<Settings>, email2: String) -> R
Err(e)
}
}
}
pub(crate) fn add_login_attempt_username(settings: &State<Settings>, username2: String) -> Result<(), diesel::result::Error>{
use crate::schema::login_attempts_usernames::dsl::*;
let connection = establish_connection(settings);
match diesel::insert_into(login_attempts_usernames).values(username.eq(username2)).execute(&connection){
Ok(_) => Ok(()),
Err(e) => {
error!("Couldn't write login attempt into DB! {}", e);
Err(e)
}
}
}

View File

@ -21,6 +21,17 @@ pub fn get_user_by_email(email: String, settings: &State<Settings>) -> Option<Us
}
}
pub fn get_user_by_username(username: String, settings: &State<Settings>) -> Option<User> {
use crate::schema::users::dsl::username as username1;
let connection = establish_connection(settings);
match users.filter(username1.eq(username)).first(&connection) {
Ok(user) => Some(user),
Err(_) => None,
}
}
/// Tries to get user associated to member entity id
/// Returns:
/// * Ok<Some<User>> if user found
@ -31,7 +42,7 @@ pub fn get_user_by_member(settings: &State<Settings>, member_uuid: uuid::Uuid) -
let connection = establish_connection(settings);
let user : Result<User, diesel::result::Error> = members.inner_join(crate::schema::users::table).filter(entity_id.eq(member_uuid)).select((id, password, email)).first(&connection);
let user : Result<User, diesel::result::Error> = members.inner_join(crate::schema::users::table).filter(entity_id.eq(member_uuid)).select((id, password, email, username)).first(&connection);
match user{
Ok(user) => Ok(Some(user)),
Err(e) => {

View File

@ -3,4 +3,5 @@ pub struct User {
pub(crate) id: uuid::Uuid,
pub(crate) password: Option<String>,
pub(crate) email: String,
pub(crate) username: Option<String>,
}

View File

@ -136,6 +136,7 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
};
let inserted_cookie = storage.add(
@ -161,6 +162,7 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
};
let inserted_cookie = storage.add(
@ -187,6 +189,7 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
};
let inserted_cookie = storage.add(
@ -210,6 +213,7 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
};
storage.add(

View File

@ -209,6 +209,7 @@ fn main() {
modules::api::events::instances::delete::delete_instance,
modules::api::events::event_units::position::read::get_check_position_requirements,
modules::api::events::read::read_future_event_for_member,
modules::api::users::read::matrix_check_credentials,
],
)
.mount("/css", StaticFiles::from("resources/css"))

View File

@ -0,0 +1,121 @@
use rocket_contrib::json::Json;
use rocket::State;
use crate::helper::settings::Settings;
use crate::database::controller::users::get_user_by_username;
use crate::database::controller::login_protection::{login_attempts_exceeded, login_attempts_usernames_exceeded, add_login_attempt, add_login_attempt_username};
use crate::database::controller::members::get_members_by_user_uuid;
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct Request{
user: User,
}
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct User{
id: String,
password: String,
}
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct Profile{
display_name: String
}
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct Auth{
success: bool,
mxid: Option<String>,
profile: Option<Profile>,
}
/// Matrix integration
#[post("/_matrix-internal/identity/v1/check_credentials", format = "json", data="<auth>")]
pub fn matrix_check_credentials(
settings: State<Settings>, auth: Json<Request>
) -> Json<Auth> {
let id = auth.user.id.replace("@", "").replace(":drk.digital", "");
match get_user_by_username(id.clone(), &settings){
None => {
match login_attempts_usernames_exceeded(&settings, id.clone()){
Ok(result) => {
if result{
return Json(Auth{
success: false,
mxid: None,
profile: None
})
}else{
add_login_attempt_username(&settings, id);
return Json(Auth{
success: false,
mxid: None,
profile: None
})
}
},
Err(_) => {
return Json(Auth{
success: false,
mxid: None,
profile: None
})
}
}
},
Some(user) => {
let user = match login_attempts_usernames_exceeded(&settings, id.clone()){
Ok(result) => {
if result{
return Json(Auth{
success: false,
mxid: None,
profile: None
})
}else{
user
}
},
Err(_) => {
return Json(Auth{
success: false,
mxid: None,
profile: None
})
}
};
let password_hash = match user.password.clone(){
None => {return Json(Auth{
success: false,
mxid: None,
profile: None
})}
Some(pw) => pw
};
if argon2::verify_encoded(&password_hash, auth.user.password.as_ref()).unwrap() {
let member = match get_members_by_user_uuid(user.id, &settings).first(){
Some(member) => {
return Json(Auth{
success: true,
mxid: Some(format!("@{}:drk.digital", id)),
profile: Some(Profile{
display_name: format!("{} {}", member.firstname, member.lastname)
})
})
},
None => return Json(Auth{
success: false,
mxid: None,
profile: None
})
};
} else {
add_login_attempt_username(&settings, id);
return Json(Auth{
success: false,
mxid: None,
profile: None
})
}
}
}
}

View File

@ -4,7 +4,7 @@ use crate::helper::session_cookies::model::SessionCookieStorage;
use crate::modules::welcome::model::login_error_type::LoginError;
use crate::helper::settings::Settings;
use crate::modules::welcome::model::login_form::LoginForm;
use crate::database::controller::login_protection::add_login_attempt;
use crate::database::controller::login_protection::{add_login_attempt, add_login_attempt_username};
use chrono::{Duration, Utc};
use rocket::http::{Cookie, Cookies};
use rocket::State;
@ -30,7 +30,7 @@ pub fn check_login(login_form: LoginForm, settings: &State<Settings>) -> Result<
if result{
return Err(LoginError::MaxLoginAttemptsExceeded)
}else{
match add_login_attempt(settings, login_form.login_email.to_lowercase()){
match add_login_attempt_username(settings, login_form.login_email.to_lowercase()){
Ok(_) => return Err(LoginError::UserNotFound),
Err(_) => return Err(LoginError::DatabaseError)
}

View File

@ -1,6 +1,6 @@
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
addresses (id) {
id -> Uuid,
@ -15,7 +15,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
addresses_entities (address_id, entitiy_id) {
address_id -> Uuid,
@ -25,7 +25,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
appointment_types (type_id) {
type_id -> Uuid,
@ -37,7 +37,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
appointments (id) {
id -> Uuid,
@ -49,7 +49,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
buildings (entity_id) {
entity_id -> Uuid,
@ -60,7 +60,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
communication_targets (target_id) {
target_id -> Uuid,
@ -73,7 +73,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
communication_types (type_id) {
type_id -> Uuid,
@ -83,7 +83,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
cost_centres (short_id) {
short_id -> Int4,
@ -93,7 +93,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
cost_centres_members (member_entity_id, cost_centre_shortid) {
member_entity_id -> Uuid,
@ -103,7 +103,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
entities (entity_id) {
entity_id -> Uuid,
@ -112,7 +112,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
eu_instances (instance_id) {
instance_id -> Uuid,
@ -124,7 +124,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
eu_position_instances (instance_id, position_id) {
instance_id -> Uuid,
@ -135,7 +135,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
eu_positions (entity_id) {
entity_id -> Uuid,
@ -147,7 +147,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
eu_positions_templates (position_entity_id, template_id) {
position_entity_id -> Uuid,
@ -157,7 +157,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
eu_templates (entity_id) {
entity_id -> Uuid,
@ -168,7 +168,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
eu_vehicle_positions (entity_id) {
entity_id -> Uuid,
@ -181,7 +181,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
event_organisers (entity_id) {
entity_id -> Uuid,
@ -196,7 +196,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
event_types (type_id) {
type_id -> Uuid,
@ -208,7 +208,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
events (entity_id) {
entity_id -> Uuid,
@ -229,7 +229,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
groups (entity_id) {
entity_id -> Uuid,
@ -240,7 +240,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
groups_entities (group_id, entity_id) {
group_id -> Uuid,
@ -250,7 +250,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
license_categories (name) {
name -> Text,
@ -260,7 +260,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
licenses_members (member_id, license_name) {
member_id -> Uuid,
@ -271,7 +271,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
login_attempts (id) {
id -> Uuid,
@ -282,7 +282,18 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
login_attempts_usernames (id) {
id -> Uuid,
username -> Text,
timestamp -> Timestamp,
}
}
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
members (entity_id) {
entity_id -> Uuid,
@ -306,7 +317,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
members_roles (member_id, role_id) {
member_id -> Uuid,
@ -316,7 +327,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
password_resets (token) {
token -> Text,
@ -327,7 +338,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
permissions (permission) {
permission -> Text,
@ -337,7 +348,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
qualification_categories (id) {
id -> Uuid,
@ -348,7 +359,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
qualifications (id) {
id -> Uuid,
@ -360,7 +371,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
qualifications_members (member_id, qualification_id) {
member_id -> Uuid,
@ -370,7 +381,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
roles (id) {
id -> Text,
@ -380,7 +391,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
roles_permissions (role_permission_id) {
role_id -> Text,
@ -391,7 +402,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
roles_permissions_context (role_permission_id, entity) {
role_permission_id -> Uuid,
@ -401,7 +412,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
units (unit_id) {
unit_id -> Uuid,
@ -411,7 +422,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
units_members (unit_id, member_id) {
unit_id -> Uuid,
@ -422,18 +433,19 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
users (id) {
id -> Uuid,
password -> Nullable<Text>,
email -> Text,
username -> Nullable<Text>,
}
}
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
vehicle_categories (id) {
id -> Uuid,
@ -444,7 +456,7 @@ table! {
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
vehicles (entity_id) {
entity_id -> Uuid,
@ -525,6 +537,7 @@ allow_tables_to_appear_in_same_query!(
license_categories,
licenses_members,
login_attempts,
login_attempts_usernames,
members,
members_roles,
password_resets,