use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProtectedResourceMetadata {
pub resource: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jwks_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scopes_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bearer_methods_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_signing_alg_values_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_documentation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_policy_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_tos_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls_client_certificate_bound_access_tokens: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_details_types_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dpop_signing_alg_values_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dpop_bound_access_tokens_required: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrmConfig {
pub resource: String,
#[serde(default)]
pub authorization_servers: Vec<String>,
#[serde(default)]
pub jwks_uri: Option<String>,
#[serde(default)]
pub scopes_supported: Vec<String>,
#[serde(default = "default_bearer_methods")]
pub bearer_methods_supported: Vec<String>,
#[serde(default)]
pub resource_signing_alg_values_supported: Vec<String>,
#[serde(default)]
pub resource_name: Option<String>,
#[serde(default)]
pub resource_documentation: Option<String>,
#[serde(default)]
pub resource_policy_uri: Option<String>,
#[serde(default)]
pub resource_tos_uri: Option<String>,
}
fn default_bearer_methods() -> Vec<String> {
vec!["header".to_string()]
}
impl Default for PrmConfig {
fn default() -> Self {
Self {
resource: String::new(),
authorization_servers: Vec::new(),
jwks_uri: None,
scopes_supported: Vec::new(),
bearer_methods_supported: default_bearer_methods(),
resource_signing_alg_values_supported: Vec::new(),
resource_name: None,
resource_documentation: None,
resource_policy_uri: None,
resource_tos_uri: None,
}
}
}
impl From<PrmConfig> for ProtectedResourceMetadata {
fn from(config: PrmConfig) -> Self {
ProtectedResourceMetadata {
resource: config.resource,
authorization_servers: if config.authorization_servers.is_empty() {
None
} else {
Some(config.authorization_servers)
},
jwks_uri: config.jwks_uri,
scopes_supported: if config.scopes_supported.is_empty() {
None
} else {
Some(config.scopes_supported)
},
bearer_methods_supported: if config.bearer_methods_supported.is_empty() {
None
} else {
Some(config.bearer_methods_supported)
},
resource_signing_alg_values_supported: if config.resource_signing_alg_values_supported.is_empty() {
None
} else {
Some(config.resource_signing_alg_values_supported)
},
resource_name: config.resource_name,
resource_documentation: config.resource_documentation,
resource_policy_uri: config.resource_policy_uri,
resource_tos_uri: config.resource_tos_uri,
tls_client_certificate_bound_access_tokens: None,
authorization_details_types_supported: None,
dpop_signing_alg_values_supported: None,
dpop_bound_access_tokens_required: None,
}
}
}
pub async fn prm_handler(State(config): State<PrmConfig>) -> Response {
let metadata: ProtectedResourceMetadata = config.into();
match serde_json::to_string(&metadata) {
Ok(json) => {
([("content-type", "application/json")], json).into_response()
}
Err(e) => {
tracing::error!("Failed to serialize PRM: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
pub fn prm_route() -> axum::routing::MethodRouter<PrmConfig> {
axum::routing::get(prm_handler)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prm_config_to_metadata() {
let config = PrmConfig {
resource: "https://api.example.com".to_string(),
authorization_servers: vec!["https://idm.example.com".to_string()],
scopes_supported: vec!["openid".to_string(), "profile".to_string()],
bearer_methods_supported: vec!["header".to_string()],
..Default::default()
};
let metadata: ProtectedResourceMetadata = config.into();
assert_eq!(metadata.resource, "https://api.example.com");
assert_eq!(
metadata.authorization_servers,
Some(vec!["https://idm.example.com".to_string()])
);
assert_eq!(
metadata.scopes_supported,
Some(vec!["openid".to_string(), "profile".to_string()])
);
}
#[test]
fn test_metadata_serialization() {
let metadata = ProtectedResourceMetadata {
resource: "https://api.example.com".to_string(),
authorization_servers: Some(vec!["https://idm.example.com".to_string()]),
scopes_supported: Some(vec!["openid".to_string()]),
bearer_methods_supported: Some(vec!["header".to_string()]),
..Default::default()
};
let json = serde_json::to_string(&metadata).unwrap();
assert!(json.contains("\"resource\":\"https://api.example.com\""));
assert!(json.contains("\"authorization_servers\":[\"https://idm.example.com\"]"));
}
#[test]
fn test_metadata_skip_none_fields() {
let metadata = ProtectedResourceMetadata {
resource: "https://api.example.com".to_string(),
authorization_servers: None,
jwks_uri: None,
scopes_supported: None,
bearer_methods_supported: Some(vec!["header".to_string()]),
..Default::default()
};
let json = serde_json::to_string(&metadata).unwrap();
assert!(!json.contains("\"authorization_servers\""));
assert!(!json.contains("\"jwks_uri\""));
assert!(json.contains("\"bearer_methods_supported\""));
}
#[test]
fn test_empty_arrays_become_none() {
let config = PrmConfig {
resource: "https://api.example.com".to_string(),
authorization_servers: vec![], scopes_supported: vec![], ..Default::default()
};
let metadata: ProtectedResourceMetadata = config.into();
assert_eq!(metadata.authorization_servers, None);
assert_eq!(metadata.scopes_supported, None);
}
}