use crate::api::ApiState;
use axum::{
Form,
extract::State,
http::{HeaderMap, StatusCode},
response::Json,
};
use base64::{Engine as _, engine::general_purpose};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use std::collections::HashMap;
use tracing::{debug, error};
use url::Url;
use uuid::Uuid;
#[derive(Debug, Deserialize)]
pub struct IntrospectRequest {
pub token: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_type_hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct IntrospectResponse {
pub active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: 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<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct PARRequest {
pub response_type: String,
pub client_id: String,
pub redirect_uri: String,
pub scope: Option<String>,
pub state: Option<String>,
pub nonce: Option<String>,
pub code_challenge: Option<String>,
pub code_challenge_method: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PARResponse {
pub request_uri: String,
pub expires_in: u64,
}
async fn verify_client_credentials(
state: &State<ApiState>,
client_id: &str,
client_secret: &str,
) -> Result<bool, (StatusCode, Json<JsonValue>)> {
let client_key = format!("oauth2_client:{}", client_id);
let client_data = match state.auth_framework.storage().get_kv(&client_key).await {
Ok(Some(bytes)) => match serde_json::from_slice::<serde_json::Value>(&bytes) {
Ok(v) => v,
Err(_) => {
error!(
"Introspect: failed to deserialize client record for {}",
client_id
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": "server_error",
"error_description": "Internal server error"
})),
));
}
},
Ok(None) => {
return Ok(false);
}
Err(e) => {
error!(
"Introspect: storage error looking up client {}: {}",
client_id, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": "server_error",
"error_description": "Internal server error"
})),
));
}
};
let stored_secret = client_data["client_secret"].as_str().unwrap_or("");
Ok(
crate::security::timing_protection::constant_time_string_compare(
client_secret,
stored_secret,
),
)
}
pub async fn introspect_token(
state: State<ApiState>,
headers: HeaderMap,
form: Form<IntrospectRequest>,
) -> Result<Json<IntrospectResponse>, (StatusCode, Json<JsonValue>)> {
debug!("Processing token introspection request");
let auth_header = headers.get(axum::http::header::AUTHORIZATION);
let authenticated = match auth_header {
Some(value) => {
let value_str = value.to_str().unwrap_or("");
if value_str.starts_with("Bearer ") {
debug!("Introspect rejected: Bearer auth is not allowed");
return Err((
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "invalid_client",
"error_description": "Bearer token authentication is not supported for token introspection"
})),
));
} else if let Some(encoded) = value_str.strip_prefix("Basic ") {
match general_purpose::STANDARD.decode(encoded) {
Ok(decoded_bytes) => {
let decoded = String::from_utf8_lossy(&decoded_bytes);
let mut parts = decoded.splitn(2, ':');
let basic_client_id = parts.next().unwrap_or("").to_string();
let basic_client_secret = parts.next().unwrap_or("").to_string();
verify_client_credentials(&state, &basic_client_id, &basic_client_secret)
.await?
}
Err(_) => {
debug!("Introspect rejected: invalid Basic auth encoding");
return Err((
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "invalid_client",
"error_description": "Invalid Basic authentication encoding"
})),
));
}
}
} else {
debug!("Introspect rejected: unknown auth scheme");
return Err((
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "invalid_client",
"error_description": "Unsupported authentication scheme"
})),
));
}
}
None => {
match (&form.client_id, &form.client_secret) {
(Some(id), Some(secret)) => verify_client_credentials(&state, id, secret).await?,
_ => {
debug!("Introspect rejected: missing client credentials");
return Err((
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "invalid_client",
"error_description": "client_id and client_secret are required"
})),
));
}
}
}
};
if !authenticated {
debug!("Introspect rejected: invalid client credentials");
return Err((
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "invalid_client",
"error_description": "Client authentication failed"
})),
));
}
let token_manager = state.auth_framework.token_manager();
match token_manager.validate_jwt_token(&form.token) {
Ok(claims) => {
let revocation_key = format!("revoked_token:{}", claims.jti);
if let Ok(Some(_)) = state.auth_framework.storage().get_kv(&revocation_key).await {
debug!(
"Token introspection: token has been revoked (jti: {})",
claims.jti
);
return Ok(Json(IntrospectResponse {
active: false,
sub: None,
client_id: None,
scope: None,
exp: None,
iat: None,
nbf: None,
iss: None,
aud: None,
jti: None,
token_type: None,
username: None,
}));
}
debug_assert!(!claims.sub.is_empty(), "Token subject should not be empty");
Ok(Json(IntrospectResponse {
active: true,
sub: Some(claims.sub.clone()),
client_id: claims.client_id.clone(),
scope: Some(claims.scope.clone()),
exp: Some(claims.exp),
iat: Some(claims.iat),
nbf: Some(claims.nbf),
iss: Some(claims.iss.clone()),
aud: Some(JsonValue::String(claims.aud.clone())),
jti: Some(claims.jti.clone()),
token_type: Some("Bearer".to_string()),
username: Some(claims.sub),
}))
}
Err(_e) => {
debug!("Token introspection: token is inactive");
Ok(Json(IntrospectResponse {
active: false,
sub: None,
client_id: None,
scope: None,
exp: None,
iat: None,
nbf: None,
iss: None,
aud: None,
jti: None,
token_type: None,
username: None,
}))
}
}
}
pub async fn pushed_authorization_request(
State(state): State<ApiState>,
Form(req): Form<PARRequest>,
) -> (StatusCode, Json<PARResponse>) {
debug!("Processing PAR request for client_id={}", req.client_id);
if Url::parse(&req.redirect_uri).is_err() {
return (
StatusCode::BAD_REQUEST,
Json(PARResponse {
request_uri: String::new(),
expires_in: 0,
}),
);
}
let request_id = Uuid::new_v4().to_string();
let request_uri = format!("urn:ietf:params:oauth:request_uri:{}", request_id);
let par_data = json!({
"response_type": req.response_type,
"client_id": req.client_id,
"redirect_uri": req.redirect_uri,
"scope": req.scope,
"state": req.state,
"nonce": req.nonce,
"code_challenge": req.code_challenge,
"code_challenge_method": req.code_challenge_method,
});
let storage_key = format!("par_request:{}", request_id);
if let Err(e) = state
.auth_framework
.storage()
.store_kv(
&storage_key,
par_data.to_string().as_bytes(),
Some(std::time::Duration::from_secs(90)),
)
.await
{
error!("Failed to store PAR request: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(PARResponse {
request_uri: String::new(),
expires_in: 0,
}),
);
}
(
StatusCode::CREATED,
Json(PARResponse {
request_uri,
expires_in: 90,
}),
)
}
pub async fn device_authorization(
State(state): State<ApiState>,
Form(form): Form<HashMap<String, String>>,
) -> Result<Json<JsonValue>, (StatusCode, Json<JsonValue>)> {
if form.get("client_id").map(|s| s.is_empty()).unwrap_or(true) {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({
"error": "invalid_request",
"error_description": "client_id is required"
})),
));
}
let device_code = format!("dc_{}", Uuid::new_v4().simple());
let user_code = generate_user_code();
let verification_uri = "/device";
let expires_in: u64 = 600;
let device_data = json!({
"client_id": form.get("client_id").cloned().unwrap_or_default(),
"scope": form.get("scope").cloned().unwrap_or_default(),
"user_code": user_code,
"authorized": false
});
state
.auth_framework
.storage()
.store_kv(
&format!("device:{}", device_code),
device_data.to_string().as_bytes(),
Some(std::time::Duration::from_secs(expires_in)),
)
.await
.map_err(|e| {
error!("Failed to store device authorization request: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": "server_error",
"error_description": "Failed to initiate device authorization flow"
})),
)
})?;
debug!(
"Device authorization initiated for client_id={}",
form.get("client_id")
.map(String::as_str)
.unwrap_or_default()
);
Ok(Json(json!({
"device_code": device_code,
"user_code": user_code,
"verification_uri": verification_uri,
"verification_uri_complete": format!("{}?user_code={}", verification_uri, user_code),
"expires_in": expires_in,
"interval": 5
})))
}
pub async fn ciba_backchannel_auth(
State(state): State<ApiState>,
Form(form): Form<HashMap<String, String>>,
) -> Result<Json<JsonValue>, (StatusCode, Json<JsonValue>)> {
let login_hint = form
.get("login_hint")
.or_else(|| form.get("login_hint_token"))
.or_else(|| form.get("id_token_hint"))
.cloned()
.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(json!({
"error": "invalid_request",
"error_description":
"One of login_hint, login_hint_token, or id_token_hint is required"
})),
)
})?;
let auth_req_id = Uuid::new_v4().to_string();
let expires_in: u64 = 120;
let ciba_data = json!({
"login_hint": login_hint,
"client_id": form.get("client_id").cloned().unwrap_or_default(),
"scope": form.get("scope").cloned().unwrap_or_default(),
"binding_message": form.get("binding_message").cloned(),
"status": "pending"
});
state
.auth_framework
.storage()
.store_kv(
&format!("ciba:{}", auth_req_id),
ciba_data.to_string().as_bytes(),
Some(std::time::Duration::from_secs(expires_in)),
)
.await
.map_err(|e| {
error!("Failed to store CIBA request: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": "server_error",
"error_description": "Failed to initiate backchannel authentication"
})),
)
})?;
debug!("CIBA request created: auth_req_id={}", auth_req_id);
Ok(Json(json!({
"auth_req_id": auth_req_id,
"expires_in": expires_in,
"interval": 5
})))
}
fn generate_user_code() -> String {
use rand::RngExt;
const CHARS: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let mut rng = rand::rng();
(0..8)
.map(|_| CHARS[rng.random_range(0..CHARS.len())] as char)
.collect()
}