Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dashboard endpoints #48

Merged
merged 4 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 112 additions & 6 deletions atoma-auth/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}
use rand::Rng;
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
use tracing::instrument;
use tracing::{error, instrument};

use crate::AtomaAuthConfig;

Expand All @@ -28,6 +28,7 @@ pub struct Claims {
}

/// The Auth struct
#[derive(Clone)]
pub struct Auth {
/// The secret key for JWT authentication.
secret_key: String,
Expand Down Expand Up @@ -67,8 +68,7 @@ impl Auth {
/// # Errors
///
/// * If the token generation fails
#[instrument(level = "trace", skip(self))]
fn generate_refresh_token(&self, user_id: i64) -> Result<String> {
async fn generate_refresh_token(&self, user_id: i64) -> Result<String> {
let expiration = Utc::now() + Duration::days(self.refresh_token_lifetime as i64);
let claims = Claims {
user_id,
Expand All @@ -80,6 +80,11 @@ impl Auth {
&claims,
&EncodingKey::from_secret(self.secret_key.as_ref()),
)?;
self.state_manager_sender
.send(AtomaAtomaStateManagerEvent::StoreRefreshToken {
user_id,
refresh_token_hash: self.hash_string(&token),
})?;
Ok(token)
}

Expand Down Expand Up @@ -185,13 +190,34 @@ impl Auth {
/// # Returns
///
/// * `String` - The hashed password
fn hash_string(&self, text: &str) -> String {
pub fn hash_string(&self, text: &str) -> String {
let mut hasher = Blake2b::new();
hasher.update(text);
let hash_result: GenericArray<u8, U32> = hasher.finalize();
hex::encode(hash_result)
}

/// Register user with username/password.
/// This method will register a new user with a username and password
/// The password is hashed and stored in the DB
/// The method will generate a new refresh and access token
pub async fn register(&self, username: &str, password: &str) -> Result<(String, String)> {
let (result_sender, result_receiver) = oneshot::channel();
self.state_manager_sender
.send(AtomaAtomaStateManagerEvent::RegisterUserWithPassword {
username: username.to_string(),
password: self.hash_string(password),
result_sender,
})?;
let user_id = result_receiver
.await??
.map(|user_id| user_id as u64)
.ok_or_else(|| anyhow::anyhow!("User already registred"))?;
let refresh_token = self.generate_refresh_token(user_id as i64).await?;
let access_token = self.generate_access_token(&refresh_token).await?;
Ok((refresh_token, access_token))
}

/// Check the user password
/// This method will check if the user password is correct
/// The password is hashed and compared with the hashed password in the DB
Expand All @@ -214,7 +240,7 @@ impl Auth {
.await??
.map(|user_id| user_id as u64)
.ok_or_else(|| anyhow::anyhow!("User not found"))?;
let refresh_token = self.generate_refresh_token(user_id as i64)?;
let refresh_token = self.generate_refresh_token(user_id as i64).await?;
let access_token = self.generate_access_token(&refresh_token).await?;
Ok((refresh_token, access_token))
}
Expand Down Expand Up @@ -256,6 +282,76 @@ impl Auth {
})?;
Ok(api_token)
}

/// Revoke an API token
/// This method will revoke an API token for the user
/// The method will check if the access token and its corresponding refresh token is valid and revoke the API token in the state manager
///
/// # Arguments
///
/// * `jwt` - The access token to be used to revoke the API token
/// * `api_token` - The API token to be revoked
///
/// # Returns
///
/// * `Result<()>` - If the API token was revoked
#[instrument(level = "info", skip(self))]
pub async fn revoke_api_token(&self, jwt: &str, api_token: &str) -> Result<()> {
Cifko marked this conversation as resolved.
Show resolved Hide resolved
let claims = self.validate_token(jwt, false)?;
if !self
.check_refresh_token_validity(
claims.user_id,
&claims
.refresh_token_hash
.expect("Access token should have refresh token hash"),
)
.await?
{
return Err(anyhow::anyhow!("Access token was revoked"));
}
self.state_manager_sender
.send(AtomaAtomaStateManagerEvent::RevokeApiToken {
user_id: claims.user_id,
api_token: api_token.to_string(),
})?;
Ok(())
}

/// Get all API tokens for a user
/// This method will get all API tokens for a user
/// The method will check if the access token and its corresponding refresh token is valid
///
/// # Arguments
///
/// * `jwt` - The access token to be used to get the API tokens
///
/// # Returns
///
/// * `Result<Vec<String>>` - The list of API tokens
#[instrument(level = "info", skip(self))]
pub async fn get_all_api_tokens(&self, jwt: &str) -> Result<Vec<String>> {
Cifko marked this conversation as resolved.
Show resolved Hide resolved
let claims = self.validate_token(jwt, false)?;
if !self
.check_refresh_token_validity(
claims.user_id,
&claims
.refresh_token_hash
.expect("Access token should have refresh token hash"),
)
.await?
{
error!("Access token was revoked");
return Err(anyhow::anyhow!("Access token was revoked"));
Cifko marked this conversation as resolved.
Show resolved Hide resolved
}

let (result_sender, result_receiver) = oneshot::channel();
self.state_manager_sender
.send(AtomaAtomaStateManagerEvent::GetApiTokensForUser {
user_id: claims.user_id,
result_sender,
})?;
Ok(result_receiver.await??)
}
}

// TODO: Add more comprehensive tests, for now test the happy path only
Expand All @@ -279,9 +375,14 @@ mod test {
async fn test_access_token_regenerate() {
let (auth, receiver) = setup_test();
let user_id = 123;
let refresh_token = auth.generate_refresh_token(user_id).unwrap();
let refresh_token = auth.generate_refresh_token(user_id).await.unwrap();
let refresh_token_hash = auth.hash_string(&refresh_token);
let mock_handle = tokio::task::spawn(async move {
let event = receiver.recv_async().await.unwrap();
match event {
AtomaAtomaStateManagerEvent::StoreRefreshToken { .. } => {}
_ => panic!("Unexpected event"),
}
let event = receiver.recv_async().await.unwrap();
match event {
AtomaAtomaStateManagerEvent::IsRefreshTokenValid {
Expand Down Expand Up @@ -330,6 +431,11 @@ mod test {
}
_ => panic!("Unexpected event"),
}
let event = receiver.recv_async().await.unwrap();
match event {
AtomaAtomaStateManagerEvent::StoreRefreshToken { .. } => {}
_ => panic!("Unexpected event"),
}
for _ in 0..2 {
// During the token generation, the refresh token is checked for validity
// 1) when the user logs in
Expand Down
2 changes: 2 additions & 0 deletions atoma-proxy-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ license.workspace = true

[dependencies]
anyhow.workspace = true
atoma-auth.workspace = true
atoma-state.workspace = true
atoma-sui.workspace = true
axum.workspace = true
config.workspace = true
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full"] }
tower-http = { version = "0.6.2", features = ["cors"] }
tracing.workspace = true
tracing-subscriber.workspace = true
utoipa = { workspace = true, features = ["axum_extras"] }
Loading