use serde::{Deserialize, Serialize};
pub const PROTECTED_RESOURCE_WELL_KNOWN_PATH: &str = "/.well-known/oauth-protected-resource";
pub fn resource_path(resource: &str) -> &str {
let path = crate::metadata::issuer_path(resource);
if !path.is_empty() {
if path.starts_with("/?") {
return &path[1..];
}
return path;
}
match resource.find('?') {
Some(i) => &resource[i..],
None => "",
}
}
pub fn well_known_path(resource: &str) -> String {
let path = resource_path(resource);
let mut out = String::with_capacity(PROTECTED_RESOURCE_WELL_KNOWN_PATH.len() + path.len());
out.push_str(PROTECTED_RESOURCE_WELL_KNOWN_PATH);
out.push_str(path);
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BearerMethod {
Header,
Body,
Query,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ProtectedResourceConfig {
pub resource: String,
pub authorization_servers: Vec<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
pub bearer_methods_supported: Vec<BearerMethod>,
pub resource_signing_alg_values_supported: Vec<String>,
pub resource_name: Option<String>,
pub resource_documentation: Option<String>,
pub resource_policy_uri: Option<String>,
pub resource_tos_uri: Option<String>,
pub tls_client_certificate_bound_access_tokens: bool,
pub dpop_bound_access_tokens_required: bool,
pub dpop_signing_alg_values_supported: Vec<String>,
pub authorization_details_types_supported: Vec<String>,
pub signed_metadata: Option<String>,
}
impl ProtectedResourceConfig {
pub fn new(resource: impl Into<String>, authorization_server: impl Into<String>) -> Self {
ProtectedResourceConfig {
resource: resource.into(),
authorization_servers: vec![authorization_server.into()],
jwks_uri: None,
scopes_supported: None,
bearer_methods_supported: vec![BearerMethod::Header],
resource_signing_alg_values_supported: Vec::new(),
resource_name: None,
resource_documentation: None,
resource_policy_uri: None,
resource_tos_uri: None,
tls_client_certificate_bound_access_tokens: false,
dpop_bound_access_tokens_required: false,
dpop_signing_alg_values_supported: Vec::new(),
authorization_details_types_supported: Vec::new(),
signed_metadata: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
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<BearerMethod>>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signed_metadata: Option<String>,
}
fn some_unless_empty<T>(values: Vec<T>) -> Option<Vec<T>> {
(!values.is_empty()).then_some(values)
}
impl ProtectedResourceMetadata {
pub fn from_config(config: &ProtectedResourceConfig) -> Self {
ProtectedResourceMetadata {
resource: config.resource.trim_end_matches('/').to_string(),
authorization_servers: some_unless_empty(config.authorization_servers.clone()),
jwks_uri: config.jwks_uri.clone(),
scopes_supported: config.scopes_supported.clone(),
bearer_methods_supported: some_unless_empty(config.bearer_methods_supported.clone()),
resource_signing_alg_values_supported: some_unless_empty(
config.resource_signing_alg_values_supported.clone(),
),
resource_name: config.resource_name.clone(),
resource_documentation: config.resource_documentation.clone(),
resource_policy_uri: config.resource_policy_uri.clone(),
resource_tos_uri: config.resource_tos_uri.clone(),
tls_client_certificate_bound_access_tokens: config
.tls_client_certificate_bound_access_tokens
.then_some(true),
authorization_details_types_supported: some_unless_empty(
config.authorization_details_types_supported.clone(),
),
dpop_signing_alg_values_supported: some_unless_empty(
config.dpop_signing_alg_values_supported.clone(),
),
dpop_bound_access_tokens_required: config
.dpop_bound_access_tokens_required
.then_some(true),
signed_metadata: config.signed_metadata.clone(),
}
}
pub fn well_known_path(&self) -> String {
well_known_path(&self.resource)
}
}
#[cfg(test)]
#[path = "tests/resource_metadata.rs"]
mod tests;