Pierre HUBERT
c7de64cc02
All checks were successful
continuous-integration/drone/push Build is passing
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>
152 lines
5.2 KiB
Rust
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 })
|
|
})
|
|
}
|
|
}
|