147 lines
4.6 KiB
Rust
147 lines
4.6 KiB
Rust
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<ServiceRequest, actix_web::error::Error> {
|
|
// Retrieve shared app state
|
|
let state = req
|
|
.app_data::<Data<AppState>>()
|
|
.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::<AuthClaims>(
|
|
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::Model> = 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<Vec<u8>> {
|
|
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::<AuthClaims>(
|
|
&token,
|
|
&DecodingKey::from_secret(&jwt_secret),
|
|
&Validation::default(),
|
|
);
|
|
|
|
decoded.expect("Decode failure");
|
|
}
|
|
}
|