From 084beb2ef6f0bdd29bbc74dbc2896a499171eb8f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Mar 2025 11:28:30 +0000 Subject: [PATCH 1/2] 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. --- .../src/v1/middlewares/auth.rs | 85 ++++- .../server/v1/contract/authentication.rs | 325 +++++++++++++----- .../rest-tracker-api-client/src/v1/client.rs | 35 +- 3 files changed, 345 insertions(+), 100 deletions(-) diff --git a/packages/axum-rest-tracker-api-server/src/v1/middlewares/auth.rs b/packages/axum-rest-tracker-api-server/src/v1/middlewares/auth.rs index 2ec046bed..9b5ec2320 100644 --- a/packages/axum-rest-tracker-api-server/src/v1/middlewares/auth.rs +++ b/packages/axum-rest-tracker-api-server/src/v1/middlewares/auth.rs @@ -1,7 +1,20 @@ //! Authentication middleware for the API. //! -//! It uses a "token" GET param to authenticate the user. URLs must be of the -//! form: +//! It uses a "token" to authenticate the user. The token must be one of the +//! `access_tokens` in the tracker [HTTP API configuration](torrust_tracker_configuration::HttpApi). +//! +//! There are two ways to provide the token: +//! +//! 1. As a `Bearer` token in the `Authorization` header. +//! 2. As a `token` GET param in the URL. +//! +//! Using the `Authorization` header: +//! +//! ```console +//! curl -H "Authorization: Bearer MyAccessToken" http://:/api/v1/ +//! ``` +//! +//! Using the `token` GET param: //! //! `http://:/api/v1/?token=`. //! @@ -21,6 +34,12 @@ //! All the tokes have the same permissions, so it is not possible to have //! different permissions for different tokens. The label is only used to //! identify the token. +//! +//! NOTICE: The token is not encrypted, so it is recommended to use HTTPS to +//! protect the token from being intercepted. +//! +//! NOTICE: If both the `Authorization` header and the `token` GET param are +//! provided, the `Authorization` header will be used. use std::sync::Arc; use axum::extract::{self}; @@ -32,6 +51,8 @@ use torrust_tracker_configuration::AccessTokens; use crate::v1::responses::unhandled_rejection_response; +pub const AUTH_BEARER_TOKEN_HEADER_PREFIX: &str = "Bearer"; + /// Container for the `token` extracted from the query params. #[derive(Deserialize, Debug)] pub struct QueryParams { @@ -43,7 +64,8 @@ pub struct State { pub access_tokens: Arc, } -/// Middleware for authentication using a "token" GET param. +/// Middleware for authentication. +/// /// The token must be one of the tokens in the tracker [HTTP API configuration](torrust_tracker_configuration::HttpApi). pub async fn auth( extract::State(state): extract::State, @@ -51,8 +73,20 @@ pub async fn auth( request: Request, next: Next, ) -> Response { - let Some(token) = params.token else { - return AuthError::Unauthorized.into_response(); + let token_from_header = match extract_bearer_token_from_header(&request) { + Ok(token) => token, + Err(err) => return err.into_response(), + }; + + let token_from_get_param = params.token.clone(); + + let provided_tokens = (token_from_header, token_from_get_param); + + let token = match provided_tokens { + (Some(token_from_header), Some(_token_from_get_param)) => token_from_header, + (Some(token_from_header), None) => token_from_header, + (None, Some(token_from_get_param)) => token_from_get_param, + (None, None) => return AuthError::Unauthorized.into_response(), }; if !authenticate(&token, &state.access_tokens) { @@ -62,11 +96,42 @@ pub async fn auth( next.run(request).await } +fn extract_bearer_token_from_header(request: &Request) -> Result, AuthError> { + let headers = request.headers(); + + let header_value = headers + .get(axum::http::header::AUTHORIZATION) + .and_then(|header_value| header_value.to_str().ok()); + + match header_value { + None => Ok(None), + Some(header_value) => { + if header_value == AUTH_BEARER_TOKEN_HEADER_PREFIX { + // Empty token + return Ok(Some(String::new())); + } + + if !header_value.starts_with(&format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} ").to_string()) { + // Invalid token type. Missing "Bearer" prefix. + return Err(AuthError::UnknownTokenProvided); + } + + Ok(header_value + .strip_prefix(&format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} ").to_string()) + .map(std::string::ToString::to_string)) + } + } +} + enum AuthError { /// Missing token for authentication. Unauthorized, + /// Token was provided but it is not valid. TokenNotValid, + + /// Token was provided but it is not in a format that the server can't understands. + UnknownTokenProvided, } impl IntoResponse for AuthError { @@ -74,6 +139,7 @@ impl IntoResponse for AuthError { match self { AuthError::Unauthorized => unauthorized_response(), AuthError::TokenNotValid => token_not_valid_response(), + AuthError::UnknownTokenProvided => unknown_auth_data_provided_response(), } } } @@ -93,3 +159,12 @@ pub fn unauthorized_response() -> Response { pub fn token_not_valid_response() -> Response { unhandled_rejection_response("token not valid".to_string()) } + +/// `500` error response when the provided token type is not valid. +/// +/// The client has provided authentication information that the server does not +/// understand. +#[must_use] +pub fn unknown_auth_data_provided_response() -> Response { + unhandled_rejection_response("unknown token provided".to_string()) +} diff --git a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs index 3b6419187..0822f9fec 100644 --- a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs +++ b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs @@ -1,130 +1,275 @@ -use torrust_axum_rest_tracker_api_server::environment::Started; -use torrust_rest_tracker_api_client::common::http::{Query, QueryParam}; -use torrust_rest_tracker_api_client::v1::client::{headers_with_request_id, Client}; -use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; -use torrust_tracker_test_helpers::{configuration, logging}; -use uuid::Uuid; +mod given_that_the_token_is_only_provided_in_the_authentication_header { + use hyper::header; + use torrust_axum_rest_tracker_api_server::environment::Started; + use torrust_rest_tracker_api_client::common::http::Query; + use torrust_rest_tracker_api_client::v1::client::{ + headers_with_auth_token, headers_with_request_id, Client, AUTH_BEARER_TOKEN_HEADER_PREFIX, + }; + use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; + use torrust_tracker_test_helpers::{configuration, logging}; + use uuid::Uuid; -use crate::server::v1::asserts::{assert_token_not_valid, assert_unauthorized}; + use crate::server::v1::asserts::assert_token_not_valid; -#[tokio::test] -async fn should_authenticate_requests_by_using_a_token_query_param() { - logging::setup(); + #[tokio::test] + async fn it_should_authenticate_requests_when_the_token_is_provided_in_the_authentication_header() { + logging::setup(); - let env = Started::new(&configuration::ephemeral().into()).await; + let env = Started::new(&configuration::ephemeral().into()).await; - let token = env.get_connection_info().api_token.unwrap(); + let token = env.get_connection_info().api_token.unwrap(); - let response = Client::new(env.get_connection_info()) - .unwrap() - .get_request_with_query("stats", Query::params([QueryParam::new("token", &token)].to_vec()), None) - .await; + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query("stats", Query::default(), Some(headers_with_auth_token(&token))) + .await; - assert_eq!(response.status(), 200); + assert_eq!(response.status(), 200); - env.stop().await; -} + env.stop().await; + } + + #[tokio::test] + async fn it_should_not_authenticate_requests_when_the_token_is_empty() { + logging::setup(); + + let env = Started::new(&configuration::ephemeral().into()).await; + + let request_id = Uuid::new_v4(); + + let mut headers = headers_with_request_id(request_id); + + // Send the header with an empty token + headers.insert( + header::AUTHORIZATION, + format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} ") + .parse() + .expect("the auth token is not a valid header value"), + ); + + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query("stats", Query::default(), Some(headers)) + .await; + + assert_token_not_valid(response).await; + + assert!( + logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), + "Expected logs to contain: ERROR ... API ... request_id={request_id}" + ); + + env.stop().await; + } -#[tokio::test] -async fn should_not_authenticate_requests_when_the_token_is_missing() { - logging::setup(); + #[tokio::test] + async fn it_should_not_authenticate_requests_when_the_token_is_invalid() { + logging::setup(); - let env = Started::new(&configuration::ephemeral().into()).await; + let env = Started::new(&configuration::ephemeral().into()).await; - let request_id = Uuid::new_v4(); + let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) - .unwrap() - .get_request_with_query("stats", Query::default(), Some(headers_with_request_id(request_id))) - .await; + let mut headers = headers_with_request_id(request_id); - assert_unauthorized(response).await; + // Send the header with an empty token + headers.insert( + header::AUTHORIZATION, + "Bearer INVALID TOKEN" + .parse() + .expect("the auth token is not a valid header value"), + ); - assert!( - logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), - "Expected logs to contain: ERROR ... API ... request_id={request_id}" - ); + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query("stats", Query::default(), Some(headers)) + .await; - env.stop().await; + assert_token_not_valid(response).await; + + assert!( + logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), + "Expected logs to contain: ERROR ... API ... request_id={request_id}" + ); + + env.stop().await; + } } +mod given_that_the_token_is_only_provided_in_the_query_param { + + use torrust_axum_rest_tracker_api_server::environment::Started; + use torrust_rest_tracker_api_client::common::http::{Query, QueryParam}; + use torrust_rest_tracker_api_client::v1::client::{headers_with_request_id, Client}; + use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; + use torrust_tracker_test_helpers::{configuration, logging}; + use uuid::Uuid; + + use crate::server::v1::asserts::assert_token_not_valid; + + #[tokio::test] + async fn it_should_authenticate_requests_when_the_token_is_provided_as_a_query_param() { + logging::setup(); + + let env = Started::new(&configuration::ephemeral().into()).await; + + let token = env.get_connection_info().api_token.unwrap(); + + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query("stats", Query::params([QueryParam::new("token", &token)].to_vec()), None) + .await; + + assert_eq!(response.status(), 200); -#[tokio::test] -async fn should_not_authenticate_requests_when_the_token_is_empty() { - logging::setup(); + env.stop().await; + } - let env = Started::new(&configuration::ephemeral().into()).await; + #[tokio::test] + async fn it_should_not_authenticate_requests_when_the_token_is_empty() { + logging::setup(); - let request_id = Uuid::new_v4(); + let env = Started::new(&configuration::ephemeral().into()).await; - let response = Client::new(env.get_connection_info()) - .unwrap() - .get_request_with_query( - "stats", - Query::params([QueryParam::new("token", "")].to_vec()), - Some(headers_with_request_id(request_id)), - ) - .await; + let request_id = Uuid::new_v4(); - assert_token_not_valid(response).await; + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query( + "stats", + Query::params([QueryParam::new("token", "")].to_vec()), + Some(headers_with_request_id(request_id)), + ) + .await; - assert!( - logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), - "Expected logs to contain: ERROR ... API ... request_id={request_id}" - ); + assert_token_not_valid(response).await; - env.stop().await; + assert!( + logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), + "Expected logs to contain: ERROR ... API ... request_id={request_id}" + ); + + env.stop().await; + } + + #[tokio::test] + async fn it_should_not_authenticate_requests_when_the_token_is_invalid() { + logging::setup(); + + let env = Started::new(&configuration::ephemeral().into()).await; + + let request_id = Uuid::new_v4(); + + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query( + "stats", + Query::params([QueryParam::new("token", "INVALID TOKEN")].to_vec()), + Some(headers_with_request_id(request_id)), + ) + .await; + + assert_token_not_valid(response).await; + + assert!( + logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), + "Expected logs to contain: ERROR ... API ... request_id={request_id}" + ); + + env.stop().await; + } + + #[tokio::test] + async fn it_should_allow_the_token_query_param_to_be_at_any_position_in_the_url_query() { + logging::setup(); + + let env = Started::new(&configuration::ephemeral().into()).await; + + let token = env.get_connection_info().api_token.unwrap(); + + // At the beginning of the query component + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request(&format!("torrents?token={token}&limit=1")) + .await; + + assert_eq!(response.status(), 200); + + // At the end of the query component + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request(&format!("torrents?limit=1&token={token}")) + .await; + + assert_eq!(response.status(), 200); + + env.stop().await; + } } -#[tokio::test] -async fn should_not_authenticate_requests_when_the_token_is_invalid() { - logging::setup(); +mod given_that_not_token_is_provided { + + use torrust_axum_rest_tracker_api_server::environment::Started; + use torrust_rest_tracker_api_client::common::http::Query; + use torrust_rest_tracker_api_client::v1::client::{headers_with_request_id, Client}; + use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; + use torrust_tracker_test_helpers::{configuration, logging}; + use uuid::Uuid; + + use crate::server::v1::asserts::assert_unauthorized; + + #[tokio::test] + async fn it_should_not_authenticate_requests_when_the_token_is_missing() { + logging::setup(); - let env = Started::new(&configuration::ephemeral().into()).await; + let env = Started::new(&configuration::ephemeral().into()).await; - let request_id = Uuid::new_v4(); + let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) - .unwrap() - .get_request_with_query( - "stats", - Query::params([QueryParam::new("token", "INVALID TOKEN")].to_vec()), - Some(headers_with_request_id(request_id)), - ) - .await; + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query("stats", Query::default(), Some(headers_with_request_id(request_id))) + .await; - assert_token_not_valid(response).await; + assert_unauthorized(response).await; - assert!( - logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), - "Expected logs to contain: ERROR ... API ... request_id={request_id}" - ); + assert!( + logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]), + "Expected logs to contain: ERROR ... API ... request_id={request_id}" + ); - env.stop().await; + env.stop().await; + } } -#[tokio::test] -async fn should_allow_the_token_query_param_to_be_at_any_position_in_the_url_query() { - logging::setup(); +mod given_that_token_is_provided_via_get_param_and_authentication_header { + use torrust_axum_rest_tracker_api_server::environment::Started; + use torrust_rest_tracker_api_client::common::http::{Query, QueryParam}; + use torrust_rest_tracker_api_client::v1::client::{headers_with_auth_token, Client, TOKEN_PARAM_NAME}; + use torrust_tracker_test_helpers::{configuration, logging}; - let env = Started::new(&configuration::ephemeral().into()).await; + #[tokio::test] + async fn it_should_authenticate_requests_using_the_token_provided_in_the_authentication_header() { + logging::setup(); - let token = env.get_connection_info().api_token.unwrap(); + let env = Started::new(&configuration::ephemeral().into()).await; - // At the beginning of the query component - let response = Client::new(env.get_connection_info()) - .unwrap() - .get_request(&format!("torrents?token={token}&limit=1")) - .await; + let authorized_token = env.get_connection_info().api_token.unwrap(); - assert_eq!(response.status(), 200); + let non_authorized_token = "NonAuthorizedToken"; - // At the end of the query component - let response = Client::new(env.get_connection_info()) - .unwrap() - .get_request(&format!("torrents?limit=1&token={token}")) - .await; + let response = Client::new(env.get_connection_info()) + .unwrap() + .get_request_with_query( + "stats", + Query::params([QueryParam::new(TOKEN_PARAM_NAME, non_authorized_token)].to_vec()), + Some(headers_with_auth_token(&authorized_token)), + ) + .await; - assert_eq!(response.status(), 200); + // The token provided in the query param should be ignored and the token + // in the authentication header should be used. + assert_eq!(response.status(), 200); - env.stop().await; + env.stop().await; + } } diff --git a/packages/rest-tracker-api-client/src/v1/client.rs b/packages/rest-tracker-api-client/src/v1/client.rs index 65e3fceb8..d13a567bb 100644 --- a/packages/rest-tracker-api-client/src/v1/client.rs +++ b/packages/rest-tracker-api-client/src/v1/client.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use hyper::HeaderMap; +use hyper::{header, HeaderMap}; use reqwest::{Error, Response}; use serde::Serialize; use url::Url; @@ -9,7 +9,9 @@ use uuid::Uuid; use crate::common::http::{Query, QueryParam, ReqwestQuery}; use crate::connection_info::ConnectionInfo; -const TOKEN_PARAM_NAME: &str = "token"; +pub const TOKEN_PARAM_NAME: &str = "token"; +pub const AUTH_BEARER_TOKEN_HEADER_PREFIX: &str = "Bearer"; + const API_PATH: &str = "api/v1/"; const DEFAULT_REQUEST_TIMEOUT_IN_SECS: u64 = 5; @@ -180,15 +182,38 @@ pub async fn get(path: Url, query: Option, headers: Option) -> builder.send().await.unwrap() } -/// Returns a `HeaderMap` with a request id header +/// Returns a `HeaderMap` with a request id header. /// /// # Panics /// -/// Will panic if the request ID can't be parsed into a string. +/// Will panic if the request ID can't be parsed into a `HeaderValue`. #[must_use] pub fn headers_with_request_id(request_id: Uuid) -> HeaderMap { let mut headers = HeaderMap::new(); - headers.insert("x-request-id", request_id.to_string().parse().unwrap()); + headers.insert( + "x-request-id", + request_id + .to_string() + .parse() + .expect("the request ID is not a valid header value"), + ); + headers +} + +/// Returns a `HeaderMap` with an authorization token. +/// +/// # Panics +/// +/// Will panic if the token can't be parsed into a `HeaderValue`. +#[must_use] +pub fn headers_with_auth_token(token: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + header::AUTHORIZATION, + format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} {token}") + .parse() + .expect("the auth token is not a valid header value"), + ); headers } From 34f2f437db7cfbce2fed5a94f906c39a7794b0f9 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Mar 2025 15:58:26 +0000 Subject: [PATCH 2/2] refactor: [#727] use the Authentication header in the API client Instead of passing the `token` via GET param. The server supports both. Since we have not released any version crate for the client yet we can use the header by deafault which is more secure. --- .../server/v1/contract/authentication.rs | 39 ++++++--- .../rest-tracker-api-client/src/v1/client.rs | 81 ++++++++++++++----- 2 files changed, 89 insertions(+), 31 deletions(-) diff --git a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs index 0822f9fec..be291a50c 100644 --- a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs +++ b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/authentication.rs @@ -2,6 +2,7 @@ mod given_that_the_token_is_only_provided_in_the_authentication_header { use hyper::header; use torrust_axum_rest_tracker_api_server::environment::Started; use torrust_rest_tracker_api_client::common::http::Query; + use torrust_rest_tracker_api_client::connection_info::ConnectionInfo; use torrust_rest_tracker_api_client::v1::client::{ headers_with_auth_token, headers_with_request_id, Client, AUTH_BEARER_TOKEN_HEADER_PREFIX, }; @@ -80,7 +81,9 @@ mod given_that_the_token_is_only_provided_in_the_authentication_header { .expect("the auth token is not a valid header value"), ); - let response = Client::new(env.get_connection_info()) + let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); + + let response = Client::new(connection_info) .unwrap() .get_request_with_query("stats", Query::default(), Some(headers)) .await; @@ -99,7 +102,8 @@ mod given_that_the_token_is_only_provided_in_the_query_param { use torrust_axum_rest_tracker_api_server::environment::Started; use torrust_rest_tracker_api_client::common::http::{Query, QueryParam}; - use torrust_rest_tracker_api_client::v1::client::{headers_with_request_id, Client}; + use torrust_rest_tracker_api_client::connection_info::ConnectionInfo; + use torrust_rest_tracker_api_client::v1::client::{headers_with_request_id, Client, TOKEN_PARAM_NAME}; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; use uuid::Uuid; @@ -114,9 +118,15 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let token = env.get_connection_info().api_token.unwrap(); - let response = Client::new(env.get_connection_info()) + let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); + + let response = Client::new(connection_info) .unwrap() - .get_request_with_query("stats", Query::params([QueryParam::new("token", &token)].to_vec()), None) + .get_request_with_query( + "stats", + Query::params([QueryParam::new(TOKEN_PARAM_NAME, &token)].to_vec()), + None, + ) .await; assert_eq!(response.status(), 200); @@ -132,11 +142,13 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); + + let response = Client::new(connection_info) .unwrap() .get_request_with_query( "stats", - Query::params([QueryParam::new("token", "")].to_vec()), + Query::params([QueryParam::new(TOKEN_PARAM_NAME, "")].to_vec()), Some(headers_with_request_id(request_id)), ) .await; @@ -159,11 +171,13 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); + + let response = Client::new(connection_info) .unwrap() .get_request_with_query( "stats", - Query::params([QueryParam::new("token", "INVALID TOKEN")].to_vec()), + Query::params([QueryParam::new(TOKEN_PARAM_NAME, "INVALID TOKEN")].to_vec()), Some(headers_with_request_id(request_id)), ) .await; @@ -186,8 +200,10 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let token = env.get_connection_info().api_token.unwrap(); + let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); + // At the beginning of the query component - let response = Client::new(env.get_connection_info()) + let response = Client::new(connection_info) .unwrap() .get_request(&format!("torrents?token={token}&limit=1")) .await; @@ -210,6 +226,7 @@ mod given_that_not_token_is_provided { use torrust_axum_rest_tracker_api_server::environment::Started; use torrust_rest_tracker_api_client::common::http::Query; + use torrust_rest_tracker_api_client::connection_info::ConnectionInfo; use torrust_rest_tracker_api_client::v1::client::{headers_with_request_id, Client}; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; @@ -225,7 +242,9 @@ mod given_that_not_token_is_provided { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); + + let response = Client::new(connection_info) .unwrap() .get_request_with_query("stats", Query::default(), Some(headers_with_request_id(request_id))) .await; diff --git a/packages/rest-tracker-api-client/src/v1/client.rs b/packages/rest-tracker-api-client/src/v1/client.rs index d13a567bb..da1b709da 100644 --- a/packages/rest-tracker-api-client/src/v1/client.rs +++ b/packages/rest-tracker-api-client/src/v1/client.rs @@ -92,16 +92,18 @@ impl Client { /// /// Will panic if the request can't be sent pub async fn post_empty(&self, path: &str, headers: Option) -> Response { - let builder = self - .client - .post(self.base_url(path).clone()) - .query(&ReqwestQuery::from(self.query_with_token())); + let builder = self.client.post(self.base_url(path).clone()); let builder = match headers { Some(headers) => builder.headers(headers), None => builder, }; + let builder = match &self.connection_info.api_token { + Some(token) => builder.header(header::AUTHORIZATION, format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} {token}")), + None => builder, + }; + builder.send().await.unwrap() } @@ -109,17 +111,18 @@ impl Client { /// /// Will panic if the request can't be sent pub async fn post_form(&self, path: &str, form: &T, headers: Option) -> Response { - let builder = self - .client - .post(self.base_url(path).clone()) - .query(&ReqwestQuery::from(self.query_with_token())) - .json(&form); + let builder = self.client.post(self.base_url(path).clone()).json(&form); let builder = match headers { Some(headers) => builder.headers(headers), None => builder, }; + let builder = match &self.connection_info.api_token { + Some(token) => builder.header(header::AUTHORIZATION, format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} {token}")), + None => builder, + }; + builder.send().await.unwrap() } @@ -127,34 +130,70 @@ impl Client { /// /// Will panic if the request can't be sent async fn delete(&self, path: &str, headers: Option) -> Response { - let builder = self - .client - .delete(self.base_url(path).clone()) - .query(&ReqwestQuery::from(self.query_with_token())); + let builder = self.client.delete(self.base_url(path).clone()); let builder = match headers { Some(headers) => builder.headers(headers), None => builder, }; + let builder = match &self.connection_info.api_token { + Some(token) => builder.header(header::AUTHORIZATION, format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} {token}")), + None => builder, + }; + builder.send().await.unwrap() } + /// # Panics + /// + /// Will panic if it can't convert the authentication token to a `HeaderValue`. pub async fn get_request_with_query(&self, path: &str, params: Query, headers: Option) -> Response { - get(self.base_url(path), Some(params), headers).await + match &self.connection_info.api_token { + Some(token) => { + let headers = if let Some(headers) = headers { + // Headers provided -> add auth token if not already present + + if headers.get(header::AUTHORIZATION).is_some() { + // Auth token already present -> use provided + headers + } else { + let mut headers = headers; + + headers.insert( + header::AUTHORIZATION, + format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} {token}") + .parse() + .expect("the auth token is not a valid header value"), + ); + + headers + } + } else { + // No headers provided -> create headers with auth token + + let mut headers = HeaderMap::new(); + + headers.insert( + header::AUTHORIZATION, + format!("{AUTH_BEARER_TOKEN_HEADER_PREFIX} {token}") + .parse() + .expect("the auth token is not a valid header value"), + ); + + headers + }; + + get(self.base_url(path), Some(params), Some(headers)).await + } + None => get(self.base_url(path), Some(params), headers).await, + } } pub async fn get_request(&self, path: &str) -> Response { get(self.base_url(path), None, None).await } - fn query_with_token(&self) -> Query { - match &self.connection_info.api_token { - Some(token) => Query::params([QueryParam::new("token", token)].to_vec()), - None => Query::default(), - } - } - fn base_url(&self, path: &str) -> Url { Url::parse(&format!("{}{}{path}", &self.connection_info.origin, &self.base_path)).unwrap() }