mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-03-27 03:03:09 +00:00
backend: harden auth token validation
This commit is contained in:
parent
070257597e
commit
33311c51c8
3 changed files with 121 additions and 10 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -34,10 +35,14 @@ pub fn create_token(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_token(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
pub fn verify_token(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||||
|
let mut validation = Validation::new(Algorithm::HS256);
|
||||||
|
validation.leeway = 30;
|
||||||
|
validation.required_spec_claims = HashSet::from(["exp".to_string(), "sub".to_string()]);
|
||||||
|
|
||||||
let token_data = decode::<Claims>(
|
let token_data = decode::<Claims>(
|
||||||
token,
|
token,
|
||||||
&DecodingKey::from_secret(secret.as_bytes()),
|
&DecodingKey::from_secret(secret.as_bytes()),
|
||||||
&Validation::default(),
|
&validation,
|
||||||
)?;
|
)?;
|
||||||
Ok(token_data.claims)
|
Ok(token_data.claims)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::FromRequestParts,
|
extract::FromRequestParts,
|
||||||
|
http::header,
|
||||||
http::{request::Parts, StatusCode},
|
http::{request::Parts, StatusCode},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -8,6 +9,7 @@ use uuid::Uuid;
|
||||||
use super::jwt::{verify_token, Claims};
|
use super::jwt::{verify_token, Claims};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AuthUser {
|
pub struct AuthUser {
|
||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
@ -22,23 +24,39 @@ where
|
||||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
let auth_header = parts
|
let auth_header = parts
|
||||||
.headers
|
.headers
|
||||||
.get("Authorization")
|
.get(header::AUTHORIZATION)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.ok_or((StatusCode::UNAUTHORIZED, "Missing authorization header"))?;
|
.ok_or((StatusCode::UNAUTHORIZED, "Missing authorization header"))?;
|
||||||
|
|
||||||
let token = auth_header.strip_prefix("Bearer ").ok_or((
|
let mut pieces = auth_header.split_whitespace();
|
||||||
|
let scheme = pieces.next().ok_or((
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"Invalid authorization header format",
|
"Invalid authorization header format",
|
||||||
))?;
|
))?;
|
||||||
|
let token = pieces.next().ok_or((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid authorization header format",
|
||||||
|
))?;
|
||||||
|
if pieces.next().is_some() || !scheme.eq_ignore_ascii_case("bearer") {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"Invalid authorization header format",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let secret = parts
|
let config = parts
|
||||||
.extensions
|
.extensions
|
||||||
.get::<Arc<Config>>()
|
.get::<Arc<Config>>()
|
||||||
.map(|c| c.jwt_secret.clone())
|
.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Auth config missing"))?;
|
||||||
.or_else(|| std::env::var("JWT_SECRET").ok())
|
|
||||||
.unwrap_or_else(|| "dev-secret-change-in-production".to_string());
|
|
||||||
|
|
||||||
let claims: Claims = verify_token(token, &secret)
|
if config.jwt_secret.trim().is_empty() {
|
||||||
|
return Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"JWT secret not configured",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let claims: Claims = verify_token(token, &config.jwt_secret)
|
||||||
.map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid token"))?;
|
.map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid token"))?;
|
||||||
|
|
||||||
Ok(AuthUser {
|
Ok(AuthUser {
|
||||||
|
|
@ -47,3 +65,83 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::AuthUser;
|
||||||
|
use crate::auth::create_token;
|
||||||
|
use crate::config::Config;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::FromRequestParts;
|
||||||
|
use axum::http::Request;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn parts_with_auth(
|
||||||
|
auth: Option<&str>,
|
||||||
|
config: Option<Arc<Config>>,
|
||||||
|
) -> axum::http::request::Parts {
|
||||||
|
let mut req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
||||||
|
if let Some(auth) = auth {
|
||||||
|
req.headers_mut()
|
||||||
|
.insert(axum::http::header::AUTHORIZATION, auth.parse().unwrap());
|
||||||
|
}
|
||||||
|
if let Some(config) = config {
|
||||||
|
req.extensions_mut().insert(config);
|
||||||
|
}
|
||||||
|
let (parts, _) = req.into_parts();
|
||||||
|
parts
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rejects_missing_auth_header() {
|
||||||
|
let config = Arc::new(Config {
|
||||||
|
jwt_secret: "secret".to_string(),
|
||||||
|
..Config::default()
|
||||||
|
});
|
||||||
|
let mut parts = parts_with_auth(None, Some(config));
|
||||||
|
let err = AuthUser::from_request_parts(&mut parts, &())
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.0, axum::http::StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn accepts_bearer_case_insensitive_and_whitespace() {
|
||||||
|
let config = Arc::new(Config {
|
||||||
|
jwt_secret: "secret".to_string(),
|
||||||
|
..Config::default()
|
||||||
|
});
|
||||||
|
let token = create_token(Uuid::new_v4(), "alice", &config.jwt_secret).unwrap();
|
||||||
|
let auth = format!(" bEaReR {token} ");
|
||||||
|
let mut parts = parts_with_auth(Some(&auth), Some(config));
|
||||||
|
let user = AuthUser::from_request_parts(&mut parts, &()).await.unwrap();
|
||||||
|
assert_eq!(user.username, "alice");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rejects_non_bearer_scheme() {
|
||||||
|
let config = Arc::new(Config {
|
||||||
|
jwt_secret: "secret".to_string(),
|
||||||
|
..Config::default()
|
||||||
|
});
|
||||||
|
let token = create_token(Uuid::new_v4(), "alice", &config.jwt_secret).unwrap();
|
||||||
|
let auth = format!("Token {token}");
|
||||||
|
let mut parts = parts_with_auth(Some(&auth), Some(config));
|
||||||
|
let err = AuthUser::from_request_parts(&mut parts, &())
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.0, axum::http::StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn errors_when_config_missing() {
|
||||||
|
let token = create_token(Uuid::new_v4(), "alice", "secret").unwrap();
|
||||||
|
let auth = format!("Bearer {token}");
|
||||||
|
let mut parts = parts_with_auth(Some(&auth), None);
|
||||||
|
let err = AuthUser::from_request_parts(&mut parts, &())
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.0, axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,15 @@ fn parse_ip_from_connect_info<B>(request: &Request<B>) -> Option<IpAddr> {
|
||||||
|
|
||||||
fn parse_user_from_auth(headers: &HeaderMap, secret: &str) -> Option<Uuid> {
|
fn parse_user_from_auth(headers: &HeaderMap, secret: &str) -> Option<Uuid> {
|
||||||
let auth = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
|
let auth = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
|
||||||
let token = auth.strip_prefix("Bearer ")?;
|
let mut pieces = auth.split_whitespace();
|
||||||
|
let scheme = pieces.next()?;
|
||||||
|
let token = pieces.next()?;
|
||||||
|
if pieces.next().is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !scheme.eq_ignore_ascii_case("bearer") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
verify_token(token, secret).ok().map(|c| c.sub)
|
verify_token(token, secret).ok().map(|c| c.sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue