feat(opaque): add change password
This commit is contained in:
@@ -468,41 +468,41 @@ mod integration_tests {
|
||||
#[tokio::test]
|
||||
async fn mark_migrated_stamps_once_and_is_idempotent() {
|
||||
let repo = test_repo().await;
|
||||
let user =
|
||||
seed_user(&repo, &format!("opaque-mig-{}@example.invalid", Uuid::new_v4())).await;
|
||||
let user = seed_user(
|
||||
&repo,
|
||||
&format!("opaque-mig-{}@example.invalid", Uuid::new_v4()),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Read the initial NULL state
|
||||
let initial: (Option<chrono::DateTime<chrono::Utc>>,) = sqlx::query_as(
|
||||
"SELECT opaque_migrated_at FROM auth.users WHERE id = $1",
|
||||
)
|
||||
.bind(user)
|
||||
.fetch_one(repo.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
let initial: (Option<chrono::DateTime<chrono::Utc>>,) =
|
||||
sqlx::query_as("SELECT opaque_migrated_at FROM auth.users WHERE id = $1")
|
||||
.bind(user)
|
||||
.fetch_one(repo.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
initial.0.is_none(),
|
||||
"new user starts with no opaque_migrated_at"
|
||||
);
|
||||
|
||||
repo.mark_migrated(user).await.expect("first mark");
|
||||
let first: (Option<chrono::DateTime<chrono::Utc>>,) = sqlx::query_as(
|
||||
"SELECT opaque_migrated_at FROM auth.users WHERE id = $1",
|
||||
)
|
||||
.bind(user)
|
||||
.fetch_one(repo.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
let first: (Option<chrono::DateTime<chrono::Utc>>,) =
|
||||
sqlx::query_as("SELECT opaque_migrated_at FROM auth.users WHERE id = $1")
|
||||
.bind(user)
|
||||
.fetch_one(repo.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
let first_ts = first.0.expect("timestamp set after first mark");
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
repo.mark_migrated(user).await.expect("second mark");
|
||||
let second: (Option<chrono::DateTime<chrono::Utc>>,) = sqlx::query_as(
|
||||
"SELECT opaque_migrated_at FROM auth.users WHERE id = $1",
|
||||
)
|
||||
.bind(user)
|
||||
.fetch_one(repo.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
let second: (Option<chrono::DateTime<chrono::Utc>>,) =
|
||||
sqlx::query_as("SELECT opaque_migrated_at FROM auth.users WHERE id = $1")
|
||||
.bind(user)
|
||||
.fetch_one(repo.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
second.0.unwrap(),
|
||||
first_ts,
|
||||
|
||||
@@ -256,7 +256,10 @@ mod tests {
|
||||
let id = cache.store(state, Some(Uuid::new_v4()));
|
||||
// Second call after take must miss — single-use semantic.
|
||||
let taken = cache.take(id).expect("first take retrieves the state");
|
||||
assert!(taken.user_id.is_some(), "user_id round-trips through the stash");
|
||||
assert!(
|
||||
taken.user_id.is_some(),
|
||||
"user_id round-trips through the stash"
|
||||
);
|
||||
assert!(
|
||||
cache.take(id).is_none(),
|
||||
"second take must miss — exchange_id is single-use"
|
||||
@@ -306,6 +309,9 @@ mod tests {
|
||||
let cache = OpaqueLoginExchange::new();
|
||||
let id = cache.store(build_server_login_state(), None);
|
||||
let taken = cache.take(id).expect("take dummy stash");
|
||||
assert!(taken.user_id.is_none(), "dummy-branch stash carries no user_id");
|
||||
assert!(
|
||||
taken.user_id.is_none(),
|
||||
"dummy-branch stash carries no user_id"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::user_dto::AuthResponseDto;
|
||||
use crate::common::di::AppState;
|
||||
use crate::infrastructure::services::opaque_login_exchange::{
|
||||
ExchangeId, OpaqueLoginExchange,
|
||||
};
|
||||
use crate::infrastructure::services::opaque_login_exchange::{ExchangeId, OpaqueLoginExchange};
|
||||
use crate::infrastructure::services::opaque_service::{OpaqueService, OxiCloudSuite};
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUserId;
|
||||
@@ -381,9 +379,9 @@ pub async fn login_ke1(
|
||||
let repo = require_opaque_repo(&state)?;
|
||||
let exchange = require_opaque_exchange(&state)?;
|
||||
|
||||
let cred_bytes = B64.decode(dto.start_login_request.trim()).map_err(|_| {
|
||||
malformed("startLoginRequest is not valid base64")
|
||||
})?;
|
||||
let cred_bytes = B64
|
||||
.decode(dto.start_login_request.trim())
|
||||
.map_err(|_| malformed("startLoginRequest is not valid base64"))?;
|
||||
let cred_request = CredentialRequest::<OxiCloudSuite>::deserialize(&cred_bytes)
|
||||
.map_err(|_| malformed("startLoginRequest failed to deserialize"))?;
|
||||
|
||||
@@ -513,9 +511,9 @@ pub async fn login_ke3(
|
||||
invalid_credentials()
|
||||
})?;
|
||||
|
||||
let cred_bytes = B64.decode(dto.finish_login_request.trim()).map_err(|_| {
|
||||
malformed("finishLoginRequest is not valid base64")
|
||||
})?;
|
||||
let cred_bytes = B64
|
||||
.decode(dto.finish_login_request.trim())
|
||||
.map_err(|_| malformed("finishLoginRequest is not valid base64"))?;
|
||||
let cred_final = CredentialFinalization::<OxiCloudSuite>::deserialize(&cred_bytes)
|
||||
.map_err(|_| malformed("finishLoginRequest failed to deserialize"))?;
|
||||
|
||||
@@ -552,22 +550,19 @@ pub async fn login_ke3(
|
||||
// Fetch the user entity — needed by mint_session_for_authenticated_user
|
||||
// (it calls dispatch_login + register_login + generates tokens
|
||||
// from the user's role/email/etc.).
|
||||
let user = auth
|
||||
.get_user_entity(user_id)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
// User row vanished between KE1's envelope fetch and now
|
||||
// (delete race). Same shape as bad passphrase — never
|
||||
// leak "you passed the crypto but the account is gone."
|
||||
tracing::warn!(
|
||||
target: "audit",
|
||||
event = "opaque.login_ke3_rejected",
|
||||
reason = "user_gone_after_ke3",
|
||||
user_id = %user_id,
|
||||
"👮🏻♂️ OPAQUE KE3: user disappeared between KE1 and KE3"
|
||||
);
|
||||
invalid_credentials()
|
||||
})?;
|
||||
let user = auth.get_user_entity(user_id).await.map_err(|_| {
|
||||
// User row vanished between KE1's envelope fetch and now
|
||||
// (delete race). Same shape as bad passphrase — never
|
||||
// leak "you passed the crypto but the account is gone."
|
||||
tracing::warn!(
|
||||
target: "audit",
|
||||
event = "opaque.login_ke3_rejected",
|
||||
reason = "user_gone_after_ke3",
|
||||
user_id = %user_id,
|
||||
"👮🏻♂️ OPAQUE KE3: user disappeared between KE1 and KE3"
|
||||
);
|
||||
invalid_credentials()
|
||||
})?;
|
||||
|
||||
// Mint the session BEFORE stamping opaque_migrated_at — if the
|
||||
// session mint fails (rare, but not impossible under DB failure),
|
||||
@@ -653,9 +648,7 @@ pub struct OpaqueParamsResponse {
|
||||
),
|
||||
tag = "auth"
|
||||
)]
|
||||
pub async fn opaque_params(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn opaque_params(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
// Reads from OpaqueService when substrate is wired; falls back
|
||||
// to the OpaqueConfig defaults otherwise so an
|
||||
// `enabled=false` payload still has plausible-shape numeric
|
||||
|
||||
Reference in New Issue
Block a user