Initial Commit

This commit is contained in:
BloxerHD 2026-04-23 13:09:24 +01:00
commit c04b8996bf
6 changed files with 146 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target/
Cargo.lock

10
Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "keycloak-helper"
version = "0.1.0"
edition = "2024"
[dependencies]
reqwest = { version = "0.13.2", features = ["json", "form"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2.0.18"

16
src/errors.rs Normal file
View file

@ -0,0 +1,16 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum TokenValidationError {
#[error("Missing Environment Variables: {0}")]
MissingEnvs(String),
#[error("Token Inactive")]
Inactive,
#[error("Insufficient Permissions")]
Forbidden,
#[error("Keycloak Request Failed: {0}")]
RequestFailed(String),
}

3
src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod errors;
pub mod types;
pub mod validate;

43
src/types.rs Normal file
View file

@ -0,0 +1,43 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct TokenRealmAccess {
pub roles: Vec<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct TokenAccountResourceAccess {
pub roles: Vec<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct TokenResourceAccess {
pub account: TokenAccountResourceAccess,
}
#[derive(Debug, Deserialize, Clone)]
pub struct TokenValidationResponse {
pub exp: u64, // Token expiry time
pub iat: u64, // Token issue time
pub auth_time: u64,
pub jti: String,
pub iss: String,
pub aud: String,
pub sub: String,
pub sid: String,
pub acr: String,
pub allowed_origins: Option<Vec<String>>,
pub realm_access: TokenRealmAccess,
pub resource_access: TokenResourceAccess,
pub scope: String,
pub email_verified: bool,
pub name: String,
pub preferred_username: String,
pub given_name: String,
pub family_name: String,
pub email: String,
pub client_id: String, // Client that issued the token
pub username: String,
pub token_type: String,
pub active: bool,
}

72
src/validate.rs Normal file
View file

@ -0,0 +1,72 @@
use reqwest::Client;
use std::env;
use std::collections::HashMap;
use serde::Deserialize;
use crate::errors;
use crate::types;
#[derive(Debug, Deserialize)]
struct TokenActiveCheck {
active: bool
}
pub async fn validate_token(token: &str, required_roles: Option<Vec<&str>>, needs_all_roles: bool) -> Result<types::TokenValidationResponse, errors::TokenValidationError> {
let client = Client::new();
let keycloak_url = env::var("KEYCLOAK_URL")
.map_err(|_| errors::TokenValidationError::MissingEnvs("KEYCLOAK_URL".to_string()))?;
let realm = env::var("KEYCLOAK_REALM")
.map_err(|_| errors::TokenValidationError::MissingEnvs("KEYCLOAK_REALM".to_string()))?;
let client_id = env::var("KEYCLOAK_CLIENT_ID")
.map_err(|_| errors::TokenValidationError::MissingEnvs("KEYCLOAK_CLIENT_ID".to_string()))?;
let client_secret = env::var("KEYCLOAK_CLIENT_SECRET")
.map_err(|_| errors::TokenValidationError::MissingEnvs("KEYCLOAK_CLIENT_SECRET".to_string()))?;
let url = format!("{}/realms/{}/protocol/openid-connect/token/introspect",
keycloak_url,
realm
);
let mut form = HashMap::new();
form.insert("token", token);
form.insert("client_id", &client_id);
form.insert("client_secret", &client_secret);
let res = client.post(url).form(&form).send().await
.map_err(|e| errors::TokenValidationError::RequestFailed(format!("{e}")))?;
let text = res.text().await
.map_err(|_| errors::TokenValidationError::RequestFailed("Failed to Get Response Text".to_string()))?;
let json: TokenActiveCheck = serde_json::from_str(&text)
.map_err(|e| errors::TokenValidationError::RequestFailed(format!("Failed to Map Response to Active Validation JSON: {e}")))?;
if !json.active { return Err(errors::TokenValidationError::Inactive) }
let json: types::TokenValidationResponse = serde_json::from_str(&text)
.map_err(|e| errors::TokenValidationError::RequestFailed(format!("Failed to Map Response to Response JSON: {e}")))?;
match required_roles {
Some(required_roles) => {
let roles = json.realm_access.roles.clone();
let mut included = 0;
for role in required_roles {
if !roles.contains(&role.to_string()) {match needs_all_roles {
true => return Err(errors::TokenValidationError::Forbidden),
false => (),
}} else { included += 1 }
}
if included <= 0 { return Err(errors::TokenValidationError::Forbidden) }
},
None => (),
}
Ok(json)
}