Skip to content

Commit c04df74

Browse files
committed
Merge #1367: New API feature: allow API clients to authenticate via authentication header
34f2f43 refactor: [#727] use the Authentication header in the API client (Jose Celano) 084beb2 feat: [#727] allow to authenticate API via authentication header (Jose Celano) Pull request description: The API allows 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 an `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, etc. For now, it's only optional and recommendable. It could be mandatory in future major API versions. The API client uses by default the `Authentication Header`. It could be a breaking change if you use the newer client witn an old API that does not support it. However we have not released any crate for the API client yet. And [we are still using a different client in the Index](torrust/torrust-index#806). ACKs for top commit: josecelano: ACK 34f2f43 Tree-SHA512: 94e83465f0f105200ea4257aa9a8e1f15b810410fd421e30f286cbea4bd47f3917a83088337ca6608f572f828f82f5a90aa18298763821c1c5e0da7e02c7ea6a
2 parents 6a22b1e + 34f2f43 commit c04df74

File tree

3 files changed

+424
-121
lines changed

3 files changed

+424
-121
lines changed

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

+80-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,29 @@ 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 = match extract_bearer_token_from_header(&request) {
77+
Ok(token) => token,
78+
Err(err) => return err.into_response(),
79+
};
80+
81+
let token_from_get_param = params.token.clone();
82+
83+
let provided_tokens = (token_from_header, token_from_get_param);
84+
85+
let token = match provided_tokens {
86+
(Some(token_from_header), Some(_token_from_get_param)) => token_from_header,
87+
(Some(token_from_header), None) => token_from_header,
88+
(None, Some(token_from_get_param)) => token_from_get_param,
89+
(None, None) => return AuthError::Unauthorized.into_response(),
5690
};
5791

5892
if !authenticate(&token, &state.access_tokens) {
@@ -62,18 +96,50 @@ pub async fn auth(
6296
next.run(request).await
6397
}
6498

99+
fn extract_bearer_token_from_header(request: &Request<axum::body::Body>) -> Result<Option<String>, AuthError> {
100+
let headers = request.headers();
101+
102+
let header_value = headers
103+
.get(axum::http::header::AUTHORIZATION)
104+
.and_then(|header_value| header_value.to_str().ok());
105+
106+
match header_value {
107+
None => Ok(None),
108+
Some(header_value) => {
109+
if header_value == AUTH_BEARER_TOKEN_HEADER_PREFIX {
110+
// Empty token
111+
return Ok(Some(String::new()));
112+
}
113+
114+
if !header_value.starts_with(&format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} ").to_string()) {
115+
// Invalid token type. Missing "Bearer" prefix.
116+
return Err(AuthError::UnknownTokenProvided);
117+
}
118+
119+
Ok(header_value
120+
.strip_prefix(&format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} ").to_string())
121+
.map(std::string::ToString::to_string))
122+
}
123+
}
124+
}
125+
65126
enum AuthError {
66127
/// Missing token for authentication.
67128
Unauthorized,
129+
68130
/// Token was provided but it is not valid.
69131
TokenNotValid,
132+
133+
/// Token was provided but it is not in a format that the server can't understands.
134+
UnknownTokenProvided,
70135
}
71136

72137
impl IntoResponse for AuthError {
73138
fn into_response(self) -> Response {
74139
match self {
75140
AuthError::Unauthorized => unauthorized_response(),
76141
AuthError::TokenNotValid => token_not_valid_response(),
142+
AuthError::UnknownTokenProvided => unknown_auth_data_provided_response(),
77143
}
78144
}
79145
}
@@ -93,3 +159,12 @@ pub fn unauthorized_response() -> Response {
93159
pub fn token_not_valid_response() -> Response {
94160
unhandled_rejection_response("token not valid".to_string())
95161
}
162+
163+
/// `500` error response when the provided token type is not valid.
164+
///
165+
/// The client has provided authentication information that the server does not
166+
/// understand.
167+
#[must_use]
168+
pub fn unknown_auth_data_provided_response() -> Response {
169+
unhandled_rejection_response("unknown token provided".to_string())
170+
}

0 commit comments

Comments
 (0)