diff --git a/crates/core/migrations/20211214161231_password_credentials.down.sql b/crates/core/migrations/20211214161231_password_credentials.down.sql new file mode 100644 index 00000000..6b106b32 --- /dev/null +++ b/crates/core/migrations/20211214161231_password_credentials.down.sql @@ -0,0 +1,26 @@ +-- Copyright 2021 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. + +ALTER TABLE users ADD COLUMN hashed_password TEXT; + +UPDATE users u + SET hashed_password = up.hashed_password + FROM user_passwords up + WHERE up.user_id = u.id; + +ALTER TABLE users + ALTER COLUMN hashed_password + SET NOT NULL; + +DROP TABLE user_passwords; diff --git a/crates/core/migrations/20211214161231_password_credentials.up.sql b/crates/core/migrations/20211214161231_password_credentials.up.sql new file mode 100644 index 00000000..33642987 --- /dev/null +++ b/crates/core/migrations/20211214161231_password_credentials.up.sql @@ -0,0 +1,27 @@ +-- Copyright 2021 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. + +CREATE TABLE user_passwords ( + "id" BIGSERIAL PRIMARY KEY, + "user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "hashed_password" TEXT NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +INSERT INTO + user_passwords (user_id, hashed_password, created_at) +SELECT id, hashed_password, updated_at +FROM users; + +ALTER TABLE users DROP COLUMN hashed_password; diff --git a/crates/core/sqlx-data.json b/crates/core/sqlx-data.json index 28b7648e..c63862e2 100644 --- a/crates/core/sqlx-data.json +++ b/crates/core/sqlx-data.json @@ -794,26 +794,6 @@ "nullable": [] } }, - "a552eee8a8e5ffdee4d4789c634851bd64780dfe730807aac20142d7cd643814": { - "query": "\n SELECT u.hashed_password\n FROM user_sessions s\n INNER JOIN users u\n ON u.id = s.user_id \n WHERE s.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "hashed_password", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "c29e741474aacc91c0aacc028a9e7452a5327d5ce6d4b791bf20a2636069087e": { "query": "\n INSERT INTO oauth2_sessions\n (user_session_id, client_id, scope)\n SELECT\n $1,\n og.client_id,\n og.scope\n FROM\n oauth2_authorization_grants og\n WHERE\n og.id = $2\n RETURNING id, created_at\n ", "describe": { @@ -874,6 +854,19 @@ ] } }, + "d7200c0def0662fda4af259c7872e06b8208e36f320ca90ea781c13d2bf85a9f": { + "query": "\n INSERT INTO user_passwords (user_id, hashed_password)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [] + } + }, "d9d27eb4a0c11818a636d407438c4bc567a39396e7e236b3e776504417988eab": { "query": "\n INSERT INTO user_session_authentications (session_id)\n VALUES ($1)\n RETURNING id, created_at\n ", "describe": { @@ -900,8 +893,8 @@ ] } }, - "f9a09ff53b6f221649f4f050e3d5ade114f852ddf50a78610a6c0ef0689af681": { - "query": "\n INSERT INTO users (username, hashed_password)\n VALUES ($1, $2)\n RETURNING id\n ", + "dda03ba41249bff965cb8f129acc15f4e40807adb9b75dee0ac43edd7809de84": { + "query": "\n INSERT INTO users (username)\n VALUES ($1)\n RETURNING id\n ", "describe": { "columns": [ { @@ -912,7 +905,6 @@ ], "parameters": { "Left": [ - "Text", "Text" ] }, @@ -920,5 +912,25 @@ false ] } + }, + "fe1fcf14de164f06a8fa1a0512ff955beaf83df0eebc4d63ea28d837613f8fed": { + "query": "\n SELECT up.hashed_password\n FROM user_sessions s\n INNER JOIN user_passwords up\n ON up.id = s.user_id \n WHERE s.id = $1\n ORDER BY up.created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hashed_password", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } } } \ No newline at end of file diff --git a/crates/core/src/handlers/views/register.rs b/crates/core/src/handlers/views/register.rs index 02275e9f..20d7f992 100644 --- a/crates/core/src/handlers/views/register.rs +++ b/crates/core/src/handlers/views/register.rs @@ -18,7 +18,7 @@ use mas_config::{CookiesConfig, CsrfConfig}; use mas_data_model::{BrowserSession, StorageBackend}; use mas_templates::{RegisterContext, TemplateContext, Templates}; use serde::Deserialize; -use sqlx::{pool::PoolConnection, PgPool, Postgres}; +use sqlx::{pool::PoolConnection, PgPool, Postgres, Transaction}; use warp::{reply::html, Filter, Rejection, Reply}; use super::{LoginRequest, PostAuthAction}; @@ -27,7 +27,7 @@ use crate::{ filters::{ cookies::{encrypted_cookie_saver, EncryptedCookieSaver}, csrf::{protected_form, updated_csrf_token}, - database::connection, + database::{connection, transaction}, session::{optional_session, SessionCookie}, with_templates, CsrfToken, }, @@ -108,7 +108,7 @@ pub(super) fn filter( .and_then(get); let post = warp::post() - .and(connection(pool)) + .and(transaction(pool)) .and(encrypted_cookie_saver(cookies_config)) .and(protected_form(cookies_config)) .and(warp::query()) @@ -147,7 +147,7 @@ async fn get( } async fn post( - mut conn: PoolConnection, + mut txn: Transaction<'_, Postgres>, cookie_saver: EncryptedCookieSaver, form: RegisterForm, query: RegisterRequest, @@ -158,11 +158,11 @@ async fn post( } let pfh = Argon2::default(); - let user = register_user(&mut conn, pfh, &form.username, &form.password) + let user = register_user(&mut txn, pfh, &form.username, &form.password) .await .wrap_error()?; - let session_info = start_session(&mut conn, user).await.wrap_error()?; + let session_info = start_session(&mut txn, user).await.wrap_error()?; let session_cookie = SessionCookie::from_session(&session_info); let reply = query.redirect()?; diff --git a/crates/core/src/storage/user.rs b/crates/core/src/storage/user.rs index 91f590dd..4d5fb004 100644 --- a/crates/core/src/storage/user.rs +++ b/crates/core/src/storage/user.rs @@ -241,11 +241,13 @@ pub async fn authenticate_session( // First, fetch the hashed password from the user associated with that session let hashed_password: String = sqlx::query_scalar!( r#" - SELECT u.hashed_password + SELECT up.hashed_password FROM user_sessions s - INNER JOIN users u - ON u.id = s.user_id + INNER JOIN user_passwords up + ON up.id = s.user_id WHERE s.id = $1 + ORDER BY up.created_at DESC + LIMIT 1 "#, session.data, ) @@ -285,28 +287,40 @@ pub async fn authenticate_session( } pub async fn register_user( - executor: impl PgExecutor<'_>, + txn: &mut Transaction<'_, Postgres>, phf: impl PasswordHasher, username: &str, password: &str, ) -> anyhow::Result> { - let salt = SaltString::generate(&mut OsRng); - let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?; - let id: i64 = sqlx::query_scalar!( r#" - INSERT INTO users (username, hashed_password) - VALUES ($1, $2) + INSERT INTO users (username) + VALUES ($1) RETURNING id "#, username, - hashed_password.to_string(), ) - .fetch_one(executor) + .fetch_one(txn.borrow_mut()) .instrument(info_span!("Register user")) .await .context("could not insert user")?; + let salt = SaltString::generate(&mut OsRng); + let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?; + + sqlx::query_scalar!( + r#" + INSERT INTO user_passwords (user_id, hashed_password) + VALUES ($1, $2) + "#, + id, + hashed_password.to_string(), + ) + .execute(txn.borrow_mut()) + .instrument(info_span!("Save user credentials")) + .await + .context("could not insert user password")?; + Ok(User { data: id, username: username.to_string(),