Start to support token authentication

This commit is contained in:
2025-03-20 20:38:09 +01:00
parent 7fe950488f
commit c6f7830d9d
6 changed files with 644 additions and 3 deletions

View File

@ -6,6 +6,9 @@ pub const TOKENS_LEN: usize = 50;
/// Header used to authenticate API requests made using a token
pub const API_TOKEN_HEADER: &str = "X-Auth-Token";
/// Max token validity
pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60;
/// Session-specific constants
pub mod sessions {
/// OpenID auth session state key

View File

@ -1,11 +1,24 @@
use crate::app_config::AppConfig;
use crate::constants;
use crate::extractors::money_session::MoneySession;
use crate::models::tokens::Token;
use crate::models::tokens::{Token, TokenID};
use crate::models::users::{User, UserID};
use crate::services::users_service;
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 {
@ -36,7 +49,125 @@ impl FromRequest for AuthExtractor {
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/attachment/") && token.right_attachment)
|| (uri.starts_with("/api/auth/") && token.right_auth);
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!",
));
};
// TODO : update token last activity & expiration
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| {

View File

@ -3,11 +3,20 @@ use crate::schema::*;
use crate::utils::time_utils::time;
use diesel::prelude::*;
use std::cmp::min;
use std::num::ParseIntError;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct TokenID(pub i32);
impl FromStr for TokenID {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse()?))
}
}
#[derive(Default, Queryable, Debug, Clone, serde::Serialize)]
pub struct Token {
id: i32,