use crate::{ entity::setting, error::{Error, ErrorCode}, AppState, }; use actix_web::{dev::ServiceRequest, web::Data}; use actix_web_httpauth::extractors::bearer::BearerAuth; use bcrypt::verify; use jsonwebtoken::{decode, DecodingKey, Validation}; use sea_orm::{prelude::*, ActiveValue::NotSet, Set}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct AuthClaims { pub exp: usize, } /// JWT validation middleware. Used to wrap protected routes in Actix . pub async fn validator( req: ServiceRequest, credentials: BearerAuth, ) -> Result { // Retrieve shared app state let state = req .app_data::>() .ok_or_else(|| Error::new(ErrorCode::Internal, "Could not retrieve state"))?; // Get the secret from the database let secret = get_secret(&state.db).await?; // decode the JWT token from the header let decoded = decode::( credentials.token(), &DecodingKey::from_secret(&secret), &Validation::default(), ); // We have no users or anything else to do, if the token is properly formed we are good to go. match decoded { Ok(_) => Ok(req), Err(e) => Err(Error::new(ErrorCode::UnAuthorized, &e.to_string()).into()), } } /// Verifies the passed password against the password hash stored in the settings table in the database. pub async fn verify_password(db: &DatabaseConnection, password: &str) -> bool { let rec: Option = crate::entity::setting::Entity::find() .filter(crate::entity::setting::Column::SettingName.eq("password_hash".to_string())) .one(db) .await .unwrap_or_default(); if let Some(rec) = rec { verify(password, &rec.setting_value).unwrap_or(false) } else { false } } pub async fn set_password(db: &DatabaseConnection, password: &str) -> crate::error::Result<()> { let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST) .map_err(|e| Error::new(ErrorCode::Internal, &format!("Hash error: {}", e)))?; let rec = crate::entity::setting::Entity::find() .filter(crate::entity::setting::Column::SettingName.eq("password_hash".to_string())) .one(db) .await?; if let Some(rec) = rec { setting::ActiveModel { id: Set(rec.id), setting_value: Set(password_hash), setting_name: NotSet, } .update(db) .await?; } else { setting::ActiveModel { id: NotSet, setting_name: Set("password_hash".into()), setting_value: Set(password_hash), } .insert(db) .await?; } Ok(()) } use rand::Rng; use tracing::instrument; #[instrument(skip_all)] /// Generate a new secret and store it to the settings table. This is used during intiial configuration. pub async fn generate_secret(db: &DatabaseConnection) -> crate::error::Result<()> { let b = rand::thread_rng().gen::<[u8; 32]>(); setting::ActiveModel { id: NotSet, setting_name: Set("secret".to_string()), setting_value: Set(base64::encode(b)), } .insert(db) .await?; Ok(()) } #[instrument(skip_all)] /// Retrieves the JWT secret from the database. pub async fn get_secret(db: &DatabaseConnection) -> crate::error::Result> { let rec = setting::Entity::find() .filter(setting::Column::SettingName.eq("secret".to_string())) .one(db) .await?; if let Some(rec) = rec { if let Ok(b64) = base64::decode(rec.setting_value) { Ok(b64) } else { Err(Error::new(ErrorCode::Internal, "Could not decode secret")) } } else { Err(Error::new(ErrorCode::NoSetup, "Not setup")) } } #[cfg(test)] mod tests { use jsonwebtoken::{decode, DecodingKey, Validation}; use super::{get_secret, AuthClaims}; #[actix_rt::test] async fn test_tokens() { let state = crate::api::test_prelude::setup_state().await.unwrap(); let jwt_secret = get_secret(&state.db).await.unwrap(); let token = jsonwebtoken::encode( &jsonwebtoken::Header::default(), &crate::auth::AuthClaims { exp: 10_000_000_000, }, &jsonwebtoken::EncodingKey::from_secret(&jwt_secret), ) .unwrap(); let decoded = decode::( &token, &DecodingKey::from_secret(&jwt_secret), &Validation::default(), ); decoded.expect("Decode failure"); } }