216 lines
8.4 KiB
Rust
216 lines
8.4 KiB
Rust
use crate::app_config::AppConfig;
|
|
use crate::constants;
|
|
use crate::extractors::money_session::MoneySession;
|
|
use crate::models::tokens::{Token, TokenID};
|
|
use crate::models::users::{User, UserID};
|
|
use crate::services::{tokens_service, users_service};
|
|
use actix_remote_ip::RemoteIP;
|
|
use actix_web::dev::Payload;
|
|
use actix_web::error::ErrorPreconditionFailed;
|
|
use actix_web::{Error, FromRequest, HttpRequest};
|
|
use jwt_simple::algorithms::{HS256Key, MACLike};
|
|
use jwt_simple::common::VerificationOptions;
|
|
use jwt_simple::prelude::Duration;
|
|
use std::str::FromStr;
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
pub struct TokenClaims {
|
|
#[serde(rename = "met")]
|
|
pub method: String,
|
|
pub uri: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum AuthenticatedMethod {
|
|
/// User is authenticated using a cookie
|
|
Cookie,
|
|
/// User is authenticated through command line, for debugging purposes only
|
|
Dev,
|
|
/// User is authenticated using an API token
|
|
Token(Token),
|
|
}
|
|
|
|
/// Authentication extractor. Extract authentication information from request
|
|
pub struct AuthExtractor {
|
|
pub method: AuthenticatedMethod,
|
|
pub user: User,
|
|
}
|
|
|
|
impl AuthExtractor {
|
|
/// Get current user ID
|
|
pub fn user_id(&self) -> UserID {
|
|
self.user.id()
|
|
}
|
|
}
|
|
|
|
impl FromRequest for AuthExtractor {
|
|
type Error = Error;
|
|
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
|
|
|
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
|
let req = req.clone();
|
|
|
|
let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() {
|
|
Ok(ip) => ip,
|
|
Err(e) => return Box::pin(async { Err(e) }),
|
|
};
|
|
|
|
Box::pin(async move {
|
|
// Check for authentication using OpenID
|
|
if let Some(token) = req.headers().get(constants::API_TOKEN_HEADER) {
|
|
let Ok(jwt_token) = token.to_str() else {
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"Failed to decode token as string!",
|
|
));
|
|
};
|
|
|
|
let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
log::error!("Failed to decode JWT header metadata! {e}");
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"Failed to decode JWT header metadata!",
|
|
));
|
|
}
|
|
};
|
|
|
|
// Extract token ID
|
|
let Some(kid) = metadata.key_id() else {
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"Missing key id in request!",
|
|
));
|
|
};
|
|
|
|
let token_id = match TokenID::from_str(kid) {
|
|
Ok(i) => i,
|
|
Err(e) => {
|
|
log::error!("Failed to parse token id! {e}");
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"Failed to parse token id!",
|
|
));
|
|
}
|
|
};
|
|
|
|
// Get token information
|
|
let Ok(token) = tokens_service::get_by_id(token_id).await else {
|
|
log::error!("Token not found!");
|
|
return Err(actix_web::error::ErrorForbidden("Token not found!"));
|
|
};
|
|
|
|
// Decode JWT
|
|
let key = HS256Key::from_bytes(token.token_value.as_ref());
|
|
let verif = VerificationOptions {
|
|
max_validity: Some(Duration::from_secs(constants::API_TOKEN_JWT_MAX_DURATION)),
|
|
..Default::default()
|
|
};
|
|
|
|
let claims = match key.verify_token::<TokenClaims>(jwt_token, Some(verif)) {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
log::error!("JWT validation failed! {e}");
|
|
return Err(actix_web::error::ErrorForbidden("JWT validation failed!"));
|
|
}
|
|
};
|
|
|
|
// Check for nonce
|
|
if claims.nonce.is_none() {
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"A nonce is required in auth JWT!",
|
|
));
|
|
}
|
|
|
|
// Check IP restriction
|
|
if let Some(net) = token.ip_net() {
|
|
if !net.contains(&remote_ip.0) {
|
|
log::error!(
|
|
"Trying to use token {:?} from unauthorized IP address: {remote_ip:?}",
|
|
token.id()
|
|
);
|
|
return Err(actix_web::error::ErrorForbidden(
|
|
"This token cannot be used from this IP address!",
|
|
));
|
|
}
|
|
}
|
|
|
|
// Check for write access
|
|
if token.read_only && !req.method().is_safe() {
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"Read only token cannot perform write operations!",
|
|
));
|
|
}
|
|
|
|
// Check for authorization
|
|
let uri = req.uri().to_string();
|
|
let authorized = (uri.starts_with("/api/account") && token.right_account)
|
|
|| (uri.starts_with("/api/movement") && token.right_movement)
|
|
|| (uri.starts_with("/api/inbox") && token.right_inbox)
|
|
|| (uri.starts_with("/api/file") && token.right_file)
|
|
|| (uri.starts_with("/api/auth/") && token.right_auth)
|
|
|| (uri.starts_with("/api/stats") && token.right_stats)
|
|
|| (uri.starts_with("/api/backup") && token.right_backup);
|
|
|
|
if !authorized {
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"This token cannot be used to query this route!",
|
|
));
|
|
}
|
|
|
|
// Get user information
|
|
let Ok(user) = users_service::get_user_by_id(token.user_id()) else {
|
|
return Err(actix_web::error::ErrorBadRequest(
|
|
"Failed to get user information from token!",
|
|
));
|
|
};
|
|
|
|
// Update last use (if needed)
|
|
if token.shall_update_time_used() {
|
|
if let Err(e) = tokens_service::update_time_used(&token).await {
|
|
log::error!("Failed to refresh last usage of token! {}", e);
|
|
}
|
|
}
|
|
|
|
// Handle tokens expiration
|
|
if token.is_expired() {
|
|
log::error!("Attempted to use expired token! {:?}", token);
|
|
return Err(actix_web::error::ErrorBadRequest("Token has expired!"));
|
|
}
|
|
|
|
return Ok(Self {
|
|
method: AuthenticatedMethod::Token(token),
|
|
user,
|
|
});
|
|
}
|
|
|
|
// Check if login is hard-coded as program argument
|
|
if let Some(email) = &AppConfig::get().unsecure_auto_login_email {
|
|
let user = users_service::get_user_by_email(email).map_err(|e| {
|
|
log::error!("Failed to retrieve dev user: {e}");
|
|
ErrorPreconditionFailed("Unable to retrieve dev user!")
|
|
})?;
|
|
return Ok(Self {
|
|
method: AuthenticatedMethod::Dev,
|
|
user,
|
|
});
|
|
}
|
|
|
|
// Check for cookie authentication
|
|
let session = MoneySession::extract(&req).await?;
|
|
if let Some(user_id) = session.current_user().map_err(|e| {
|
|
log::error!("Failed to retrieve user id: {e}");
|
|
ErrorPreconditionFailed("Failed to read session information!")
|
|
})? {
|
|
let user = users_service::get_user_by_id(user_id).map_err(|e| {
|
|
log::error!("Failed to retrieve user from cookie session: {e}");
|
|
ErrorPreconditionFailed("Failed to retrieve user information!")
|
|
})?;
|
|
return Ok(Self {
|
|
method: AuthenticatedMethod::Cookie,
|
|
user,
|
|
});
|
|
};
|
|
|
|
Err(ErrorPreconditionFailed("Authentication required!"))
|
|
})
|
|
}
|
|
}
|