Moving back to using passwords for authentication
This commit is contained in:
parent
6a5bbfd2a8
commit
864ce0c13d
17 changed files with 169 additions and 198 deletions
2
api/.env
2
api/.env
|
@ -1,7 +1,7 @@
|
|||
HOSTNAME=localhost
|
||||
PORT=42069
|
||||
DOMAIN=http://api.debtpirate.app
|
||||
RP_ID=api.debtpirate.app
|
||||
RP_ID=debtpirate.app
|
||||
TOKEN_KEY=k4.local.hWoS2ZulK9xPEATtXH1Dvj_iynzqfUv5ER5_IFTg5-Q
|
||||
DATABASE_URL=postgres://debt_pirate:HRURqlUmtjIy@192.168.122.251/debt_pirate
|
||||
ASSETS_DIR=/home/zcdziura/Documents/Projects/debt-pirate/api/assets
|
||||
|
|
|
@ -14,8 +14,9 @@ axum = { version = "0.7", features = [
|
|||
base64 = "0.22"
|
||||
dotenvy = "0.15"
|
||||
futures = "0.3"
|
||||
http = "1.0.0"
|
||||
http = "1.0"
|
||||
humantime = "2.1.0"
|
||||
humantime-serde = "1.1"
|
||||
hyper = { version = "1.1", features = ["full"] }
|
||||
lettre = { version = "0.11", default-features = false, features = [
|
||||
"builder",
|
||||
|
@ -28,9 +29,10 @@ lettre = { version = "0.11", default-features = false, features = [
|
|||
log = "0.4"
|
||||
num_cpus = "1.16"
|
||||
once_cell = "1.19"
|
||||
pasetors = "0.6"
|
||||
pasetors = "0.7"
|
||||
serde = { version = "1.0", features = ["derive", "rc", "std"] }
|
||||
serde_json = "1.0"
|
||||
serde_with = "3.9"
|
||||
sqlx = { version = "0.8", features = [
|
||||
"default",
|
||||
"chrono",
|
||||
|
@ -38,8 +40,6 @@ sqlx = { version = "0.8", features = [
|
|||
"runtime-tokio",
|
||||
] }
|
||||
tokio = { version = "1.35", features = ["full"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["full"] }
|
||||
tower-sessions = "0.12.3"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["full"] }
|
||||
uuid = { version = "1.10", features = ["serde", "v7"] }
|
||||
webauthn-rs = { version = "0.5.0", features = ["danger-allow-state-serialisation"] }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS
|
||||
public.status (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
|
@ -20,7 +20,7 @@ VALUES
|
|||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.user (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
|
@ -35,7 +35,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS user_email_uniq_idx ON public.user(email);
|
|||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.permission (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
status_id INT NOT NULL REFERENCES status(id) DEFAULT 1,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
|
@ -46,7 +46,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS permission_name_uniq_idx ON public.permission(
|
|||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.user_permission (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
permission_id INT NOT NULL REFERENCES permission(id),
|
||||
user_id INT NOT NULL REFERENCES public.user(id),
|
||||
status_id INT NOT NULL REFERENCES status(id)
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
use sqlx::prelude::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
use super::DbPool;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NewUserEntity {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
impl NewUserEntity {
|
||||
pub fn new(name: String, email: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
email,
|
||||
user_id: Uuid::now_v7(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
@ -26,40 +18,40 @@ pub struct UserEntity {
|
|||
pub id: i32,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub user_id: Uuid,
|
||||
pub status_id: i32,
|
||||
}
|
||||
|
||||
// pub async fn _insert_new_user(
|
||||
// pool: &DbPool,
|
||||
// new_user: NewUserEntity,
|
||||
// ) -> Result<UserEntity, AppError> {
|
||||
// let NewUserEntity {
|
||||
// name,
|
||||
// email,
|
||||
// user_id,
|
||||
// } = new_user;
|
||||
pub async fn insert_new_user(
|
||||
pool: &DbPool,
|
||||
new_user: NewUserEntity,
|
||||
) -> Result<UserEntity, AppError> {
|
||||
let NewUserEntity {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
name,
|
||||
} = new_user;
|
||||
|
||||
// sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;")
|
||||
// .bind(username)
|
||||
// .bind(email)
|
||||
// .bind(password)
|
||||
// .bind(name)
|
||||
// .fetch_one(pool).await
|
||||
// .map_err(|err| {
|
||||
// eprintln!("Error inserting NewUserEntity {err:?}");
|
||||
// AppError::from(err)
|
||||
// })
|
||||
// }
|
||||
sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;")
|
||||
.bind(username)
|
||||
.bind(email)
|
||||
.bind(password)
|
||||
.bind(name)
|
||||
.fetch_one(pool).await
|
||||
.map_err(|err| {
|
||||
eprintln!("Error inserting NewUserEntity {err:?}");
|
||||
AppError::from(err)
|
||||
})
|
||||
}
|
||||
|
||||
// pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
|
||||
// sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;")
|
||||
// .bind(user_id)
|
||||
// .execute(pool)
|
||||
// .await
|
||||
// .map_err(|err| {
|
||||
// eprintln!("Error verifying user with id '{user_id}'.");
|
||||
// AppError::from(err)
|
||||
// })
|
||||
// .map(|_| ())
|
||||
// }
|
||||
pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
|
||||
sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;")
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
eprintln!("Error verifying user with id '{user_id}'.");
|
||||
AppError::from(err)
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ mod models;
|
|||
mod requests;
|
||||
mod services;
|
||||
|
||||
use db::create_connection_pool;
|
||||
use db::{create_connection_pool, run_migrations};
|
||||
use requests::start_app;
|
||||
use services::{start_emailer_service, UserConfirmationMessage};
|
||||
use tokio::runtime::Handle;
|
||||
|
@ -25,10 +25,10 @@ async fn main() {
|
|||
};
|
||||
|
||||
let pool = create_connection_pool(env.db_connection_uri()).await;
|
||||
// if let Err(err) = run_migrations(&pool).await {
|
||||
// eprintln!("{err:?}");
|
||||
// process::exit(2);
|
||||
// }
|
||||
if let Err(err) = run_migrations(&pool).await {
|
||||
eprintln!("{err:?}");
|
||||
process::exit(2);
|
||||
}
|
||||
|
||||
start_emailer_service(Handle::current(), env.assets_dir(), rx);
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
mod user;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
use webauthn_rs::prelude::*;
|
||||
|
||||
use crate::{
|
||||
db::DbPool,
|
||||
|
@ -15,30 +12,11 @@ use crate::{
|
|||
pub struct AppState {
|
||||
pool: DbPool,
|
||||
env: Environment,
|
||||
webauthn: Arc<Webauthn>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(pool: DbPool, env: Environment) -> Self {
|
||||
let rp_id = env.rp_id();
|
||||
let rp_origin = env
|
||||
.domain()
|
||||
.parse::<Url>()
|
||||
.expect("RP_ORIGIN must be in a valid domain name format");
|
||||
|
||||
let webauthn = Arc::new(
|
||||
WebauthnBuilder::new(rp_id, &rp_origin)
|
||||
.map(|builder| builder.allow_any_port(true))
|
||||
.and_then(WebauthnBuilder::build)
|
||||
.inspect_err(|err| eprintln!("{err}"))
|
||||
.expect("Unable to build authenticator"),
|
||||
);
|
||||
|
||||
Self {
|
||||
pool,
|
||||
env,
|
||||
webauthn,
|
||||
}
|
||||
Self { pool, env }
|
||||
}
|
||||
|
||||
pub fn pool(&self) -> &DbPool {
|
||||
|
@ -48,10 +26,6 @@ impl AppState {
|
|||
pub fn env(&self) -> &Environment {
|
||||
&self.env
|
||||
}
|
||||
|
||||
pub fn webauthn(&self) -> Arc<Webauthn> {
|
||||
Arc::clone(&self.webauthn)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> {
|
||||
|
@ -66,7 +40,6 @@ pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> {
|
|||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.inspect(|_| println!("Application started successfully."))
|
||||
.map_err(AppError::app_startup)?;
|
||||
|
||||
Ok(())
|
||||
|
|
78
api/src/requests/user/create/handler.rs
Normal file
78
api/src/requests/user/create/handler.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
db::{insert_new_user, NewUserEntity, UserEntity},
|
||||
models::ApiResponse,
|
||||
requests::AppState,
|
||||
services::{auth_token::generate_token, hash_string, UserConfirmationMessage},
|
||||
};
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use http::StatusCode;
|
||||
|
||||
use super::models::{UserRegistrationRequest, UserRegistrationResponse};
|
||||
|
||||
static FIFTEEN_MINUTES: u64 = 60 * 15;
|
||||
|
||||
pub async fn user_registration_post_handler(
|
||||
State(app_state): State<AppState>,
|
||||
Json(request): Json<UserRegistrationRequest>,
|
||||
) -> Result<Response, Response> {
|
||||
let UserRegistrationRequest {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
name,
|
||||
} = request;
|
||||
|
||||
let hashed_password = hash_string(password);
|
||||
|
||||
let new_user = NewUserEntity {
|
||||
username,
|
||||
password: hashed_password.to_string(),
|
||||
email,
|
||||
name,
|
||||
};
|
||||
|
||||
let UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await
|
||||
.map_err(|err| {
|
||||
if err.is_duplicate_record() {
|
||||
(StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response()
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::error("An error occurred while creating your new user account. Please try again later.").into_json_response()).into_response()
|
||||
}
|
||||
})?;
|
||||
|
||||
let signing_key = app_state.env().token_key();
|
||||
let (auth_token, expiration) = generate_token(
|
||||
signing_key,
|
||||
user_id,
|
||||
Some(Duration::from_secs(FIFTEEN_MINUTES)),
|
||||
Some("user-verify.debtpirate.bikeshedengineering.internal"),
|
||||
);
|
||||
|
||||
let new_user_confirmation_message =
|
||||
UserConfirmationMessage::new(email.as_str(), name.as_str(), auth_token.as_str());
|
||||
|
||||
let _ = app_state
|
||||
.env()
|
||||
.email_sender()
|
||||
.send(new_user_confirmation_message)
|
||||
.inspect_err(|err| {
|
||||
eprintln!("Got the rollowing error while sending across the channel: {err}");
|
||||
});
|
||||
|
||||
let response = (
|
||||
StatusCode::CREATED,
|
||||
ApiResponse::new(UserRegistrationResponse {
|
||||
id: user_id,
|
||||
expiration,
|
||||
})
|
||||
.into_json_response(),
|
||||
);
|
||||
|
||||
Ok(response.into_response())
|
||||
}
|
4
api/src/requests/user/create/mod.rs
Normal file
4
api/src/requests/user/create/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod handler;
|
||||
mod models;
|
||||
|
||||
pub use handler::*;
|
5
api/src/requests/user/create/models/mod.rs
Normal file
5
api/src/requests/user/create/models/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod registration_request;
|
||||
mod registration_response;
|
||||
|
||||
pub use registration_request::*;
|
||||
pub use registration_response::*;
|
|
@ -3,6 +3,8 @@ use serde::Deserialize;
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserRegistrationRequest {
|
||||
pub name: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
}
|
13
api/src/requests/user/create/models/registration_response.rs
Normal file
13
api/src/requests/user/create/models/registration_response.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_with::serde_as;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserRegistrationResponse {
|
||||
pub id: i32,
|
||||
|
||||
#[serde(serialize_with = "humantime_serde::serialize")]
|
||||
pub expiration: SystemTime,
|
||||
}
|
|
@ -1,29 +1,14 @@
|
|||
use axum::Router;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_sessions::{
|
||||
cookie::{time::Duration, SameSite},
|
||||
Expiry, MemoryStore, SessionManagerLayer,
|
||||
};
|
||||
use axum::{routing::post, Router};
|
||||
use create::user_registration_post_handler;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub mod new_user;
|
||||
pub mod create;
|
||||
// pub mod verify;
|
||||
|
||||
pub fn requests(app_state: AppState) -> Router {
|
||||
let domain = app_state.env().domain().to_owned();
|
||||
let user_requests_middleware = ServiceBuilder::new().layer(
|
||||
SessionManagerLayer::new(MemoryStore::default())
|
||||
.with_domain(domain)
|
||||
.with_secure(false)
|
||||
.with_same_site(SameSite::Strict)
|
||||
.with_expiry(Expiry::OnInactivity(Duration::seconds(300))),
|
||||
);
|
||||
|
||||
Router::new()
|
||||
.nest(
|
||||
"/user",
|
||||
Router::new().merge(new_user::request(app_state.clone())), // .merge(verify::request(app_state.clone())),
|
||||
)
|
||||
.layer(user_requests_middleware)
|
||||
Router::new().route(
|
||||
"/user",
|
||||
post(user_registration_post_handler).with_state(app_state.clone()),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
mod models;
|
||||
mod request;
|
||||
|
||||
pub use request::*;
|
|
@ -1,3 +0,0 @@
|
|||
mod registration_request;
|
||||
|
||||
pub use registration_request::*;
|
|
@ -1,78 +0,0 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
routing::post,
|
||||
Json, Router,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use tower_sessions::Session;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::CreationChallengeResponse;
|
||||
|
||||
use crate::{
|
||||
db::NewUserEntity,
|
||||
models::ApiResponse,
|
||||
requests::AppState,
|
||||
services::{auth_token::generate_token, UserConfirmationMessage},
|
||||
};
|
||||
|
||||
use super::models::UserRegistrationRequest;
|
||||
|
||||
static FIFTEEN_MINUTES: u64 = 60 * 15;
|
||||
|
||||
pub fn request(app_state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", post(user_registration_post_handler))
|
||||
.with_state(app_state)
|
||||
}
|
||||
|
||||
async fn user_registration_post_handler(
|
||||
State(app_state): State<AppState>,
|
||||
session: Session,
|
||||
Json(request): Json<UserRegistrationRequest>,
|
||||
) -> Result<Response, Response> {
|
||||
let _ = session.remove_value("reg_state").await;
|
||||
|
||||
let UserRegistrationRequest { name, email } = request;
|
||||
let user_uuid = Uuid::now_v7();
|
||||
|
||||
// TODO: Fetch any already saved credentials for the given user for exclusion
|
||||
let (creation_challenge_response, reg) = app_state
|
||||
.webauthn()
|
||||
.start_passkey_registration(user_uuid, email.as_str(), name.as_str(), None)
|
||||
.expect("Invalid passkey registration");
|
||||
|
||||
// let new_user = NewUserEntity::new(email.clone(), name.clone());
|
||||
// let UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await
|
||||
// .map_err(|err| {
|
||||
// if err.is_duplicate_record() {
|
||||
// (StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response()
|
||||
// } else {
|
||||
// (StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::error("An error occurred while creating your new user account. Please try again later.").into_json_response()).into_response()
|
||||
// }
|
||||
// })?;
|
||||
|
||||
// let new_user_confirmation_message =
|
||||
// UserConfirmationMessage::new(email.as_str(), name.as_str(), auth_token.as_str());
|
||||
|
||||
// let _ = app_state
|
||||
// .env()
|
||||
// .email_sender()
|
||||
// .send(new_user_confirmation_message)
|
||||
// .inspect_err(|err| {
|
||||
// eprintln!("Got the rollowing error while sending across the channel: {err}");
|
||||
// });
|
||||
|
||||
// let new_user_entity = NewUserEntity::new(name, email);
|
||||
|
||||
let response = (
|
||||
StatusCode::OK,
|
||||
Json(ApiResponse::<CreationChallengeResponse>::new(
|
||||
creation_challenge_response,
|
||||
)),
|
||||
);
|
||||
|
||||
Ok(response.into_response())
|
||||
}
|
|
@ -78,7 +78,11 @@ pub fn generate_token(
|
|||
.token_identifier(Uuid::now_v7().to_string().as_str())
|
||||
.map(|_| claims)
|
||||
})
|
||||
.and_then(|mut claims| claims.issuer("auth-test").map(|_| claims))
|
||||
.and_then(|mut claims| {
|
||||
claims
|
||||
.issuer("debtpirate.bikeshedengineering.internal")
|
||||
.map(|_| claims)
|
||||
})
|
||||
.and_then(|mut claims| claims.subject(user_id.to_string().as_str()).map(|_| claims))
|
||||
.and_then(|mut claims| {
|
||||
if let Some(audience) = audience {
|
||||
|
|
|
@ -3,7 +3,7 @@ use argon2::{
|
|||
Argon2,
|
||||
};
|
||||
|
||||
pub fn hash_string(string: &str) -> PasswordHashString {
|
||||
pub fn hash_string(string: String) -> PasswordHashString {
|
||||
let algorithm = Argon2::default();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue