Skip to content

Commit 3f915a2

Browse files
committed
feat: [#727] allow to authenticate API via authentication header
The API allos client authentication via a `token` parameter in the URL query: ```console curl http://0.0.0.0:1212/api/v1/stats?token=MyAccessToken | jq ``` Now it's also possible to do it via Authentication Header: ```console curl -H "Authorization: Bearer MyAccessToken" http://0.0.0.0:1212/api/v1/stats | jq ``` This is to avoid leaking the token in logs, proxies, etc. For now, it's only optional and recommendable. It could be mandatory in future major API versions.
1 parent 6a22b1e commit 3f915a2

File tree

3 files changed

+327
-100
lines changed

3 files changed

+327
-100
lines changed

packages/axum-rest-tracker-api-server/src/v1/middlewares/auth.rs

+62-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
//! Authentication middleware for the API.
22
//!
3-
//! It uses a "token" GET param to authenticate the user. URLs must be of the
4-
//! form:
3+
//! It uses a "token" to authenticate the user. The token must be one of the
4+
//! `access_tokens` in the tracker [HTTP API configuration](torrust_tracker_configuration::HttpApi).
5+
//!
6+
//! There are two ways to provide the token:
7+
//!
8+
//! 1. As a `Bearer` token in the `Authorization` header.
9+
//! 2. As a `token` GET param in the URL.
10+
//!
11+
//! Using the `Authorization` header:
12+
//!
13+
//! ```console
14+
//! curl -H "Authorization: Bearer MyAccessToken" http://<host>:<port>/api/v1/<context>
15+
//! ```
16+
//!
17+
//! Using the `token` GET param:
518
//!
619
//! `http://<host>:<port>/api/v1/<context>?token=<token>`.
720
//!
@@ -21,6 +34,12 @@
2134
//! All the tokes have the same permissions, so it is not possible to have
2235
//! different permissions for different tokens. The label is only used to
2336
//! identify the token.
37+
//!
38+
//! NOTICE: The token is not encrypted, so it is recommended to use HTTPS to
39+
//! protect the token from being intercepted.
40+
//!
41+
//! NOTICE: If both the `Authorization` header and the `token` GET param are
42+
//! provided, the `Authorization` header will be used.
2443
use std::sync::Arc;
2544

2645
use axum::extract::{self};
@@ -32,6 +51,8 @@ use torrust_tracker_configuration::AccessTokens;
3251

3352
use crate::v1::responses::unhandled_rejection_response;
3453

54+
pub const AUTH_BEARER_TOKEN_HEADER_PREFIX: &str = "Bearer";
55+
3556
/// Container for the `token` extracted from the query params.
3657
#[derive(Deserialize, Debug)]
3758
pub struct QueryParams {
@@ -43,16 +64,28 @@ pub struct State {
4364
pub access_tokens: Arc<AccessTokens>,
4465
}
4566

46-
/// Middleware for authentication using a "token" GET param.
67+
/// Middleware for authentication.
68+
///
4769
/// The token must be one of the tokens in the tracker [HTTP API configuration](torrust_tracker_configuration::HttpApi).
4870
pub async fn auth(
4971
extract::State(state): extract::State<State>,
5072
extract::Query(params): extract::Query<QueryParams>,
5173
request: Request<axum::body::Body>,
5274
next: Next,
5375
) -> Response {
54-
let Some(token) = params.token else {
55-
return AuthError::Unauthorized.into_response();
76+
let token_from_header = extract_bearer_token_from_header(&request);
77+
78+
let token_from_get_param = params.token.clone();
79+
80+
let provided_tokens = (token_from_header, token_from_get_param);
81+
82+
println!("Provided tokens: {provided_tokens:?}");
83+
84+
let token = match provided_tokens {
85+
(Some(token_from_header), Some(_token_from_get_param)) => token_from_header,
86+
(Some(token_from_header), None) => token_from_header,
87+
(None, Some(token_from_get_param)) => token_from_get_param,
88+
(None, None) => return AuthError::Unauthorized.into_response(),
5689
};
5790

5891
if !authenticate(&token, &state.access_tokens) {
@@ -62,9 +95,33 @@ pub async fn auth(
6295
next.run(request).await
6396
}
6497

98+
fn extract_bearer_token_from_header(request: &Request<axum::body::Body>) -> Option<String> {
99+
let headers = request.headers();
100+
headers
101+
.get(axum::http::header::AUTHORIZATION)
102+
.and_then(|header_value| header_value.to_str().ok())
103+
.and_then(|header_str| {
104+
if (header_str) == AUTH_BEARER_TOKEN_HEADER_PREFIX {
105+
// Empty token
106+
return Some(String::new());
107+
}
108+
109+
if !header_str.starts_with(&format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} ").to_string()) {
110+
// Invalid token type. Missing "Bearer" prefix.
111+
// We return the header as is, so the caller can decide what to do.
112+
return Some(header_str.to_owned());
113+
}
114+
115+
header_str
116+
.strip_prefix(&format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} ").to_string())
117+
.map(std::string::ToString::to_string)
118+
})
119+
}
120+
65121
enum AuthError {
66122
/// Missing token for authentication.
67123
Unauthorized,
124+
68125
/// Token was provided but it is not valid.
69126
TokenNotValid,
70127
}

0 commit comments

Comments
 (0)