mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-11 03:40:05 +00:00
refactor(auth): simplify translation middleware and jwt_auth
- drop per-request AuthMode clone in auth_translation_middleware - remove dead CredentialSource enum and VerifiedAuth field - collapse cookie_key_error + jwt_key_error into session_secret_key_error - use header::AUTHORIZATION constant in bearer_token - remove narrating doc comments on extractor structs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab6c7f3e8c
commit
7b28b12f49
2 changed files with 38 additions and 65 deletions
|
|
@ -35,30 +35,26 @@ pub(crate) async fn auth_translation_middleware(
|
|||
mut req: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let auth_mode = req
|
||||
let translated = match req
|
||||
.extensions()
|
||||
.get::<AuthMode>()
|
||||
.expect("AuthMode extension must be added to the router")
|
||||
.clone();
|
||||
|
||||
let AuthMode::Enabled(config) = auth_mode else {
|
||||
return next.run(req).await;
|
||||
{
|
||||
AuthMode::Disabled => return next.run(req).await,
|
||||
AuthMode::Enabled(config) => {
|
||||
if let Some(auth_header) = req.headers().get(header::AUTHORIZATION) {
|
||||
auth_header
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.and_then(|token| translate_bearer_token(token, config))
|
||||
} else {
|
||||
translate_session_cookie(req.headers(), state.as_ref(), config)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(auth_header) = req.headers().get(header::AUTHORIZATION) {
|
||||
if let Some(token) = auth_header
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.and_then(|token| translate_bearer_token(token, &config))
|
||||
{
|
||||
req.headers_mut()
|
||||
.insert(header::AUTHORIZATION, bearer_header_value(&token));
|
||||
}
|
||||
return next.run(req).await;
|
||||
}
|
||||
|
||||
if let Some(token) = translate_session_cookie(req.headers(), state.as_ref(), &config) {
|
||||
if let Some(token) = translated {
|
||||
req.headers_mut()
|
||||
.insert(header::AUTHORIZATION, bearer_header_value(&token));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::{Result, anyhow};
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::header;
|
||||
use axum::http::request::Parts;
|
||||
use fabro_types::settings::{ServerAuthMethod, ServerSettings as ResolvedServerSettings};
|
||||
use fabro_types::{IdpIdentity, RunAuthMethod};
|
||||
|
|
@ -14,21 +15,15 @@ use crate::error::ApiError;
|
|||
type HmacSha256 = Hmac<Sha256>;
|
||||
const DEV_TOKEN_COMPARE_KEY: &[u8] = b"fabro-dev-token-compare-key";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CredentialSource {
|
||||
JwtAccessToken,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VerifiedAuth {
|
||||
pub login: String,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub avatar_url: String,
|
||||
pub user_url: String,
|
||||
pub auth_method: RunAuthMethod,
|
||||
pub credential_source: CredentialSource,
|
||||
pub identity: Option<IdpIdentity>,
|
||||
pub login: String,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub avatar_url: String,
|
||||
pub user_url: String,
|
||||
pub auth_method: RunAuthMethod,
|
||||
pub identity: Option<IdpIdentity>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -97,7 +92,7 @@ where
|
|||
anyhow!("Fabro server refuses to start: auth is configured but SESSION_SECRET is not set.")
|
||||
})?;
|
||||
if web_enabled {
|
||||
auth::derive_cookie_key(secret.as_bytes()).map_err(|err| cookie_key_error(&err))?;
|
||||
auth::derive_cookie_key(secret.as_bytes()).map_err(|err| session_secret_key_error(&err))?;
|
||||
}
|
||||
|
||||
let dev_token = if methods.contains(&ServerAuthMethod::DevToken) {
|
||||
|
|
@ -116,7 +111,9 @@ where
|
|||
None
|
||||
};
|
||||
|
||||
let jwt_key = Some(auth::derive_jwt_key(secret.as_bytes()).map_err(|err| jwt_key_error(&err))?);
|
||||
let jwt_key = Some(
|
||||
auth::derive_jwt_key(secret.as_bytes()).map_err(|err| session_secret_key_error(&err))?,
|
||||
);
|
||||
let jwt_issuer = Some(resolve_jwt_issuer(settings, &lookup));
|
||||
|
||||
Ok(AuthMode::Enabled(ConfiguredAuth {
|
||||
|
|
@ -150,23 +147,7 @@ where
|
|||
.unwrap_or_else(|| "fabro-server".to_string())
|
||||
}
|
||||
|
||||
fn cookie_key_error(err: &KeyDeriveError) -> anyhow::Error {
|
||||
match err {
|
||||
KeyDeriveError::Empty => {
|
||||
anyhow!(
|
||||
"Fabro server refuses to start: auth is configured but SESSION_SECRET is not set."
|
||||
)
|
||||
}
|
||||
KeyDeriveError::TooShort {
|
||||
got_bytes,
|
||||
min_bytes,
|
||||
} => anyhow!(
|
||||
"Fabro server refuses to start: SESSION_SECRET must be at least {min_bytes} bytes (64 hex characters) when auth is configured. Current length: {got_bytes} bytes."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn jwt_key_error(err: &KeyDeriveError) -> anyhow::Error {
|
||||
fn session_secret_key_error(err: &KeyDeriveError) -> anyhow::Error {
|
||||
match err {
|
||||
KeyDeriveError::Empty => {
|
||||
anyhow!(
|
||||
|
|
@ -205,12 +186,12 @@ fn config_allows_run_auth_method(config: &ConfiguredAuth, method: RunAuthMethod)
|
|||
}
|
||||
|
||||
fn bearer_token(parts: &Parts) -> Option<Result<&str, ApiError>> {
|
||||
let header = parts.headers.get("authorization")?;
|
||||
let Ok(header) = header.to_str() else {
|
||||
let value = parts.headers.get(header::AUTHORIZATION)?;
|
||||
let Ok(value) = value.to_str() else {
|
||||
return Some(Err(ApiError::unauthorized()));
|
||||
};
|
||||
Some(
|
||||
header
|
||||
value
|
||||
.strip_prefix("Bearer ")
|
||||
.ok_or_else(ApiError::unauthorized),
|
||||
)
|
||||
|
|
@ -255,14 +236,13 @@ fn authenticate_jwt_bearer(token: &str, config: &ConfiguredAuth) -> Result<Verif
|
|||
})?;
|
||||
|
||||
Ok(VerifiedAuth {
|
||||
login: claims.login,
|
||||
name: claims.name,
|
||||
email: claims.email,
|
||||
avatar_url: claims.avatar_url,
|
||||
user_url: claims.user_url,
|
||||
auth_method: claims.auth_method,
|
||||
credential_source: CredentialSource::JwtAccessToken,
|
||||
identity: Some(identity),
|
||||
login: claims.login,
|
||||
name: claims.name,
|
||||
email: claims.email,
|
||||
avatar_url: claims.avatar_url,
|
||||
user_url: claims.user_url,
|
||||
auth_method: claims.auth_method,
|
||||
identity: Some(identity),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +297,6 @@ fn authenticate_parts(parts: &Parts) -> Result<Option<VerifiedAuth>, ApiError> {
|
|||
.map(Some)
|
||||
}
|
||||
|
||||
/// Axum extractor that enforces authentication on a route.
|
||||
pub struct AuthenticatedService;
|
||||
|
||||
pub fn authenticate_service_parts(parts: &Parts) -> Result<(), ApiError> {
|
||||
|
|
@ -333,7 +312,6 @@ impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Axum extractor that authenticates and extracts the request subject.
|
||||
pub struct AuthenticatedSubject {
|
||||
pub login: Option<String>,
|
||||
pub name: String,
|
||||
|
|
@ -903,7 +881,6 @@ client_id = "Iv1.test"
|
|||
let auth = authenticate_parts(&parts).unwrap().unwrap();
|
||||
assert_eq!(auth.login, "octocat");
|
||||
assert_eq!(auth.auth_method, RunAuthMethod::Github);
|
||||
assert_eq!(auth.credential_source, CredentialSource::JwtAccessToken);
|
||||
assert_eq!(
|
||||
auth.identity,
|
||||
Some(IdpIdentity::new("https://github.com", "12345").unwrap())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue