Skip to content

Commit bb5dba8

Browse files
committed
impl middleware authentication, enable authentication for everything
1 parent 3ac17fe commit bb5dba8

5 files changed

Lines changed: 92 additions & 58 deletions

File tree

backend/src/axum.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use crate::commands::command_controller::{delete_by_id, get_all_commands, get_all_user_commands, get_by_trigger_id, save, set_enabled, set_visible};
22
use crate::panel_frontend::embedded_panel_server::{embedded_panel_service, to_panel_redirect, SERVER_PANEL_PATH};
33
use crate::service_oauth::oauth_endpoint::{list_oauth, receive_oauth};
4+
use crate::webserver_authentication::auth_mod;
45
use crate::AppState;
6+
use axum::middleware::from_fn_with_state;
57
use axum::routing::{any, delete, get, post};
68
use axum::Router;
79
use http::Uri;
@@ -13,11 +15,9 @@ use url::{form_urlencoded, Url};
1315
pub type AxumState = Arc<AppState>;
1416

1517
pub async fn axum(on_port: u16, state: AxumState) {
16-
let (panel_base_url, server_base_url) = {
17-
let guard = state.webserver_config.read().unwrap();
18-
(guard.panel_base_url.clone(), guard.server_base_url.clone())
18+
let webserver_config = {
19+
state.webserver_config.read().unwrap().clone()
1920
};
20-
println!("Hosting embedded panel at: {}panel", server_base_url);
2121

2222
let cors_allow_all = CorsLayer::very_permissive();
2323

@@ -31,13 +31,13 @@ pub async fn axum(on_port: u16, state: AxumState) {
3131
.route("/commands/delete/{id}", delete(delete_by_id))
3232

3333
.route("/setup/auth/list", get(list_oauth))
34-
// apply auth middleware to all above here
34+
.layer(from_fn_with_state(state.clone(), auth_mod))
3535
.route("/auth/{service}", any(receive_oauth))
3636
.layer(cors_allow_all);
3737

3838
let app = Router::new()
3939
.nest_service(SERVER_PANEL_PATH, embedded_panel_service(state.clone()))
40-
.route_service("/", to_panel_redirect(server_base_url))
40+
.route_service("/", to_panel_redirect(webserver_config.server_base_url))
4141
.nest("/bot", bot_router)
4242
.with_state(state);
4343

backend/src/panel_frontend/embedded_panel_server.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub fn embedded_panel_service(state: AxumState) -> ServeDir<DynamicIndexHtmlHand
3636
eprintln!();
3737
} else if !index_exists.unwrap() {
3838
eprintln!("panel_dist is missing index.html cannot server embedded panel in a working state!");
39+
} else {
40+
let g = state.webserver_config.read().unwrap();
41+
println!("Hosting embedded panel at: {}panel", g.server_base_url);
3942
}
4043
ServeDir::new(PANEL_DIST_DIR)
4144
.append_index_html_on_directories(false)
@@ -69,7 +72,7 @@ impl Service<Request<Body>> for DynamicIndexHtmlHandlerService {
6972
}
7073

7174
fn call(&mut self, _req: Request<Body>) -> Self::Future {
72-
let index = std::fs::read_to_string(PANEL_DIST_DIR.to_owned() + "/index.html").unwrap();
75+
let index = fs::read_to_string(PANEL_DIST_DIR.to_owned() + "/index.html").unwrap();
7376

7477
let c = {
7578
let g = self.state.webserver_config.read().unwrap();

backend/src/panel_user.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub struct PanelUser {
1717
}
1818

1919
/// Add this to the mapping function to add authentication to it
20+
#[derive(Clone)]
2021
pub struct Moderator(PanelUser);
2122

2223
impl Moderator {

backend/src/service_oauth/oauth_endpoint.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ pub async fn receive_oauth(
6161
})))
6262
}
6363

64-
pub async fn list_oauth(_: Moderator, State(state): State<AxumState>) -> impl IntoResponse {
64+
pub async fn list_oauth(State(state): State<AxumState>) -> impl IntoResponse {
6565
Json(state.oauth_service.get_active_requests())
6666
}

backend/src/webserver_authentication.rs

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
use crate::axum::AxumState;
22
pub(crate) use crate::panel_user::Moderator;
3-
use crate::panel_user::PanelUserService;
3+
use crate::panel_user::{PanelUser, PanelUserService};
44
use anyhow::Context;
5-
use axum::extract::FromRequestParts;
5+
use axum::extract::{FromRequestParts, Request, State};
66
use axum::http::request::Parts;
77
use axum::http::{HeaderMap, HeaderValue, StatusCode};
8+
use axum::middleware::Next;
9+
use axum::response::IntoResponse;
10+
use axum::response::Result as AxResult;
811
use reqwest::Method;
912
use serde::Deserialize;
1013
use std::time::Instant;
@@ -18,6 +21,66 @@ struct ValidationReturn {
1821
user_id: String
1922
}
2023

24+
const FORBIDDEN: fn() -> (StatusCode, String) = || (StatusCode::FORBIDDEN, "user lacks required permissions".to_owned());
25+
26+
pub async fn auth_user(State(state): State<AxumState>, mut request: Request, next: Next) -> AxResult<impl IntoResponse> {
27+
let (mut request, user) = authenticate_user(state, request).await?;
28+
29+
request.extensions_mut().insert(user);
30+
Ok(next.run(request).await)
31+
}
32+
33+
pub async fn auth_mod(State(state): State<AxumState>, request: Request, next: Next) -> AxResult<impl IntoResponse> {
34+
let (mut request, user) = match request
35+
.extensions()
36+
.get::<PanelUser>()
37+
.cloned() {
38+
Some(user) => (request, user),
39+
None => authenticate_user(state, request).await?,
40+
};
41+
42+
let moderator = Moderator::new(user).ok_or(FORBIDDEN())?;
43+
request.extensions_mut().insert(moderator);
44+
Ok(next.run(request).await)
45+
}
46+
47+
async fn authenticate_user(state: AxumState, request: Request) -> AxResult<(Request, PanelUser)> {
48+
let access_token = get_header(request.headers(), "token")?;
49+
let user_agent = get_header(request.headers(), "User-Agent")?;
50+
51+
//we would still want to implement the authentication bypass, although handling those anonymous users for extractors would be a challenge
52+
match state.session_service.get_by_access_token(&access_token) {
53+
Some(session) => {
54+
if session.user_agent != user_agent {
55+
Err((StatusCode::UNAUTHORIZED, "Reauthenticate with access token".to_string()))?
56+
} else if session.last_refreshed_at + state.session_service.session_timeout() < Instant::now() {
57+
state.session_service.delete_by_access_token(&access_token);
58+
Err((StatusCode::UNAUTHORIZED, "Reauthenticate with access token".to_string()))?
59+
} else {
60+
_ = state.session_service.refresh_session(access_token);
61+
Ok((request, session.panel_user))
62+
}
63+
}
64+
None => {
65+
let validated = validate_token(&access_token).await
66+
.map_err(|_err| {
67+
// log errors
68+
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error. Retry authentication".to_string())
69+
})?
70+
.ok_or((StatusCode::UNAUTHORIZED, "Invalid access token, Reauthenticate".to_string()))?;
71+
let user = PanelUserService::find_by_id(&state.prod_db, validated.user_id)
72+
.await
73+
.map_err(|_err| {
74+
// log error
75+
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error. Retry authentication".to_string())
76+
})?
77+
.ok_or(FORBIDDEN())?;
78+
state.session_service.create_session(user.clone(), access_token, user_agent);
79+
Ok((request, user))
80+
}
81+
}
82+
}
83+
2184
/// Validate an accessToken with Twitch. Ok(None) represents a successful validation, but the token being invalid
2285
async fn validate_token(access_token: &str) -> anyhow::Result<Option<ValidationReturn>> {
2386
// prob move client into app state
@@ -41,57 +104,24 @@ async fn validate_token(access_token: &str) -> anyhow::Result<Option<ValidationR
41104
Ok(Some(validation_return))
42105
}
43106

107+
fn get_header(headers: &HeaderMap<HeaderValue>, key: &str) -> Result<String, (StatusCode, String)> {
108+
headers
109+
.get(key).ok_or((StatusCode::UNAUTHORIZED, format!("Missing '{}' authentication header", key)))
110+
.map(|value| value.to_str().map_err(|_| (StatusCode::BAD_REQUEST, "malformed authentication header token, non ascii string".to_string())))
111+
.and_then(|value| value)
112+
.map(|s| s.to_string())
113+
}
114+
44115
impl FromRequestParts<AxumState> for Moderator {
45116
type Rejection = (StatusCode, String);
46117

47-
//this is correct for routes that would definitely need to know which user this was, but we would still want to implement the authentication bypass
48-
fn from_request_parts(parts: &mut Parts, state: &AxumState) -> impl Future<Output=Result<Self, Self::Rejection>> + Send {
49-
50-
fn get_header(headers: &HeaderMap<HeaderValue>, key: &str) -> Result<String, (StatusCode, String)> {
51-
headers
52-
.get(key).ok_or((StatusCode::UNAUTHORIZED, format!("Missing '{}' authentication header", key)))
53-
.map(|value| value.to_str().map_err(|_| (StatusCode::BAD_REQUEST, "malformed authentication header token, non ascii string".to_string())))
54-
.and_then(|value| value)
55-
.map(|s| s.to_string())
56-
}
57-
58-
async {
59-
let access_token = get_header(&parts.headers, "token")?;
60-
let user_agent = get_header(&parts.headers,"User-Agent")?;
61-
62-
match state.session_service.get_by_access_token(&access_token) {
63-
Some(session) => {
64-
if session.user_agent != user_agent {
65-
Err((StatusCode::UNAUTHORIZED, "Reauthenticate with access token".to_string()))
66-
} else if session.last_refreshed_at + state.session_service.session_timeout() < Instant::now() {
67-
state.session_service.delete_by_access_token(&access_token);
68-
Err((StatusCode::UNAUTHORIZED, "Reauthenticate with access token".to_string()))
69-
} else {
70-
let moderator = Moderator::new(session.panel_user)
71-
.ok_or((StatusCode::FORBIDDEN, "user lacks required permissions".to_string()))?;
72-
_ = state.session_service.refresh_session(access_token);
73-
Ok(moderator)
74-
}
75-
}
76-
None => {
77-
let validated = validate_token(&access_token).await
78-
.map_err(|_err| {
79-
// log errors
80-
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error. Retry authentication".to_string())
81-
})?
82-
.ok_or((StatusCode::UNAUTHORIZED, "Invalid access token, Reauthenticate".to_string()))?;
83-
let user = PanelUserService::find_by_id(&state.prod_db, validated.user_id)
84-
.await
85-
.map_err(|_err| {
86-
// log error
87-
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error. Retry authentication".to_string())
88-
})?
89-
.ok_or((StatusCode::FORBIDDEN, "user lacks required permissions".to_string()))?;
90-
let moderator = Moderator::new(user.clone())
91-
.ok_or((StatusCode::FORBIDDEN, "user lacks required permissions".to_string()))?;
92-
state.session_service.create_session(user, access_token, user_agent);
93-
Ok(moderator)
94-
}
118+
async fn from_request_parts(parts: &mut Parts, _state: &AxumState) -> Result<Self, Self::Rejection> {
119+
match parts.extensions.get::<Moderator>() {
120+
Some(extension) => Ok(extension.clone()),
121+
None => match parts.extensions.get::<PanelUser>() {
122+
None => Err(FORBIDDEN()),
123+
Some(u) => Moderator::new(u.clone())
124+
.ok_or(FORBIDDEN()),
95125
}
96126
}
97127
}

0 commit comments

Comments
 (0)