From 25d2e1d036a94b1b0fae93005238fc94aecbaf55 Mon Sep 17 00:00:00 2001 From: BloxerHD018 Date: Thu, 23 Apr 2026 15:39:38 +0100 Subject: [PATCH] Implement Keycloak authentication to Admin API --- .env.example | 22 ++++++++---- Cargo.toml | 1 + src/admin_auth.rs | 71 -------------------------------------- src/api/add_task.rs | 6 ++-- src/api/upload_file_wup.rs | 6 ++-- src/auth.rs | 67 +++++++++++++++++++++++++++++++++++ src/main.rs | 2 +- 7 files changed, 91 insertions(+), 84 deletions(-) delete mode 100644 src/admin_auth.rs create mode 100644 src/auth.rs diff --git a/.env.example b/.env.example index 5cfe46b..e122eb7 100644 --- a/.env.example +++ b/.env.example @@ -8,9 +8,19 @@ BOSS_CDN_URL=https://boss.example.com AES_KEY=KEY HMAC_KEY=KEY -# Can be paired with an admin auth server for extra security, -# if disabled auth will instead check to see if the sent token -# matches ADMIN_AUTH_TOKEN -USE_ADMIN_AUTH=true -ADMIN_AUTH_URL=http://admin.example.com # Only needed when USE_ADMIN_AUTH=true -ADMIN_AUTH_TOKEN=AUTH_TOKEN +# S3 Config for Boss File Storage +S3_ENDPOINT=https://s3.example.com/ +S3_BUCKET_NAME=boss-files +S3_ACCESS_KEY=key +S3_SECRET_KEY=key + +# Authentication Config for Admin API +# Can either use token or Keycloak server +FORCE_KEYCLOAK=false +KEYCLOAK_URL=https://keycloak.example.com +KEYCLOAK_REALM=admin +KEYCLOAK_CLIENT_ID=id +KEYCLOAK_CLIENT_SECRET=secret + +# Fallback when not using Keycloak, doesn't have to be set if using Keycloak only +AUTH_TOKEN=token diff --git a/Cargo.toml b/Cargo.toml index 648d81b..d1e82ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ aws-config = { version = "1.1", features = ["behavior-version-latest"] } aws-sdk-s3 = "1.17" tokio = { version = "1.50.0", features = ["full"] } log = "0.4.29" +keycloak-helper = { git = "https://git.virintox.com/BloxerHD018/keycloak-helper.git" } diff --git a/src/admin_auth.rs b/src/admin_auth.rs deleted file mode 100644 index aba6732..0000000 --- a/src/admin_auth.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::env; -use rocket::{Request, http::Status, request::{FromRequest, Outcome}}; -use reqwest::Client; - -pub struct AdminAuth(pub String); - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for AdminAuth { - type Error = (); - - async fn from_request(req: &'r Request<'_>) -> Outcome { - let use_admin_server = env::var("USE_ADMIN_AUTH") - .map(|v| v == "true") - .unwrap_or(false); - - if use_admin_server { - let auth = match req.headers().get_one("Authorization") { - Some(auth) => auth, - None => return Outcome::Error((Status::BadRequest, ())), - }; - - let auth_url = match env::var("ADMIN_AUTH_URL") { - Ok(url) => url, - Err(_) => return Outcome::Error((Status::InternalServerError, ())), - }; - let auth_token = match env::var("ADMIN_AUTH_TOKEN") { - Ok(url) => url, - Err(_) => return Outcome::Error((Status::InternalServerError, ())), - }; - - let http = Client::new(); - let response = http.get(format!("{}/api/v1/validate_token/boss", auth_url)) - .header("Authorization", format!("InterServer {}", auth_token)) - .header("Token", auth) - .send() - .await; - - match response { - Ok(response) => { - match response.error_for_status() { - Ok(response) => match response.text().await { - Ok(text) => return Outcome::Success(AdminAuth(text)), - Err(_) => return Outcome::Error((Status::InternalServerError, ())) - }, - Err(_) => { - return Outcome::Error((Status::Unauthorized, ())); - } - } - }, - Err(_) => return Outcome::Error((Status::InternalServerError, ())), - } - } else { - let local_token = match env::var("ADMIN_AUTH_TOKEN") { - Ok(token) => token, - Err(_) => return Outcome::Error((Status::InternalServerError, ())) - }; - - let token = match req.headers().get_one("Authorization") { - Some(auth) => match auth.strip_prefix("Bearer ") { - Some(token) => token, - None => return Outcome::Error((Status::BadRequest, ())) - }, - None => return Outcome::Error((Status::BadRequest, ())), - }; - - if token != local_token.as_str() { return Outcome::Error((Status::Unauthorized, ())) } - - Outcome::Success(AdminAuth("admin".to_string())) - } - } -} \ No newline at end of file diff --git a/src/api/add_task.rs b/src/api/add_task.rs index 137e3ca..23595df 100644 --- a/src/api/add_task.rs +++ b/src/api/add_task.rs @@ -2,7 +2,7 @@ use serde::Deserialize; use rocket::{State, http::Status}; use rocket::serde::json::Json; use crate::Pool; -use crate::admin_auth::AdminAuth; +use crate::auth::Auth; use crate::models::task::Task; use crate::models::task::is_valid_task_status; @@ -17,10 +17,10 @@ pub struct AddTaskData { } #[rocket::post("/api/v1/add_task", data = "")] -pub async fn add_task(pool: &State, input: Json, auth: AdminAuth) -> Result<(), Status> { +pub async fn add_task(pool: &State, input: Json, auth: Auth) -> Result<(), Status> { let pool = pool.inner(); let data = input.into_inner(); - let admin_username = auth.0; + let admin_username = auth.0.username; let task_id_end = data.task_id .char_indices() diff --git a/src/api/upload_file_wup.rs b/src/api/upload_file_wup.rs index 319e28f..43403f2 100644 --- a/src/api/upload_file_wup.rs +++ b/src/api/upload_file_wup.rs @@ -5,7 +5,7 @@ use rocket::{State, http::Status}; use rocket::serde::json::Json; use md5::{Md5, Digest}; use crate::Pool; -use crate::admin_auth::AdminAuth; +use crate::auth::Auth; use crate::models::file_wup::{is_valid_countries, is_valid_file_notify_conditions, is_valid_file_type, is_valid_languages}; use crate::{crypto::wiiu::encrypt_wiiu, models::file_wup::FileWUPAttributes}; use aws_sdk_s3::Client as S3Client; @@ -34,12 +34,12 @@ pub async fn upload_file_wup( pool: &State, s3: &State, input: Json, - auth: AdminAuth + auth: Auth ) -> Result<(), Status> { let pool = pool.inner(); let s3 = s3.inner(); let data = input.into_inner(); - let admin_username = auth.0; + let admin_username = auth.0.username; info!("starting WUP upload for task: {} by user: {}", data.task_id, admin_username); diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..c39cd3e --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,67 @@ +use std::env; +use keycloak_helper::types::TokenValidationResponse; +use keycloak_helper::errors::TokenValidationError; +use keycloak_helper::validate::validate_token; +use rocket::{Request, http::Status, request::{FromRequest, Outcome}}; + +pub struct AuthResponse { + pub keycloak: bool, + pub username: String, + pub response: Option, +} + +pub struct Auth(pub AuthResponse); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Auth { + type Error = (); + + async fn from_request(req: &'r Request<'_>) -> Outcome { + let force_keycloak = env::var("FORCE_KEYCLOAK") + .map(|v| v == "true") + .unwrap_or(false); + + let token = match req.headers().get_one("Authorization") { + Some(auth) => match auth.strip_prefix("Bearer ") { + Some(token) => token.to_string(), + None => return Outcome::Error((Status::BadRequest, ())) + }, + None => return Outcome::Error((Status::BadRequest, ())), + }; + + if !force_keycloak { // Check Local Token + let local_token = env::var("AUTH_TOKEN"); + + if let Ok(local_token) = local_token { + if token == local_token.as_str() { + return Outcome::Success(Auth(AuthResponse{ + keycloak: false, + username: "admin".to_string(), + response: None, + })) + } + } + } + + // Validate Token with Keycloak + match validate_token(&token, Some(vec!["boss", "admin"]), false).await { + Ok(res) => return Outcome::Success(Auth(AuthResponse{ + keycloak: true, + username: res.username.clone(), + response: Some(res), + })), + Err(e) => match e { + TokenValidationError::MissingEnvs(envs) => { + println!("Missing Environment Variables: {envs}"); + return Outcome::Error((Status::InternalServerError, ())) + }, + TokenValidationError::RequestFailed(e) => { + println!("Request to Keycloak Failed: {e}"); + return Outcome::Error((Status::InternalServerError, ())) + }, + TokenValidationError::Inactive => return Outcome::Error((Status::Unauthorized, ())), + TokenValidationError::Forbidden => return Outcome::Error((Status::Unauthorized, ())), + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 843171a..79f05b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ mod services; mod database; mod api; mod crypto; -mod admin_auth; +mod auth; type Pool = sqlx::Pool;