koan_server/auth/
middleware.rs1use std::sync::Arc;
7
8use axum::extract::Request;
9use axum::http::{StatusCode, header};
10use axum::middleware::Next;
11use axum::response::{IntoResponse, Response};
12
13use koan_core::auth::{self, Role};
14
15use super::AuthUser;
16
17#[derive(Clone)]
19pub struct AuthState {
20 pub public_pem: Arc<Vec<u8>>,
22 pub auth_enabled: bool,
24 pub introspection_key: Option<Arc<String>>,
27}
28
29pub async fn auth_middleware(
34 axum::extract::State(state): axum::extract::State<AuthState>,
35 mut request: Request,
36 next: Next,
37) -> Response {
38 if !state.auth_enabled {
39 request.extensions_mut().insert(AuthUser::anonymous_admin());
40 return next.run(request).await;
41 }
42
43 if let Some(ref expected_key) = state.introspection_key
45 && let Some(provided) = request
46 .headers()
47 .get("X-Introspection-Key")
48 .and_then(|v| v.to_str().ok())
49 && provided == expected_key.as_str()
50 {
51 request.extensions_mut().insert(AuthUser::anonymous_admin());
52 return next.run(request).await;
53 }
54
55 let token = request
58 .headers()
59 .get(axum::http::header::COOKIE)
60 .and_then(|v| v.to_str().ok())
61 .and_then(|cookies| {
62 cookies
63 .split(';')
64 .find_map(|c| c.trim().strip_prefix("koan_access=").map(String::from))
65 })
66 .or_else(|| {
67 request
68 .headers()
69 .get(header::AUTHORIZATION)
70 .and_then(|v| v.to_str().ok())
71 .and_then(|v| v.strip_prefix("Bearer "))
72 .map(String::from)
73 })
74 .or_else(|| {
75 request.uri().query().and_then(|q| {
77 q.split('&')
78 .find_map(|pair| pair.strip_prefix("token=").map(String::from))
79 })
80 });
81
82 let Some(token) = token else {
83 return (
84 StatusCode::UNAUTHORIZED,
85 [("WWW-Authenticate", "Bearer")],
86 "missing or invalid Authorization header",
87 )
88 .into_response();
89 };
90
91 match auth::validate_access_token(&state.public_pem, &token) {
92 Ok(claims) => {
93 let role = claims.role.parse().unwrap_or(Role::Readonly);
94 let user = AuthUser {
95 user_id: claims.sub,
96 username: claims.username,
97 role,
98 };
99 request.extensions_mut().insert(user);
100 next.run(request).await
101 }
102 Err(_) => (
103 StatusCode::UNAUTHORIZED,
104 [("WWW-Authenticate", "Bearer")],
105 "invalid or expired token",
106 )
107 .into_response(),
108 }
109}