diff --git a/file-service/migrations/20231003154201_initial_auth_db.sql b/file-service/migrations/20231003154201_initial_auth_db.sql index f049c62..46a16bb 100644 --- a/file-service/migrations/20231003154201_initial_auth_db.sql +++ b/file-service/migrations/20231003154201_initial_auth_db.sql @@ -5,8 +5,8 @@ CREATE TABLE IF NOT EXISTS users ( token TEXT NOT NULL ); -CREATE TABLE IF NOT EXISTS session_tokens ( +CREATE TABLE IF NOT EXISTS sessions ( token TEXT NOT NULL, - user INTEGER, - FOREIGN KEY(user) REFERENCES user(id) + user_id INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 96445f0..7a60a0e 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -67,6 +67,12 @@ pub enum ReadFileError { #[derive(Debug, Error)] pub enum AuthError { + #[error("authentication token is duplicated")] + DuplicateAuthToken, + + #[error("session token is duplicated")] + DuplicateSessionToken, + #[error("database failed")] SqlError(sqlx::Error), } @@ -250,11 +256,11 @@ impl App { } } - pub async fn auth_token(&self, token: AuthToken) -> Result { + pub async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { self.authdb.read().await.auth_token(token).await } - pub async fn auth_session(&self, token: SessionToken) -> Result { + pub async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { self.authdb.read().await.auth_session(token).await } @@ -289,10 +295,9 @@ impl AuthDB { } async fn add_user(&self, username: Username) -> Result { - let auth_plaintext = format!("{}:{}", Uuid::new_v4(), username.to_string()); - let mut hasher = Sha256::new(); - hasher.update(auth_plaintext); + hasher.update(Uuid::new_v4().hyphenated().to_string()); + hasher.update(username.to_string()); let auth_token = Base64::encode_string(&hasher.finalize()); let _ = sqlx::query("INSERT INTO users (username, token) VALUES ($1, $2)") @@ -308,22 +313,57 @@ impl AuthDB { let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users") .fetch_all(&self.pool) .await?; - /* - let usernames = result - .into_iter() - .map(|row| Username::from(row.column(0))) - .collect::>(); - */ Ok(usernames) } - async fn auth_token(&self, _token: AuthToken) -> Result { - unimplemented!() + async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { + let results = sqlx::query("SELECT * FROM users WHERE token = $1") + .bind(token.to_string()) + .fetch_all(&self.pool) + .await?; + + if results.len() > 1 { + return Err(AuthError::DuplicateAuthToken); + } + + if results.len() == 0 { + return Ok(None); + } + + let user_id: i64 = results[0].try_get("id")?; + + let mut hasher = Sha256::new(); + hasher.update(Uuid::new_v4().hyphenated().to_string()); + hasher.update(token.to_string()); + let session_token = Base64::encode_string(&hasher.finalize()); + + let _ = sqlx::query("INSERT INTO sessions (token, user_id) VALUES ($1, $2)") + .bind(session_token.clone()) + .bind(user_id) + .execute(&self.pool) + .await?; + + Ok(Some(SessionToken::from(session_token))) } - async fn auth_session(&self, _token: SessionToken) -> Result { - unimplemented!() + async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { + let rows = sqlx::query( + "SELECT users.username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessions.token = $1", + ) + .bind(token.to_string()) + .fetch_all(&self.pool) + .await?; + if rows.len() > 1 { + return Err(AuthError::DuplicateSessionToken); + } + + if rows.len() == 0 { + return Ok(None); + } + + let username: String = rows[0].try_get("username")?; + Ok(Some(Username::from(username))) } } @@ -485,13 +525,53 @@ mod authdb_test { }) } - #[test] - fn can_authenticate_token() { - unimplemented!() + #[tokio::test] + async fn unknown_auth_token_returns_nothing() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let _ = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + + let token = AuthToken::from("0000000000"); + + assert_matches!(db.auth_token(token).await, Ok(None)); } - #[test] - fn can_validate_session_token() { - unimplemented!() + #[tokio::test] + async fn auth_token_becomes_session_token() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let token = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + + assert_matches!(db.auth_token(token).await, Ok(_)); + } + + #[tokio::test] + async fn can_validate_session_token() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let token = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + let session = db + .auth_token(token) + .await + .expect("token authentication should succeed") + .expect("session token should be found"); + + assert_matches!( + db.auth_session(session).await, + Ok(Some(username)) => { + assert_eq!(username, Username::from("savanni")); + }); } }