1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

Use the new password manager

This commit is contained in:
Quentin Gliech
2022-12-14 15:28:36 +01:00
parent ff2f009b0e
commit 533cabe005
19 changed files with 768 additions and 427 deletions

View File

@ -0,0 +1,105 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use mas_data_model::{Authentication, BrowserSession, Password, UpstreamOAuthLink};
use rand::Rng;
use sqlx::PgExecutor;
use ulid::Ulid;
use uuid::Uuid;
use crate::Clock;
#[tracing::instrument(
skip_all,
fields(
user.id = %user_session.user.id,
%user_password.id,
%user_session.id,
user_session_authentication.id,
),
err,
)]
pub async fn authenticate_session_with_password(
executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send,
clock: &Clock,
user_session: &mut BrowserSession,
user_password: &Password,
) -> Result<(), sqlx::Error> {
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record(
"user_session_authentication.id",
tracing::field::display(id),
);
sqlx::query!(
r#"
INSERT INTO user_session_authentications
(user_session_authentication_id, user_session_id, created_at)
VALUES ($1, $2, $3)
"#,
Uuid::from(id),
Uuid::from(user_session.id),
created_at,
)
.execute(executor)
.await?;
user_session.last_authentication = Some(Authentication { id, created_at });
Ok(())
}
#[tracing::instrument(
skip_all,
fields(
user.id = %user_session.user.id,
%upstream_oauth_link.id,
%user_session.id,
user_session_authentication.id,
),
err,
)]
pub async fn authenticate_session_with_upstream(
executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send,
clock: &Clock,
user_session: &mut BrowserSession,
upstream_oauth_link: &UpstreamOAuthLink,
) -> Result<(), sqlx::Error> {
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record(
"user_session_authentication.id",
tracing::field::display(id),
);
sqlx::query!(
r#"
INSERT INTO user_session_authentications
(user_session_authentication_id, user_session_id, created_at)
VALUES ($1, $2, $3)
"#,
Uuid::from(id),
Uuid::from(user_session.id),
created_at,
)
.execute(executor)
.await?;
user_session.last_authentication = Some(Authentication { id, created_at });
Ok(())
}

View File

@ -12,20 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::borrow::BorrowMut;
use anyhow::Context;
use argon2::Argon2;
use chrono::{DateTime, Utc};
use mas_data_model::{
Authentication, BrowserSession, UpstreamOAuthLink, User, UserEmail, UserEmailVerification,
Authentication, BrowserSession, User, UserEmail, UserEmailVerification,
UserEmailVerificationState,
};
use password_hash::{PasswordHash, PasswordHasher, SaltString};
use rand::{CryptoRng, Rng};
use sqlx::{Acquire, PgExecutor, Postgres, QueryBuilder, Transaction};
use thiserror::Error;
use tokio::task;
use rand::Rng;
use sqlx::{PgExecutor, QueryBuilder};
use tracing::{info_span, Instrument};
use ulid::Ulid;
use uuid::Uuid;
@ -35,6 +28,14 @@ use crate::{
Clock, DatabaseError, DatabaseInconsistencyError, LookupResultExt,
};
mod authentication;
mod password;
pub use self::{
authentication::{authenticate_session_with_password, authenticate_session_with_upstream},
password::{add_user_password, lookup_user_password},
};
#[derive(Debug, Clone)]
struct UserLookup {
user_id: Uuid,
@ -45,64 +46,6 @@ struct UserLookup {
user_email_confirmed_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Error)]
pub enum LoginError {
#[error("could not find user {username:?}")]
NotFound { username: String },
#[error("authentication failed for {username:?}")]
Authentication {
username: String,
#[source]
source: AuthenticationError,
},
#[error("failed to login")]
Other(#[from] anyhow::Error),
}
#[tracing::instrument(
skip_all,
fields(user.username = username),
err,
)]
pub async fn login(
conn: impl Acquire<'_, Database = Postgres> + Send,
mut rng: impl Rng + Send,
clock: &Clock,
username: &str,
password: &str,
) -> Result<BrowserSession, LoginError> {
let mut txn = conn.begin().await.context("could not start transaction")?;
let user = lookup_user_by_username(&mut txn, username)
.await
.context("Could not find user by username")?;
let Some(user) = user else {
return Err(LoginError::NotFound { username: username.to_owned() });
};
let mut session = start_session(&mut txn, &mut rng, clock, user)
.await
.context("Could not start session")?;
authenticate_session(&mut txn, &mut rng, clock, &mut session, password)
.await
.map_err(|source| {
if matches!(source, AuthenticationError::Password { .. }) {
LoginError::Authentication {
username: username.to_owned(),
source,
}
} else {
LoginError::Other(source.into())
}
})?;
txn.commit().await.context("could not commit transaction")?;
Ok(session)
}
#[derive(sqlx::FromRow)]
struct SessionLookup {
user_session_id: Uuid,
@ -336,183 +279,6 @@ pub async fn count_active_sessions(
Ok(res)
}
#[derive(Debug, Error)]
pub enum AuthenticationError {
#[error("could not verify password")]
Password(#[from] password_hash::Error),
#[error("could not fetch user password hash")]
Fetch(sqlx::Error),
#[error("could not save session auth")]
Save(sqlx::Error),
#[error("runtime error")]
Internal(#[from] tokio::task::JoinError),
}
#[tracing::instrument(
skip_all,
fields(
user.id = %user_session.user.id,
%user_session.id,
user_session_authentication.id,
),
err,
)]
pub async fn authenticate_session(
txn: &mut Transaction<'_, Postgres>,
mut rng: impl Rng + Send,
clock: &Clock,
user_session: &mut BrowserSession,
password: &str,
) -> Result<(), AuthenticationError> {
// First, 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_session.user.id),
)
.fetch_one(txn.borrow_mut())
.instrument(tracing::info_span!("Lookup hashed password"))
.await
.map_err(AuthenticationError::Fetch)?;
// 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).map_err(AuthenticationError::Password)?;
hasher
.verify_password(&[&context], &password)
.map_err(AuthenticationError::Password)
})
.instrument(tracing::info_span!("Verify hashed password"))
.await??;
// That went well, let's insert the auth info
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record(
"user_session_authentication.id",
tracing::field::display(id),
);
sqlx::query!(
r#"
INSERT INTO user_session_authentications
(user_session_authentication_id, user_session_id, created_at)
VALUES ($1, $2, $3)
"#,
Uuid::from(id),
Uuid::from(user_session.id),
created_at,
)
.execute(txn.borrow_mut())
.instrument(tracing::info_span!("Save authentication"))
.await
.map_err(AuthenticationError::Save)?;
user_session.last_authentication = Some(Authentication { id, created_at });
Ok(())
}
#[tracing::instrument(
skip_all,
fields(
user.id = %user_session.user.id,
%upstream_oauth_link.id,
%user_session.id,
user_session_authentication.id,
),
err,
)]
pub async fn authenticate_session_with_upstream(
executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send,
clock: &Clock,
user_session: &mut BrowserSession,
upstream_oauth_link: &UpstreamOAuthLink,
) -> Result<(), sqlx::Error> {
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record(
"user_session_authentication.id",
tracing::field::display(id),
);
sqlx::query!(
r#"
INSERT INTO user_session_authentications
(user_session_authentication_id, user_session_id, created_at)
VALUES ($1, $2, $3)
"#,
Uuid::from(id),
Uuid::from(user_session.id),
created_at,
)
.execute(executor)
.instrument(tracing::info_span!("Save authentication"))
.await?;
user_session.last_authentication = Some(Authentication { id, created_at });
Ok(())
}
#[tracing::instrument(
skip_all,
fields(
user.username = username,
user.id,
),
err(Debug),
)]
pub async fn register_user(
txn: &mut Transaction<'_, Postgres>,
mut rng: impl CryptoRng + Rng + Send,
clock: &Clock,
phf: impl PasswordHasher + Send,
username: &str,
password: &str,
) -> Result<User, anyhow::Error> {
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user.id", tracing::field::display(id));
sqlx::query!(
r#"
INSERT INTO users (user_id, username, created_at)
VALUES ($1, $2, $3)
"#,
Uuid::from(id),
username,
created_at,
)
.execute(txn.borrow_mut())
.instrument(info_span!("Register user"))
.await
.context("could not insert user")?;
let user = User {
id,
username: username.to_owned(),
sub: id.to_string(),
primary_email: None,
};
set_password(txn.borrow_mut(), &mut rng, clock, phf, &user, password).await?;
Ok(user)
}
#[tracing::instrument(
skip_all,
fields(
@ -521,7 +287,7 @@ pub async fn register_user(
),
err,
)]
pub async fn register_passwordless_user(
pub async fn add_user(
executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send,
clock: &Clock,
@ -551,47 +317,6 @@ pub async fn register_passwordless_user(
})
}
#[tracing::instrument(
skip_all,
fields(
%user.id,
user_password.id,
),
err(Debug),
)]
pub async fn set_password(
executor: impl PgExecutor<'_>,
mut rng: impl CryptoRng + Rng + Send,
clock: &Clock,
phf: impl PasswordHasher + Send,
user: &User,
password: &str,
) -> Result<(), anyhow::Error> {
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user_password.id", tracing::field::display(id));
let salt = SaltString::generate(&mut rng);
let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?;
sqlx::query_scalar!(
r#"
INSERT INTO user_passwords (user_password_id, user_id, hashed_password, created_at)
VALUES ($1, $2, $3, $4)
"#,
Uuid::from(id),
Uuid::from(user.id),
hashed_password.to_string(),
created_at,
)
.execute(executor)
.instrument(info_span!("Save user credentials"))
.await
.context("could not insert user password")?;
Ok(())
}
#[tracing::instrument(
skip_all,
fields(%user_session.id),
@ -1277,39 +1002,3 @@ pub async fn add_user_email_verification_code(
Ok(verification)
}
#[cfg(test)]
mod tests {
use rand::SeedableRng;
use super::*;
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_user_registration_and_login(pool: sqlx::PgPool) -> anyhow::Result<()> {
let clock = Clock::default();
let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
let mut txn = pool.begin().await?;
let exists = username_exists(&mut txn, "john").await?;
assert!(!exists);
let hasher = Argon2::default();
let user = register_user(&mut txn, &mut rng, &clock, hasher, "john", "hunter2").await?;
assert_eq!(user.username, "john");
let exists = username_exists(&mut txn, "john").await?;
assert!(exists);
let session = login(&mut txn, &mut rng, &clock, "john", "hunter2").await?;
assert_eq!(session.user.id, user.id);
let user2 = lookup_user_by_username(&mut txn, "john")
.await?
.context("Could not find user")?;
assert_eq!(user.id, user2.id);
txn.commit().await?;
Ok(())
}
}

View File

@ -0,0 +1,135 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use chrono::{DateTime, Utc};
use mas_data_model::{Password, User};
use rand::Rng;
use sqlx::PgExecutor;
use ulid::Ulid;
use uuid::Uuid;
use crate::{Clock, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
#[tracing::instrument(
skip_all,
fields(
%user.id,
%user.username,
user_password.id,
user_password.version = version,
),
err,
)]
pub async fn add_user_password(
executor: impl PgExecutor<'_>,
mut rng: impl Rng + Send,
clock: &Clock,
user: &User,
version: u16,
hashed_password: String,
upgraded_from: Option<Password>,
) -> Result<Password, DatabaseError> {
let created_at = clock.now();
let id = Ulid::from_datetime_with_source(created_at.into(), &mut rng);
tracing::Span::current().record("user_password.id", tracing::field::display(id));
let upgraded_from_id = upgraded_from.map(|p| p.id);
sqlx::query!(
r#"
INSERT INTO user_passwords
(user_password_id, user_id, hashed_password, version, upgraded_from_id, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
"#,
Uuid::from(id),
Uuid::from(user.id),
hashed_password,
i32::from(version),
upgraded_from_id.map(Uuid::from),
created_at,
)
.execute(executor)
.await?;
Ok(Password {
id,
hashed_password,
version,
upgraded_from_id,
created_at,
})
}
struct UserPasswordLookup {
user_password_id: Uuid,
hashed_password: String,
version: i32,
upgraded_from_id: Option<Uuid>,
created_at: DateTime<Utc>,
}
#[tracing::instrument(
skip_all,
fields(
%user.id,
%user.username,
),
err,
)]
pub async fn lookup_user_password(
executor: impl PgExecutor<'_>,
user: &User,
) -> Result<Option<Password>, DatabaseError> {
let res = sqlx::query_as!(
UserPasswordLookup,
r#"
SELECT up.user_password_id
, up.hashed_password
, up.version
, up.upgraded_from_id
, up.created_at
FROM user_passwords up
WHERE up.user_id = $1
ORDER BY up.created_at DESC
LIMIT 1
"#,
Uuid::from(user.id),
)
.fetch_one(executor)
.await
.to_option()?;
let Some(res) = res else { return Ok(None) };
let id = Ulid::from(res.user_password_id);
let version = res.version.try_into().map_err(|e| {
DatabaseInconsistencyError::on("user_passwords")
.column("version")
.row(id)
.source(e)
})?;
let upgraded_from_id = res.upgraded_from_id.map(Ulid::from);
let created_at = res.created_at;
let hashed_password = res.hashed_password;
Ok(Some(Password {
id,
hashed_password,
version,
upgraded_from_id,
created_at,
}))
}