VirtWeb/virtweb_backend/src/extractors/api_auth_extractor.rs
Pierre HUBERT c7de64cc02
All checks were successful
continuous-integration/drone/push Build is passing
Add API tokens support (#9)
Make it possible to create token authorized to query predetermined set of routes.

Reviewed-on: #9
Co-authored-by: Pierre HUBERT <pierre.git@communiquons.org>
Co-committed-by: Pierre HUBERT <pierre.git@communiquons.org>
2024-04-23 17:04:43 +00:00

152 lines
5.2 KiB
Rust

use crate::api_tokens::{Token, TokenID, TokenVerb};
use crate::api_tokens;
use crate::utils::time_utils::time;
use actix_remote_ip::RemoteIP;
use actix_web::dev::Payload;
use actix_web::error::{ErrorBadRequest, ErrorUnauthorized};
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();
let remote_ip = match RemoteIP::from_request(&req, payload).into_inner() {
Ok(ip) => ip,
Err(e) => return Box::pin(async { Err(e) }),
};
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 token
.pub_key
.as_ref()
.expect("All tokens shall have public key!")
.validate_jwt::<TokenClaims>(&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!"));
}
if !token.rights.contains(claims.verb, req.path()) {
log::error!(
"Attempt to use a token for an unauthorized route! (token_id={})",
token.id.0
);
return Err(ErrorUnauthorized(
"Token cannot be used to query this route!",
));
}
if let Some(ip) = token.ip_restriction {
if !ip.contains(remote_ip.0) {
log::error!(
"Attempt to use a token for an unauthorized IP! {remote_ip:?} token_id={}",
token.id.0
);
return Err(ErrorUnauthorized("Token cannot be used from this IP!"));
}
}
if token.should_update_last_activity() {
if let Err(e) = api_tokens::refresh_last_used(token.id).await {
log::error!("Could not update token last activity! {e}");
return Err(ErrorBadRequest("Couldn't refresh token last activity!"));
}
}
Ok(ApiAuthExtractor { token, claims })
})
}
}