Start to implement API tokens checks
This commit is contained in:
		@@ -5,11 +5,19 @@ use crate::constants;
 | 
				
			|||||||
use crate::utils::jwt_utils;
 | 
					use crate::utils::jwt_utils;
 | 
				
			||||||
use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey};
 | 
					use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey};
 | 
				
			||||||
use crate::utils::time_utils::time;
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
 | 
					use actix_http::Method;
 | 
				
			||||||
use std::path::Path;
 | 
					use std::path::Path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
 | 
				
			||||||
pub struct TokenID(pub uuid::Uuid);
 | 
					pub struct TokenID(pub uuid::Uuid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TokenID {
 | 
				
			||||||
 | 
					    /// Parse a string as a token id
 | 
				
			||||||
 | 
					    pub fn parse(t: &str) -> anyhow::Result<Self> {
 | 
				
			||||||
 | 
					        Ok(Self(uuid::Uuid::parse_str(t)?))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
				
			||||||
pub struct TokenRight {
 | 
					pub struct TokenRight {
 | 
				
			||||||
    verb: TokenVerb,
 | 
					    verb: TokenVerb,
 | 
				
			||||||
@@ -29,9 +37,9 @@ pub struct Token {
 | 
				
			|||||||
    #[serde(skip_serializing_if = "TokenPubKey::is_invalid")]
 | 
					    #[serde(skip_serializing_if = "TokenPubKey::is_invalid")]
 | 
				
			||||||
    pub pub_key: TokenPubKey,
 | 
					    pub pub_key: TokenPubKey,
 | 
				
			||||||
    pub rights: TokenRights,
 | 
					    pub rights: TokenRights,
 | 
				
			||||||
    pub last_used: Option<u64>,
 | 
					    pub last_used: u64,
 | 
				
			||||||
    pub ip_restriction: Option<ipnetwork::IpNetwork>,
 | 
					    pub ip_restriction: Option<ipnetwork::IpNetwork>,
 | 
				
			||||||
    pub delete_after_inactivity: Option<u64>,
 | 
					    pub max_inactivity: Option<u64>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Token {
 | 
					impl Token {
 | 
				
			||||||
@@ -45,9 +53,25 @@ impl Token {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Load token information from a file
 | 
					    /// Load token information from a file
 | 
				
			||||||
    pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
 | 
					    fn load_from_file(path: &Path) -> anyhow::Result<Self> {
 | 
				
			||||||
        Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
 | 
					        Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Check whether a token is expired or not
 | 
				
			||||||
 | 
					    pub fn is_expired(&self) -> bool {
 | 
				
			||||||
 | 
					        if let Some(max_inactivity) = self.max_inactivity {
 | 
				
			||||||
 | 
					            if max_inactivity + self.last_used < time() {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Check whether last_used shall be updated or not
 | 
				
			||||||
 | 
					    pub fn should_update_last_activity(&self) -> bool {
 | 
				
			||||||
 | 
					        self.last_used + 3600 < time()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
 | 
				
			||||||
@@ -59,6 +83,18 @@ pub enum TokenVerb {
 | 
				
			|||||||
    DELETE,
 | 
					    DELETE,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TokenVerb {
 | 
				
			||||||
 | 
					    pub fn as_method(&self) -> Method {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            TokenVerb::GET => Method::GET,
 | 
				
			||||||
 | 
					            TokenVerb::POST => Method::POST,
 | 
				
			||||||
 | 
					            TokenVerb::PUT => Method::PUT,
 | 
				
			||||||
 | 
					            TokenVerb::PATCH => Method::PATCH,
 | 
				
			||||||
 | 
					            TokenVerb::DELETE => Method::DELETE,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Structure used to create a token
 | 
					/// Structure used to create a token
 | 
				
			||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
				
			||||||
pub struct NewToken {
 | 
					pub struct NewToken {
 | 
				
			||||||
@@ -125,9 +161,9 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> {
 | 
				
			|||||||
        updated: time(),
 | 
					        updated: time(),
 | 
				
			||||||
        pub_key,
 | 
					        pub_key,
 | 
				
			||||||
        rights: t.rights.clone(),
 | 
					        rights: t.rights.clone(),
 | 
				
			||||||
        last_used: Some(time()),
 | 
					        last_used: time(),
 | 
				
			||||||
        ip_restriction: t.ip_restriction,
 | 
					        ip_restriction: t.ip_restriction,
 | 
				
			||||||
        delete_after_inactivity: t.delete_after_inactivity,
 | 
					        max_inactivity: t.delete_after_inactivity,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    token.save()?;
 | 
					    token.save()?;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										123
									
								
								virtweb_backend/src/extractors/api_auth_extractor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								virtweb_backend/src/extractors/api_auth_extractor.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					use crate::api_tokens::{Token, TokenID, TokenVerb};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::api_tokens;
 | 
				
			||||||
 | 
					use crate::utils::jwt_utils;
 | 
				
			||||||
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
 | 
					use actix_web::dev::Payload;
 | 
				
			||||||
 | 
					use actix_web::error::ErrorBadRequest;
 | 
				
			||||||
 | 
					use actix_web::{Error, FromRequest, HttpRequest};
 | 
				
			||||||
 | 
					use std::future::Future;
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(serde::Serialize, serde::Deserialize, Debug)]
 | 
				
			||||||
 | 
					pub struct TokenClaims {
 | 
				
			||||||
 | 
					    pub sub: String,
 | 
				
			||||||
 | 
					    pub iat: usize,
 | 
				
			||||||
 | 
					    pub exp: usize,
 | 
				
			||||||
 | 
					    pub verb: TokenVerb,
 | 
				
			||||||
 | 
					    pub path: String,
 | 
				
			||||||
 | 
					    pub nonce: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ApiAuthExtractor {
 | 
				
			||||||
 | 
					    pub token: Token,
 | 
				
			||||||
 | 
					    pub claims: TokenClaims,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl FromRequest for ApiAuthExtractor {
 | 
				
			||||||
 | 
					    type Error = Error;
 | 
				
			||||||
 | 
					    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
 | 
				
			||||||
 | 
					        let req = req.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Box::pin(async move {
 | 
				
			||||||
 | 
					            let (token_id, token_jwt) = match (
 | 
				
			||||||
 | 
					                req.headers().get("x-token-id"),
 | 
				
			||||||
 | 
					                req.headers().get("x-token-content"),
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                (Some(id), Some(jwt)) => (
 | 
				
			||||||
 | 
					                    id.to_str().unwrap_or("").to_string(),
 | 
				
			||||||
 | 
					                    jwt.to_str().unwrap_or("").to_string(),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (_, _) => {
 | 
				
			||||||
 | 
					                    return Err(ErrorBadRequest("API auth headers were not all specified!"));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let token_id = match TokenID::parse(&token_id) {
 | 
				
			||||||
 | 
					                Ok(t) => t,
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    log::error!("Failed to parse token id! {e}");
 | 
				
			||||||
 | 
					                    return Err(ErrorBadRequest("Unable to validate token ID!"));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let token = match api_tokens::get_single(token_id).await {
 | 
				
			||||||
 | 
					                Ok(t) => t,
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    log::error!("Failed to retrieve token: {e}");
 | 
				
			||||||
 | 
					                    return Err(ErrorBadRequest("Unable to validate token!"));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if token.is_expired() {
 | 
				
			||||||
 | 
					                log::error!("Token has expired (not been used for too long)!");
 | 
				
			||||||
 | 
					                return Err(ErrorBadRequest("Unable to validate token!"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let claims = match jwt_utils::validate_jwt::<TokenClaims>(&token.pub_key, &token_jwt) {
 | 
				
			||||||
 | 
					                Ok(c) => c,
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    log::error!("Failed to validate JWT: {e}");
 | 
				
			||||||
 | 
					                    return Err(ErrorBadRequest("Unable to validate token!"));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if claims.sub != token.id.0.to_string() {
 | 
				
			||||||
 | 
					                log::error!("JWT sub mismatch (should equal to token id)!");
 | 
				
			||||||
 | 
					                return Err(ErrorBadRequest(
 | 
				
			||||||
 | 
					                    "JWT sub mismatch (should equal to token id)!",
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if time() + 60 * 15 < claims.iat as u64 {
 | 
				
			||||||
 | 
					                log::error!("iat is in the future!");
 | 
				
			||||||
 | 
					                return Err(ErrorBadRequest("iat is in the future!"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if claims.exp < claims.iat {
 | 
				
			||||||
 | 
					                log::error!("exp shall not be smaller than iat!");
 | 
				
			||||||
 | 
					                return Err(ErrorBadRequest("exp shall not be smaller than iat!"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if claims.exp - claims.iat > 1800 {
 | 
				
			||||||
 | 
					                log::error!("JWT shall not be valid more than 30 minutes!");
 | 
				
			||||||
 | 
					                return Err(ErrorBadRequest(
 | 
				
			||||||
 | 
					                    "JWT shall not be valid more than 30 minutes!",
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if claims.path != req.path() {
 | 
				
			||||||
 | 
					                log::error!("JWT path mismatch!");
 | 
				
			||||||
 | 
					                return Err(ErrorBadRequest("JWT path mismatch!"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if claims.verb.as_method() != req.method() {
 | 
				
			||||||
 | 
					                log::error!("JWT method mismatch!");
 | 
				
			||||||
 | 
					                return Err(ErrorBadRequest("JWT method mismatch!"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // TODO : check if route is authorized with token
 | 
				
			||||||
 | 
					            // TODO : check for ip restriction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // TODO : manually validate all checks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if token.should_update_last_activity() {
 | 
				
			||||||
 | 
					                // TODO : update last activity
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(ApiAuthExtractor { token, claims })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,2 +1,3 @@
 | 
				
			|||||||
 | 
					pub mod api_auth_extractor;
 | 
				
			||||||
pub mod auth_extractor;
 | 
					pub mod auth_extractor;
 | 
				
			||||||
pub mod local_auth_extractor;
 | 
					pub mod local_auth_extractor;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ use std::rc::Rc;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::app_config::AppConfig;
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
use crate::constants;
 | 
					use crate::constants;
 | 
				
			||||||
 | 
					use crate::extractors::api_auth_extractor::ApiAuthExtractor;
 | 
				
			||||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
					use crate::extractors::auth_extractor::AuthExtractor;
 | 
				
			||||||
use actix_web::body::EitherBody;
 | 
					use actix_web::body::EitherBody;
 | 
				
			||||||
use actix_web::dev::Payload;
 | 
					use actix_web::dev::Payload;
 | 
				
			||||||
@@ -68,8 +69,28 @@ where
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            let auth_disabled = AppConfig::get().unsecure_disable_auth;
 | 
					            let auth_disabled = AppConfig::get().unsecure_disable_auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Check authentication, if required
 | 
					            // Check API authentication
 | 
				
			||||||
            if !auth_disabled
 | 
					            if req.headers().get("x-token-id").is_some() {
 | 
				
			||||||
 | 
					                let auth =
 | 
				
			||||||
 | 
					                    match ApiAuthExtractor::from_request(req.request(), &mut Payload::None).await {
 | 
				
			||||||
 | 
					                        Ok(auth) => auth,
 | 
				
			||||||
 | 
					                        Err(e) => {
 | 
				
			||||||
 | 
					                            log::error!(
 | 
				
			||||||
 | 
					                            "Failed to extract API authentication information from request! {e}"
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                            return Ok(req
 | 
				
			||||||
 | 
					                                .into_response(HttpResponse::PreconditionFailed().finish())
 | 
				
			||||||
 | 
					                                .map_into_right_body());
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                log::info!(
 | 
				
			||||||
 | 
					                    "Using API token '{}' to perform the request",
 | 
				
			||||||
 | 
					                    auth.token.name
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // Check user authentication, if required
 | 
				
			||||||
 | 
					            else if !auth_disabled
 | 
				
			||||||
                && !constants::ROUTES_WITHOUT_AUTH.contains(&req.path())
 | 
					                && !constants::ROUTES_WITHOUT_AUTH.contains(&req.path())
 | 
				
			||||||
                && req.path().starts_with("/api/")
 | 
					                && req.path().starts_with("/api/")
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user