use base64::{engine::general_purpose::STANDARD, Engine};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntrospectionResult {
pub active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
}
impl IntrospectionResult {
#[must_use]
pub fn is_valid(&self) -> bool {
if !self.active {
return false;
}
if let Some(exp) = self.exp {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if exp < now {
return false;
}
}
true
}
}
pub struct IntrospectionClient;
impl IntrospectionClient {
pub async fn introspect(
introspection_url: &str,
token: &str,
client_id: &str,
client_secret: &str,
) -> Result<IntrospectionResult, IntrospectionError> {
let body = do_introspect(introspection_url, token, client_id, client_secret).await?;
serde_json::from_str(&body)
.map_err(|e| IntrospectionError::ParseError(e.to_string()))
}
}
#[derive(Debug)]
pub enum IntrospectionError {
ConnectionFailed(String),
Unauthorized(String),
ServerError { status: u16, message: String },
ParseError(String),
}
impl core::fmt::Display for IntrospectionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
IntrospectionError::ConnectionFailed(s) => {
write!(f, "Introspection connection failed: {s}")
}
IntrospectionError::Unauthorized(s) => {
write!(f, "Introspection unauthorized: {s}")
}
IntrospectionError::ServerError { status, message } => {
write!(f, "Introspection server error ({status}): {message}")
}
IntrospectionError::ParseError(s) => {
write!(f, "Introspection parse error: {s}")
}
}
}
}
impl std::error::Error for IntrospectionError {}
fn basic_auth_header(client_id: &str, client_secret: &str) -> String {
let credentials = format!("{client_id}:{client_secret}");
format!("Basic {}", STANDARD.encode(credentials))
}
async fn do_introspect(
url: &str,
token: &str,
client_id: &str,
client_secret: &str,
) -> Result<String, IntrospectionError> {
use foundation_core::url::Uri;
use foundation_netio::default_http_client;
use foundation_netio::shared::client::body_reader::try_collect_bytes;
use foundation_netio::shared::client::request::PreparedRequest;
use foundation_netio::shared::http::{
SendSafeBody, SimpleHeader, SimpleHeaders, SimpleMethod,
};
let client = default_http_client();
let uri = Uri::parse(url)
.map_err(|e| IntrospectionError::ConnectionFailed(format!("invalid URL: {e}")))?;
let body_text = format!("token={}", urlencoding::encode(token));
let mut headers = SimpleHeaders::new();
headers.insert(
SimpleHeader::CONTENT_TYPE,
vec!["application/x-www-form-urlencoded".into()],
);
headers.insert(
SimpleHeader::AUTHORIZATION,
vec![basic_auth_header(client_id, client_secret)],
);
let req = PreparedRequest {
method: SimpleMethod::POST,
url: uri,
headers,
body: SendSafeBody::Text(body_text),
extensions: Default::default(),
};
let resp = client
.send_async(req)
.await
.map_err(|e| IntrospectionError::ConnectionFailed(e.to_string()))?;
let status: usize = resp.get_status().into();
let body = try_collect_bytes(resp.take_body())
.map(|b| String::from_utf8_lossy(&b).to_string())
.map_err(|e| IntrospectionError::ConnectionFailed(format!("read body: {e}")))?;
if status == 401 {
return Err(IntrospectionError::Unauthorized(body));
}
if !(200..300).contains(&status) {
return Err(IntrospectionError::ServerError {
status: status as u16,
message: body,
});
}
Ok(body)
}