no-password
This commit is contained in:
+63
-79
@@ -6,15 +6,20 @@ use std::time::Duration;
|
||||
use anyhow::{anyhow, ensure, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Method, Request, StatusCode};
|
||||
use axum::http::{header, Method, Request};
|
||||
use axum::Router;
|
||||
use backend::auth::jwt::JwtService;
|
||||
use backend::config::AppConfig;
|
||||
use backend::db::{self, PgPool};
|
||||
use backend::models::{Job, NewUser, NewUserMembership, NewUserPasskey, Tenant, TenantStatus};
|
||||
use backend::models::{
|
||||
Job, NewRefreshToken, NewUser, NewUserMembership, NewUserPasskey, Tenant, TenantStatus, User,
|
||||
UserMembership,
|
||||
};
|
||||
use backend::routes;
|
||||
use backend::schema::refresh_tokens::dsl as refresh_dsl;
|
||||
use backend::state::AppState;
|
||||
use backend::storage::ObjectStorage;
|
||||
use chrono::{Duration as ChronoDuration, Utc};
|
||||
use diesel::connection::SimpleConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel::OptionalExtension;
|
||||
@@ -23,8 +28,10 @@ use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use http_body_util::BodyExt;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{self, json};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::sync::Mutex;
|
||||
use tower::util::ServiceExt;
|
||||
use uuid::Uuid;
|
||||
@@ -205,16 +212,13 @@ impl TestApp {
|
||||
Ok(format!("{}{}", root, key))
|
||||
}
|
||||
|
||||
pub async fn insert_user(&self, username: &str, password: &str, _role: &str) -> Result<Uuid> {
|
||||
pub async fn insert_user(&self, username: &str, _password: &str, _role: &str) -> Result<Uuid> {
|
||||
let username = username.to_string();
|
||||
let password = password.to_string();
|
||||
let tenant_id = self.ensure_default_tenant().await?;
|
||||
self.with_conn(move |conn| {
|
||||
let password_hash = hash_password(&password)?;
|
||||
let user = NewUser {
|
||||
id: Uuid::new_v4(),
|
||||
username,
|
||||
password_hash,
|
||||
};
|
||||
diesel::insert_into(backend::schema::users::table)
|
||||
.values(&user)
|
||||
@@ -319,80 +323,59 @@ impl TestApp {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn login_token(&self, username: &str, password: &str) -> Result<String> {
|
||||
#[derive(Serialize)]
|
||||
struct LoginPayload<'a> {
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
}
|
||||
pub async fn login_token(&self, username: &str, _password: &str) -> Result<String> {
|
||||
let (access_token, _, _) = self.create_session(username).await?;
|
||||
Ok(access_token)
|
||||
}
|
||||
|
||||
let response = self
|
||||
.post_json(
|
||||
"/api/auth/login",
|
||||
&LoginPayload { username, password },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
pub async fn create_session(&self, username: &str) -> Result<(String, String, Uuid)> {
|
||||
let username = username.to_string();
|
||||
let state = self.state.clone();
|
||||
self.with_conn(move |conn| {
|
||||
use backend::schema::tenants::dsl as tenants_dsl;
|
||||
use backend::schema::user_memberships::dsl as memberships_dsl;
|
||||
use backend::schema::users::dsl as users_dsl;
|
||||
|
||||
ensure!(
|
||||
response.status() == StatusCode::OK,
|
||||
"login failed with status {}",
|
||||
response.status()
|
||||
);
|
||||
let user: User = users_dsl::users
|
||||
.filter(users_dsl::username.eq(&username))
|
||||
.first(conn)?;
|
||||
|
||||
let body = body_to_vec(response.into_body()).await?;
|
||||
#[derive(Deserialize)]
|
||||
struct LoginResponse {
|
||||
access_token: String,
|
||||
}
|
||||
let membership: UserMembership = memberships_dsl::user_memberships
|
||||
.filter(memberships_dsl::user_id.eq(user.id))
|
||||
.first(conn)?;
|
||||
|
||||
if let Ok(parsed) = serde_json::from_slice::<LoginResponse>(&body) {
|
||||
return Ok(parsed.access_token);
|
||||
}
|
||||
let tenant: Tenant = tenants_dsl::tenants
|
||||
.find(membership.tenant_id)
|
||||
.first(conn)?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TenantSummary {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
}
|
||||
let now = Utc::now();
|
||||
let access_token = state
|
||||
.jwt
|
||||
.generate_token(user.id, tenant.id, &user.username)
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TenantSelectionResponse {
|
||||
access_token: String,
|
||||
tenants: Vec<TenantSummary>,
|
||||
}
|
||||
let refresh_value = generate_refresh_token();
|
||||
let refresh_hash = hash_refresh_token(&refresh_value);
|
||||
let refresh_expires_at =
|
||||
now + ChronoDuration::days(state.config.refresh_token_expiry_days);
|
||||
|
||||
let selection: TenantSelectionResponse = serde_json::from_slice(&body)?;
|
||||
ensure!(
|
||||
!selection.tenants.is_empty(),
|
||||
"login returned no tenant options",
|
||||
);
|
||||
let new_refresh = NewRefreshToken {
|
||||
id: Uuid::new_v4(),
|
||||
user_id: user.id,
|
||||
token_hash: refresh_hash,
|
||||
issued_at: now.naive_utc(),
|
||||
expires_at: refresh_expires_at.naive_utc(),
|
||||
tenant_id: tenant.id,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SelectTenantPayload {
|
||||
tenant_id: Uuid,
|
||||
}
|
||||
diesel::insert_into(refresh_dsl::refresh_tokens)
|
||||
.values(&new_refresh)
|
||||
.execute(conn)?;
|
||||
|
||||
let target_tenant = selection.tenants[0].id;
|
||||
let select_response = self
|
||||
.post_json(
|
||||
"/api/auth/select-tenant",
|
||||
&SelectTenantPayload {
|
||||
tenant_id: target_tenant,
|
||||
},
|
||||
Some(&selection.access_token),
|
||||
)
|
||||
.await?;
|
||||
|
||||
ensure!(
|
||||
select_response.status() == StatusCode::OK,
|
||||
"tenant selection failed with status {}",
|
||||
select_response.status()
|
||||
);
|
||||
|
||||
let select_body = body_to_vec(select_response.into_body()).await?;
|
||||
let parsed: LoginResponse = serde_json::from_slice(&select_body)?;
|
||||
Ok(parsed.access_token)
|
||||
let cookie = format!("refresh_token={refresh_value}");
|
||||
Ok((access_token, cookie, tenant.id))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -747,13 +730,14 @@ fn truncate_all(conn: &mut PgConnection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String> {
|
||||
use argon2::password_hash::{PasswordHasher, SaltString};
|
||||
use argon2::Argon2;
|
||||
fn generate_refresh_token() -> String {
|
||||
let mut bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Ok(Argon2::default()
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map_err(|err| anyhow!("failed to hash password: {err}"))?
|
||||
.to_string())
|
||||
fn hash_refresh_token(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user