Initial Commit
This commit is contained in:
commit
c04b8996bf
6 changed files with 146 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
Cargo.lock
|
||||
10
Cargo.toml
Normal file
10
Cargo.toml
Normal 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
16
src/errors.rs
Normal 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
3
src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod errors;
|
||||
pub mod types;
|
||||
pub mod validate;
|
||||
43
src/types.rs
Normal file
43
src/types.rs
Normal 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
72
src/validate.rs
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue