Implement Keycloak authentication to Admin API

This commit is contained in:
BloxerHD 2026-04-23 15:39:38 +01:00
commit 25d2e1d036
7 changed files with 91 additions and 84 deletions

View file

@ -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

View file

@ -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" }

View file

@ -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<Self, Self::Error> {
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()))
}
}
}

View file

@ -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 = "<input>")]
pub async fn add_task(pool: &State<Pool>, input: Json<AddTaskData>, auth: AdminAuth) -> Result<(), Status> {
pub async fn add_task(pool: &State<Pool>, input: Json<AddTaskData>, 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()

View file

@ -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<Pool>,
s3: &State<S3Client>,
input: Json<UploadedWUP>,
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);

67
src/auth.rs Normal file
View file

@ -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<TokenValidationResponse>,
}
pub struct Auth(pub AuthResponse);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Auth {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
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, ())),
}
}
}
}

View file

@ -13,7 +13,7 @@ mod services;
mod database;
mod api;
mod crypto;
mod admin_auth;
mod auth;
type Pool = sqlx::Pool<sqlx::Postgres>;