app/src/auth.rs

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");
}
}