Skip to main content

kellnr_auth/
auth_req_token.rs

1use axum::RequestPartsExt;
2use axum::body::Body;
3use axum::extract::{Request, State};
4use axum::http::HeaderValue;
5use axum::middleware::Next;
6use axum::response::Response;
7use tracing::warn;
8
9use crate::token::Token;
10
11/// Middleware that checks if a cargo token is provided when `settings.registry.auth_required` is `true`.
12///
13/// If the user is not logged in, a 401 is returned.
14///
15/// Note: For endpoints that should accept both cargo tokens (CLI) and session cookies (Web UI),
16/// use `token_or_session_auth_when_required` instead.
17pub async fn cargo_auth_when_required(
18    State(state): State<kellnr_appstate::AppStateData>,
19    request: Request,
20    next: Next,
21) -> Response {
22    // Do not expose publicly /config.json even if auth_required is true, cargo and other registries
23    // are expected to retry with authentication if they got a 401 status code.
24    // See:
25    // - https://github.com/kellnr/kellnr/pull/773#discussion_r2300752458
26    // - https://doc.rust-lang.org/cargo/reference/registry-index.html#sparse-authentication
27    if !state.settings.registry.auth_required {
28        // If auth_required is not true, pass through.
29        return next.run(request).await;
30    }
31
32    let token = Token::from_header(request.headers(), &state).await;
33
34    match token {
35        Ok(_) => next.run(request).await,
36        Err(status) => {
37            // Forge the response to handle www-authenticate header.
38            // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/WWW-Authenticate
39            warn!("Authentication required, but failed: {status}");
40            let mut response = Response::new(Body::empty());
41
42            (*response.status_mut()) = status;
43            response.headers_mut().insert(
44                "WWW-Authenticate",
45                HeaderValue::from_static("Basic, Bearer"),
46            );
47
48            response
49        }
50    }
51}
52
53/// Middleware that allows *either* cargo token auth (Authorization header) *or* web session auth
54/// (signed session cookie) when `settings.registry.auth_required` is `true`.
55///
56/// This is intended for endpoints that are used by both the cargo CLI and the web UI.
57pub async fn token_or_session_auth_when_required(
58    State(state): State<kellnr_appstate::AppStateData>,
59    request: Request,
60    next: Next,
61) -> Response {
62    if !state.settings.registry.auth_required {
63        return next.run(request).await;
64    }
65
66    // 1) Try cargo token auth.
67    if Token::from_header(request.headers(), &state).await.is_ok() {
68        return next.run(request).await;
69    }
70
71    // 2) Try session cookie auth (signed cookie).
72    // Note: extracting `PrivateCookieJar` from a full request consumes the request.
73    // We therefore extract it from parts and then reconstruct the request.
74    let (mut parts, body) = request.into_parts();
75
76    let jar: axum_extra::extract::PrivateCookieJar = match parts.extract_with_state(&state).await {
77        Ok(j) => j,
78        Err(_) => return unauthorized_www_authenticate(),
79    };
80
81    let Some(cookie) = jar.get(kellnr_settings::constants::COOKIE_SESSION_ID) else {
82        return unauthorized_www_authenticate();
83    };
84
85    if state.db.validate_session(cookie.value()).await.is_ok() {
86        let request = Request::from_parts(parts, body);
87        return next.run(request).await;
88    }
89
90    unauthorized_www_authenticate()
91}
92
93fn unauthorized_www_authenticate() -> Response {
94    // Forge the response to handle www-authenticate header.
95    // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/WWW-Authenticate
96    let mut response = Response::new(Body::empty());
97    *response.status_mut() = axum::http::StatusCode::UNAUTHORIZED;
98    response.headers_mut().insert(
99        "WWW-Authenticate",
100        HeaderValue::from_static("Basic, Bearer"),
101    );
102    response
103}
104
105#[cfg(test)]
106mod test {
107
108    use std::sync::Arc;
109
110    use axum::body::Body;
111    use axum::http::{Request, StatusCode, header};
112    use axum::routing::get;
113    use axum::{Router, middleware};
114    use kellnr_appstate::AppStateData;
115    use kellnr_db::User;
116    use kellnr_db::error::DbError;
117    use kellnr_db::mock::MockDb;
118    use kellnr_settings::Settings;
119    use mockall::predicate::*;
120    use tower::ServiceExt;
121
122    use super::*;
123
124    #[tokio::test]
125    async fn no_auth_required() {
126        let settings = test_settings(false);
127        let r = app(settings)
128            .oneshot(Request::get("/test").body(Body::empty()).unwrap())
129            .await
130            .unwrap();
131
132        assert_eq!(r.status(), StatusCode::OK);
133    }
134
135    #[tokio::test]
136    async fn auth_required_but_not_provided() {
137        let settings = test_settings(true);
138        let r = app(settings)
139            .oneshot(Request::get("/test").body(Body::empty()).unwrap())
140            .await
141            .unwrap();
142
143        assert_eq!(r.status(), StatusCode::UNAUTHORIZED);
144    }
145
146    #[tokio::test]
147    async fn auth_required_but_wrong_token_provided() {
148        let settings = test_settings(true);
149        let r = app(settings)
150            .oneshot(
151                Request::get("/test")
152                    .header(header::AUTHORIZATION, "wrong_token")
153                    .body(Body::empty())
154                    .unwrap(),
155            )
156            .await
157            .unwrap();
158
159        assert_eq!(r.status(), StatusCode::FORBIDDEN);
160    }
161
162    #[tokio::test]
163    async fn auth_required_and_right_token_provided() {
164        let settings = test_settings(true);
165        let r = app(settings)
166            .oneshot(
167                Request::get("/test")
168                    .header(header::AUTHORIZATION, "token")
169                    .body(Body::empty())
170                    .unwrap(),
171            )
172            .await
173            .unwrap();
174
175        assert_eq!(r.status(), StatusCode::OK);
176    }
177
178    pub async fn test_auth_req_token() -> StatusCode {
179        StatusCode::OK
180    }
181
182    fn test_settings(auth_required: bool) -> Settings {
183        Settings {
184            registry: kellnr_settings::Registry {
185                auth_required,
186                ..kellnr_settings::Registry::default()
187            },
188            ..Settings::default()
189        }
190    }
191
192    fn app(settings: Settings) -> Router {
193        let mut mock_db = MockDb::new();
194        mock_db
195            .expect_get_user_from_token()
196            .with(eq("token"))
197            .returning(move |_| {
198                Ok(User {
199                    id: 0,
200                    name: "user".to_string(),
201                    pwd: String::new(),
202                    salt: String::new(),
203                    is_admin: false,
204                    is_read_only: false,
205                    created: String::new(),
206                })
207            });
208        mock_db
209            .expect_get_user_from_token()
210            .with(eq("wrong_token"))
211            .returning(move |_| Err(DbError::UserNotFound("user".to_string())));
212
213        let state = AppStateData {
214            db: Arc::new(mock_db),
215            settings: Arc::new(settings),
216            ..kellnr_appstate::test_state()
217        };
218
219        Router::new()
220            .route("/test", get(test_auth_req_token))
221            .route_layer(middleware::from_fn_with_state(
222                state.clone(),
223                cargo_auth_when_required,
224            ))
225            .with_state(state)
226    }
227}
228
229#[cfg(test)]
230mod auth_middleware_tests {
231    use std::sync::Arc;
232
233    use axum::Router;
234    use axum::body::Body;
235    use axum::http::StatusCode;
236    use axum::middleware::from_fn_with_state;
237    use axum::routing::get;
238    use hyper::{Request, header};
239    use kellnr_appstate::AppStateData;
240    use kellnr_db::DbProvider;
241    use kellnr_db::error::DbError;
242    use kellnr_db::mock::MockDb;
243    use kellnr_settings::Settings;
244    use mockall::predicate::*;
245    use tower::ServiceExt;
246
247    use super::*;
248
249    fn app_required_auth(db: Arc<dyn DbProvider>) -> Router {
250        let settings = Settings::default();
251        let state = AppStateData {
252            db,
253            settings: Arc::new(Settings {
254                registry: kellnr_settings::Registry {
255                    auth_required: true,
256                    ..kellnr_settings::Registry::default()
257                },
258                ..settings
259            }),
260            ..kellnr_appstate::test_state()
261        };
262
263        Router::new()
264            .route("/guarded", get(StatusCode::OK))
265            .route_layer(from_fn_with_state(state.clone(), cargo_auth_when_required))
266            .route("/not_guarded", get(StatusCode::OK))
267            .with_state(state)
268    }
269
270    fn app_not_required_auth(db: Arc<dyn DbProvider>) -> Router {
271        let settings = Settings::default();
272        let state = AppStateData {
273            db,
274            settings: Arc::new(settings),
275            ..kellnr_appstate::test_state()
276        };
277        Router::new()
278            .route("/guarded", get(StatusCode::OK))
279            .route_layer(from_fn_with_state(state.clone(), cargo_auth_when_required))
280            .with_state(state)
281    }
282
283    type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
284
285    #[tokio::test]
286    async fn guarded_route_with_invalid_token() -> Result {
287        let mut mock_db = MockDb::new();
288        mock_db
289            .expect_get_user_from_token()
290            .with(eq("1234"))
291            .returning(|_st| Err(DbError::UserNotFound("1234".to_owned())));
292
293        let r = app_required_auth(Arc::new(mock_db))
294            .oneshot(
295                Request::get("/guarded")
296                    .header(header::AUTHORIZATION, "1234")
297                    .body(Body::empty())?,
298            )
299            .await?;
300        assert_eq!(r.status(), StatusCode::FORBIDDEN);
301
302        Ok(())
303    }
304
305    #[tokio::test]
306    async fn guarded_route_without_token() -> Result {
307        let mock_db = MockDb::new();
308
309        let r = app_required_auth(Arc::new(mock_db))
310            .oneshot(Request::get("/guarded").body(Body::empty())?)
311            .await?;
312        assert_eq!(r.status(), StatusCode::UNAUTHORIZED);
313
314        Ok(())
315    }
316
317    #[tokio::test]
318    async fn not_guarded_route_without_token() -> Result {
319        let mock_db = MockDb::new();
320
321        let r = app_required_auth(Arc::new(mock_db))
322            .oneshot(Request::get("/not_guarded").body(Body::empty())?)
323            .await?;
324        assert_eq!(r.status(), StatusCode::OK);
325
326        Ok(())
327    }
328
329    #[tokio::test]
330    async fn app_not_required_auth_with_guarded_route() -> Result {
331        let mock_db = MockDb::new();
332
333        let r = app_not_required_auth(Arc::new(mock_db))
334            .oneshot(Request::get("/guarded").body(Body::empty())?)
335            .await?;
336        assert_eq!(r.status(), StatusCode::OK);
337
338        Ok(())
339    }
340}