124 lines
4.1 KiB
Rust
124 lines
4.1 KiB
Rust
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 })
|
|
})
|
|
}
|
|
}
|