Skip to content

Commit

Permalink
feat(api): [#143] authentication with GET param for Axum API
Browse files Browse the repository at this point in the history
It keeps the same contract of the API. It returns 500 status code with
error message in "debug" format.
  • Loading branch information
josecelano committed Jan 4, 2023
1 parent 1c6db6e commit 43dbed9
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 5 deletions.
1 change: 1 addition & 0 deletions cSpell.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"leechers",
"libtorrent",
"Lphant",
"middlewares",
"mockall",
"myacicontext",
"nanos",
Expand Down
62 changes: 62 additions & 0 deletions src/apis/middlewares/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::sync::Arc;

use axum::extract::{Query, State};
use axum::http::{header, Request, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;

use crate::config::{Configuration, HttpApi};

#[derive(Deserialize, Debug)]
pub struct QueryParams {
pub token: Option<String>,
}

/// Middleware for authentication using a "token" GET param.
/// The token must be one of the tokens in the tracker HTTP API configuration.
pub async fn auth<B>(
State(config): State<Arc<Configuration>>,
Query(params): Query<QueryParams>,
request: Request<B>,
next: Next<B>,
) -> Response
where
B: Send,
{
let token = match params.token {
None => return AuthError::Unauthorized.into_response(),
Some(token) => token,
};

if !authenticate(&token, &config.http_api) {
return AuthError::TokenNotValid.into_response();
}

next.run(request).await
}

enum AuthError {
Unauthorized,
TokenNotValid,
}

impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let body = match self {
AuthError::Unauthorized => "Unhandled rejection: Err { reason: \"unauthorized\" }",
AuthError::TokenNotValid => "Unhandled rejection: Err { reason: \"token not valid\" }",
};

(
StatusCode::INTERNAL_SERVER_ERROR,
[(header::CONTENT_TYPE, "text/plain")],
body,
)
.into_response()
}
}

fn authenticate(token: &str, http_api_config: &HttpApi) -> bool {
http_api_config.contains_token(token)
}
1 change: 1 addition & 0 deletions src/apis/middlewares/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod auth;
1 change: 1 addition & 0 deletions src/apis/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod middlewares;
pub mod routes;
pub mod server;
13 changes: 9 additions & 4 deletions src/apis/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ use std::net::SocketAddr;
use std::sync::Arc;

use axum::routing::get;
use axum::Router;
use axum::{middleware, Router};
use futures::Future;
use warp::hyper;

use super::middlewares::auth::auth;
use super::routes::{get_stats, root};
use crate::tracker;

pub fn start(socket_addr: SocketAddr, tracker: &Arc<tracker::Tracker>) -> impl Future<Output = hyper::Result<()>> {
let app = Router::new()
.route("/", get(root))
.route("/stats", get(get_stats).with_state(tracker.clone()));
.route("/stats", get(get_stats).with_state(tracker.clone()))
.layer(middleware::from_fn_with_state(tracker.config.clone(), auth));

let server = axum::Server::bind(&socket_addr).serve(app.into_make_service());

Expand All @@ -25,11 +27,14 @@ pub fn start_tls(
socket_addr: SocketAddr,
_ssl_cert_path: &str,
_ssl_key_path: &str,
_tracker: &Arc<tracker::Tracker>,
tracker: &Arc<tracker::Tracker>,
) -> impl Future<Output = hyper::Result<()>> {
// todo: for the time being, it's just a copy & paste from start(...).

let app = Router::new().route("/", get(root));
let app = Router::new()
.route("/", get(root))
.route("/stats", get(get_stats).with_state(tracker.clone()))
.layer(middleware::from_fn_with_state(tracker.config.clone(), auth));

let server = axum::Server::bind(&socket_addr).serve(app.into_make_service());

Expand Down
19 changes: 18 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::path::Path;
use std::str::FromStr;
Expand Down Expand Up @@ -44,6 +44,15 @@ pub struct HttpApi {
pub access_tokens: HashMap<String, String>,
}

impl HttpApi {
#[must_use]
pub fn contains_token(&self, token: &str) -> bool {
let tokens: HashMap<String, String> = self.access_tokens.clone();
let tokens: HashSet<String> = tokens.into_values().collect();
tokens.contains(token)
}
}

#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct Configuration {
Expand Down Expand Up @@ -366,4 +375,12 @@ mod tests {

assert_eq!(format!("{error}"), "TrackerModeIncompatible");
}

#[test]
fn http_api_configuration_should_check_if_it_contains_a_token() {
let configuration = Configuration::default();

assert!(configuration.http_api.contains_token("MyAccessToken"));
assert!(!configuration.http_api.contains_token("NonExistingToken"));
}
}
19 changes: 19 additions & 0 deletions tests/tracker_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,9 @@ mod tracker_apis {
use torrust_tracker::api::resource::stats::Stats;
use torrust_tracker::protocol::info_hash::InfoHash;

use crate::api::asserts::{assert_token_not_valid, assert_unauthorized};
use crate::api::client::Client;
use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token};
use crate::api::fixtures::sample_peer;
use crate::api::server::start_default_api;
use crate::api::Version;
Expand Down Expand Up @@ -645,5 +647,22 @@ mod tracker_apis {
}
);
}

#[tokio::test]
async fn should_not_allow_getting_tracker_statistics_for_unauthenticated_users() {
let api_server = start_default_api(&Version::Axum).await;

let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address()), &Version::Axum)
.get_tracker_statistics()
.await;

assert_token_not_valid(response).await;

let response = Client::new(connection_with_no_token(&api_server.get_bind_address()), &Version::Axum)
.get_tracker_statistics()
.await;

assert_unauthorized(response).await;
}
}
}

0 comments on commit 43dbed9

Please sign in to comment.