You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Make the compat login use the new password manager
This commit is contained in:
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -2684,7 +2684,6 @@ name = "mas-cli"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
|
||||||
"atty",
|
"atty",
|
||||||
"axum",
|
"axum",
|
||||||
"camino",
|
"camino",
|
||||||
@ -3101,21 +3100,16 @@ dependencies = [
|
|||||||
name = "mas-storage"
|
name = "mas-storage"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"argon2",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"mas-data-model",
|
"mas-data-model",
|
||||||
"mas-iana",
|
"mas-iana",
|
||||||
"mas-jose",
|
"mas-jose",
|
||||||
"oauth2-types",
|
"oauth2-types",
|
||||||
"password-hash",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rand_chacha 0.3.1",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"ulid",
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
|
@ -7,7 +7,6 @@ license = "Apache-2.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.66"
|
||||||
argon2 = { version = "0.4.1", features = ["password-hash"] }
|
|
||||||
atty = "0.2.14"
|
atty = "0.2.14"
|
||||||
axum = "0.6.1"
|
axum = "0.6.1"
|
||||||
camino = "1.1.1"
|
camino = "1.1.1"
|
||||||
|
@ -18,18 +18,20 @@ use hyper::StatusCode;
|
|||||||
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType};
|
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType};
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
compat::{
|
compat::{
|
||||||
add_compat_access_token, add_compat_refresh_token, compat_login,
|
add_compat_access_token, add_compat_refresh_token, get_compat_sso_login_by_token,
|
||||||
get_compat_sso_login_by_token, mark_compat_sso_login_as_exchanged,
|
mark_compat_sso_login_as_exchanged, start_compat_session,
|
||||||
},
|
},
|
||||||
|
user::{add_user_password, lookup_user_by_username, lookup_user_password},
|
||||||
Clock,
|
Clock,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, skip_serializing_none, DurationMilliSeconds};
|
use serde_with::{serde_as, skip_serializing_none, DurationMilliSeconds};
|
||||||
use sqlx::{PgPool, Postgres, Transaction};
|
use sqlx::{PgPool, Postgres, Transaction};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use super::{MatrixError, MatrixHomeserver};
|
use super::{MatrixError, MatrixHomeserver};
|
||||||
use crate::impl_from_error_for_route;
|
use crate::{impl_from_error_for_route, passwords::PasswordManager};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@ -132,8 +134,14 @@ pub enum RouteError {
|
|||||||
#[error("unsupported login method")]
|
#[error("unsupported login method")]
|
||||||
Unsupported,
|
Unsupported,
|
||||||
|
|
||||||
#[error("login failed")]
|
#[error("user not found")]
|
||||||
LoginFailed,
|
UserNotFound,
|
||||||
|
|
||||||
|
#[error("user has no password")]
|
||||||
|
NoPassword,
|
||||||
|
|
||||||
|
#[error("password verification failed")]
|
||||||
|
PasswordVerificationFailed(#[source] anyhow::Error),
|
||||||
|
|
||||||
#[error("login took too long")]
|
#[error("login took too long")]
|
||||||
LoginTookTooLong,
|
LoginTookTooLong,
|
||||||
@ -158,11 +166,13 @@ impl IntoResponse for RouteError {
|
|||||||
error: "Invalid login type",
|
error: "Invalid login type",
|
||||||
status: StatusCode::BAD_REQUEST,
|
status: StatusCode::BAD_REQUEST,
|
||||||
},
|
},
|
||||||
Self::LoginFailed => MatrixError {
|
Self::UserNotFound | Self::NoPassword | Self::PasswordVerificationFailed(_) => {
|
||||||
errcode: "M_UNAUTHORIZED",
|
MatrixError {
|
||||||
error: "Invalid username/password",
|
errcode: "M_UNAUTHORIZED",
|
||||||
status: StatusCode::FORBIDDEN,
|
error: "Invalid username/password",
|
||||||
},
|
status: StatusCode::FORBIDDEN,
|
||||||
|
}
|
||||||
|
}
|
||||||
Self::LoginTookTooLong => MatrixError {
|
Self::LoginTookTooLong => MatrixError {
|
||||||
errcode: "M_UNAUTHORIZED",
|
errcode: "M_UNAUTHORIZED",
|
||||||
error: "Login token expired",
|
error: "Login token expired",
|
||||||
@ -180,6 +190,7 @@ impl IntoResponse for RouteError {
|
|||||||
|
|
||||||
#[tracing::instrument(skip_all, err)]
|
#[tracing::instrument(skip_all, err)]
|
||||||
pub(crate) async fn post(
|
pub(crate) async fn post(
|
||||||
|
State(password_manager): State<PasswordManager>,
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
State(homeserver): State<MatrixHomeserver>,
|
State(homeserver): State<MatrixHomeserver>,
|
||||||
Json(input): Json<RequestBody>,
|
Json(input): Json<RequestBody>,
|
||||||
@ -190,7 +201,7 @@ pub(crate) async fn post(
|
|||||||
Credentials::Password {
|
Credentials::Password {
|
||||||
identifier: Identifier::User { user },
|
identifier: Identifier::User { user },
|
||||||
password,
|
password,
|
||||||
} => user_password_login(&mut txn, user, password).await?,
|
} => user_password_login(&password_manager, &mut txn, user, password).await?,
|
||||||
|
|
||||||
Credentials::Token { token } => token_login(&mut txn, &clock, &token).await?,
|
Credentials::Token { token } => token_login(&mut txn, &clock, &token).await?,
|
||||||
|
|
||||||
@ -295,16 +306,53 @@ async fn token_login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn user_password_login(
|
async fn user_password_login(
|
||||||
|
password_manager: &PasswordManager,
|
||||||
txn: &mut Transaction<'_, Postgres>,
|
txn: &mut Transaction<'_, Postgres>,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<CompatSession, RouteError> {
|
) -> Result<CompatSession, RouteError> {
|
||||||
let (clock, mut rng) = crate::clock_and_rng();
|
let (clock, mut rng) = crate::clock_and_rng();
|
||||||
|
|
||||||
let device = Device::generate(&mut rng);
|
// Find the user
|
||||||
let session = compat_login(txn, &mut rng, &clock, &username, &password, device)
|
let user = lookup_user_by_username(&mut *txn, &username)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::UserNotFound)?;
|
||||||
|
|
||||||
|
// Lookup its password
|
||||||
|
let user_password = lookup_user_password(&mut *txn, &user)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::NoPassword)?;
|
||||||
|
|
||||||
|
// Verify the password
|
||||||
|
let password = Zeroizing::new(password.into_bytes());
|
||||||
|
|
||||||
|
let new_password_hash = password_manager
|
||||||
|
.verify_and_upgrade(
|
||||||
|
&mut rng,
|
||||||
|
user_password.version,
|
||||||
|
password,
|
||||||
|
user_password.hashed_password.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| RouteError::LoginFailed)?;
|
.map_err(RouteError::PasswordVerificationFailed)?;
|
||||||
|
|
||||||
|
if let Some((version, hashed_password)) = new_password_hash {
|
||||||
|
// Save the upgraded password if needed
|
||||||
|
add_user_password(
|
||||||
|
&mut *txn,
|
||||||
|
&mut rng,
|
||||||
|
&clock,
|
||||||
|
&user,
|
||||||
|
version,
|
||||||
|
hashed_password,
|
||||||
|
Some(user_password),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the user credentials have been verified, start a new compat session
|
||||||
|
let device = Device::generate(&mut rng);
|
||||||
|
let session = start_compat_session(&mut *txn, &mut rng, &clock, user, device).await?;
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
@ -208,6 +208,7 @@ where
|
|||||||
UrlBuilder: FromRef<S>,
|
UrlBuilder: FromRef<S>,
|
||||||
PgPool: FromRef<S>,
|
PgPool: FromRef<S>,
|
||||||
MatrixHomeserver: FromRef<S>,
|
MatrixHomeserver: FromRef<S>,
|
||||||
|
PasswordManager: FromRef<S>,
|
||||||
{
|
{
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route(
|
||||||
|
@ -6,20 +6,15 @@ edition = "2021"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = "1.23.0"
|
|
||||||
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline", "json", "uuid"] }
|
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline", "json", "uuid"] }
|
||||||
chrono = { version = "0.4.23", features = ["serde"] }
|
chrono = { version = "0.4.23", features = ["serde"] }
|
||||||
serde = { version = "1.0.149", features = ["derive"] }
|
serde = { version = "1.0.149", features = ["derive"] }
|
||||||
serde_json = "1.0.89"
|
serde_json = "1.0.89"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
anyhow = "1.0.66"
|
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
|
|
||||||
# Password hashing
|
# Password hashing
|
||||||
argon2 = { version = "0.4.1", features = ["password-hash"] }
|
|
||||||
password-hash = { version = "0.4.2", features = ["std"] }
|
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rand_chacha = "0.3.1"
|
|
||||||
url = { version = "2.3.1", features = ["serde"] }
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
uuid = "1.2.2"
|
uuid = "1.2.2"
|
||||||
ulid = { version = "1.0.0", features = ["uuid", "serde"] }
|
ulid = { version = "1.0.0", features = ["uuid", "serde"] }
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use argon2::{Argon2, PasswordHash};
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
CompatAccessToken, CompatRefreshToken, CompatSession, CompatSsoLogin, CompatSsoLoginState,
|
CompatAccessToken, CompatRefreshToken, CompatSession, CompatSsoLogin, CompatSsoLoginState,
|
||||||
@ -21,7 +19,6 @@ use mas_data_model::{
|
|||||||
};
|
};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sqlx::{Acquire, PgExecutor, Postgres, QueryBuilder};
|
use sqlx::{Acquire, PgExecutor, Postgres, QueryBuilder};
|
||||||
use tokio::task;
|
|
||||||
use tracing::{info_span, Instrument};
|
use tracing::{info_span, Instrument};
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@ -29,7 +26,6 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
pagination::{process_page, QueryBuilderExt},
|
pagination::{process_page, QueryBuilderExt},
|
||||||
user::lookup_user_by_username,
|
|
||||||
Clock, DatabaseError, DatabaseInconsistencyError, LookupResultExt,
|
Clock, DatabaseError, DatabaseInconsistencyError, LookupResultExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -284,91 +280,6 @@ pub async fn lookup_active_compat_refresh_token(
|
|||||||
Ok(Some((refresh_token, access_token, session)))
|
Ok(Some((refresh_token, access_token, session)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
|
||||||
skip_all,
|
|
||||||
fields(
|
|
||||||
user.username = username,
|
|
||||||
user.id,
|
|
||||||
compat_session.id,
|
|
||||||
compat_session.device.id = device.as_str(),
|
|
||||||
),
|
|
||||||
err(Debug),
|
|
||||||
)]
|
|
||||||
pub async fn compat_login(
|
|
||||||
conn: impl Acquire<'_, Database = Postgres> + Send,
|
|
||||||
mut rng: impl Rng + Send,
|
|
||||||
clock: &Clock,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
device: Device,
|
|
||||||
) -> Result<CompatSession, anyhow::Error> {
|
|
||||||
// TODO: that should be split and not verify the password hash here
|
|
||||||
let mut txn = conn.begin().await.context("could not start transaction")?;
|
|
||||||
|
|
||||||
// First, lookup the user
|
|
||||||
let user = lookup_user_by_username(&mut txn, username)
|
|
||||||
.await?
|
|
||||||
.context("Could not lookup username")?;
|
|
||||||
tracing::Span::current().record("user.id", tracing::field::display(user.id));
|
|
||||||
|
|
||||||
// Now, fetch the hashed password from the user associated with that session
|
|
||||||
let hashed_password: String = sqlx::query_scalar!(
|
|
||||||
r#"
|
|
||||||
SELECT up.hashed_password
|
|
||||||
FROM user_passwords up
|
|
||||||
WHERE up.user_id = $1
|
|
||||||
ORDER BY up.created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
"#,
|
|
||||||
Uuid::from(user.id),
|
|
||||||
)
|
|
||||||
.fetch_one(&mut txn)
|
|
||||||
.instrument(tracing::info_span!("Lookup hashed password"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: pass verifiers list as parameter
|
|
||||||
// Verify the password in a blocking thread to avoid blocking the async executor
|
|
||||||
let password = password.to_owned();
|
|
||||||
task::spawn_blocking(move || {
|
|
||||||
let context = Argon2::default();
|
|
||||||
let hasher = PasswordHash::new(&hashed_password)?;
|
|
||||||
hasher.verify_password(&[&context], &password)
|
|
||||||
})
|
|
||||||
.instrument(tracing::info_span!("Verify hashed password"))
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let created_at = clock.now();
|
|
||||||
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
|
|
||||||
tracing::Span::current().record("compat_session.id", tracing::field::display(id));
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO compat_sessions
|
|
||||||
(compat_session_id, user_id, device_id, created_at)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
"#,
|
|
||||||
Uuid::from(id),
|
|
||||||
Uuid::from(user.id),
|
|
||||||
device.as_str(),
|
|
||||||
created_at,
|
|
||||||
)
|
|
||||||
.execute(&mut txn)
|
|
||||||
.instrument(tracing::info_span!("Insert compat session"))
|
|
||||||
.await
|
|
||||||
.context("could not insert compat session")?;
|
|
||||||
|
|
||||||
let session = CompatSession {
|
|
||||||
id,
|
|
||||||
user,
|
|
||||||
device,
|
|
||||||
created_at,
|
|
||||||
finished_at: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
txn.commit().await.context("could not commit transaction")?;
|
|
||||||
Ok(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(
|
fields(
|
||||||
@ -895,6 +806,48 @@ pub async fn get_compat_sso_login_by_token(
|
|||||||
Ok(Some(res.try_into()?))
|
Ok(Some(res.try_into()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
%user.id,
|
||||||
|
compat_session.id,
|
||||||
|
compat_session.device.id = device.as_str(),
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)]
|
||||||
|
pub async fn start_compat_session(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
mut rng: impl Rng + Send,
|
||||||
|
clock: &Clock,
|
||||||
|
user: User,
|
||||||
|
device: Device,
|
||||||
|
) -> Result<CompatSession, DatabaseError> {
|
||||||
|
let created_at = clock.now();
|
||||||
|
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
|
||||||
|
tracing::Span::current().record("compat_session.id", tracing::field::display(id));
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
"#,
|
||||||
|
Uuid::from(id),
|
||||||
|
Uuid::from(user.id),
|
||||||
|
device.as_str(),
|
||||||
|
created_at,
|
||||||
|
)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(CompatSession {
|
||||||
|
id,
|
||||||
|
user,
|
||||||
|
device,
|
||||||
|
created_at,
|
||||||
|
finished_at: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(
|
fields(
|
||||||
@ -920,31 +873,7 @@ pub async fn fullfill_compat_sso_login(
|
|||||||
|
|
||||||
let mut txn = conn.begin().await?;
|
let mut txn = conn.begin().await?;
|
||||||
|
|
||||||
let created_at = clock.now();
|
let session = start_compat_session(&mut txn, &mut rng, clock, user, device).await?;
|
||||||
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
|
|
||||||
tracing::Span::current().record("compat_session.id", tracing::field::display(id));
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
"#,
|
|
||||||
Uuid::from(id),
|
|
||||||
Uuid::from(user.id),
|
|
||||||
device.as_str(),
|
|
||||||
created_at,
|
|
||||||
)
|
|
||||||
.execute(&mut txn)
|
|
||||||
.instrument(tracing::info_span!("Insert compat session"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let session = CompatSession {
|
|
||||||
id,
|
|
||||||
user,
|
|
||||||
device,
|
|
||||||
created_at,
|
|
||||||
finished_at: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let fulfilled_at = clock.now();
|
let fulfilled_at = clock.now();
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
|
Reference in New Issue
Block a user