60 lines
1.7 KiB
Rust
60 lines
1.7 KiB
Rust
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
|
|
use once_cell::sync::Lazy;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use std::env;
|
|
|
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
pub struct Claims {
|
|
pub sub: String,
|
|
pub preferred_username: Option<String>,
|
|
pub exp: usize,
|
|
pub iss: String,
|
|
pub aud: Option<String>,
|
|
pub realm_access: Option<RealmAccess>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
pub struct RealmAccess {
|
|
pub roles: Vec<String>,
|
|
}
|
|
|
|
static ISSUER: Lazy<String> = Lazy::new(|| env::var("ISSUER").expect("ISSUER not set"));
|
|
|
|
pub fn validate_token(token: &str, jwks: &Value) -> Result<Claims, String> {
|
|
// 1. Decode header
|
|
let header = decode_header(token).map_err(|_| "Invalid header")?;
|
|
|
|
let kid = header.kid.ok_or("Missing kid")?;
|
|
|
|
// 2. Find matching key
|
|
let keys = jwks["keys"].as_array().ok_or("Invalid JWKS")?;
|
|
|
|
let key = keys
|
|
.iter()
|
|
.find(|k| k["kid"] == kid)
|
|
.ok_or("Matching key not found")?;
|
|
|
|
// 3. Extract RSA components
|
|
let n = key["n"].as_str().ok_or("Missing n")?;
|
|
let e = key["e"].as_str().ok_or("Missing e")?;
|
|
|
|
let decoding_key =
|
|
DecodingKey::from_rsa_components(n, e).map_err(|_| "Invalid decoding key")?;
|
|
|
|
// 4. Setup validation rules
|
|
let mut validation = Validation::new(Algorithm::RS256);
|
|
|
|
validation.set_issuer(&[ISSUER.as_str()]);
|
|
|
|
// Optional but recommended:
|
|
validation.validate_exp = true;
|
|
validation.validate_aud = false; // depends on your Keycloak config
|
|
|
|
// 5. Decode & verify
|
|
let token_data = decode::<Claims>(token, &decoding_key, &validation)
|
|
.map_err(|_| "Token validation failed")?;
|
|
|
|
Ok(token_data.claims)
|
|
}
|