use bytes::Bytes;
use std::sync::Arc;
use crate::error::{Error, ErrorCode};
pub use volga_oauth_core::{
BearerChallenge, OAuthError, OAuthErrorCode, ProtectedResourceMetadata,
WELL_KNOWN_PROTECTED_RESOURCE, canonicalize_resource_uri, protected_resource_metadata_url,
};
#[derive(Debug, Clone, Default)]
pub struct OAuthResourceOptions {
metadata: ProtectedResourceMetadata,
}
impl OAuthResourceOptions {
pub fn with_resource(mut self, uri: impl Into<String>) -> Self {
self.metadata.resource = uri.into();
self
}
pub fn with_authorization_servers<I, S>(mut self, servers: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.metadata = self.metadata.with_authorization_servers(servers);
self
}
pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.metadata = self.metadata.with_scopes(scopes);
self
}
pub fn with_metadata<F>(mut self, config: F) -> Self
where
F: FnOnce(ProtectedResourceMetadata) -> ProtectedResourceMetadata,
{
self.metadata = config(self.metadata);
self
}
pub(crate) fn resolve(self, base_url: &str) -> Result<OAuthResource, Error> {
let mut metadata = self.metadata;
let resource = if metadata.resource.is_empty() {
base_url
} else {
metadata.resource.as_str()
};
let resource = canonicalize_resource_uri(resource).map_err(config_error)?;
let metadata_url = protected_resource_metadata_url(&resource).map_err(config_error)?;
let challenge = BearerChallenge::new()
.with_resource_metadata(metadata_url.as_str())
.to_string();
metadata.resource = resource;
Ok(OAuthResource {
body: serde_json::to_vec(&metadata).map_err(Error::from)?.into(),
metadata_path: well_known_path(&metadata_url).into(),
metadata_url: metadata_url.into(),
challenge: challenge.into(),
resource: metadata.resource.into(),
})
}
}
#[derive(Debug, Clone)]
pub(crate) struct OAuthResource {
pub(crate) body: Bytes,
pub(crate) metadata_url: Arc<str>,
pub(crate) metadata_path: Arc<str>,
pub(crate) challenge: Arc<str>,
pub(crate) resource: Arc<str>,
}
fn well_known_path(url: &str) -> &str {
url.find("://")
.map(|scheme| &url[scheme + 3..])
.and_then(|rest| rest.find('/').map(|path| &rest[path..]))
.unwrap_or(WELL_KNOWN_PROTECTED_RESOURCE)
}
fn config_error(err: OAuthError) -> Error {
Error::new(ErrorCode::InternalError, err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_derives_resource_from_base_url() {
let resource = OAuthResourceOptions::default()
.resolve("http://127.0.0.1:3000/mcp")
.unwrap();
assert_eq!(
&*resource.metadata_url,
"http://127.0.0.1:3000/.well-known/oauth-protected-resource/mcp"
);
assert_eq!(
&*resource.metadata_path,
"/.well-known/oauth-protected-resource/mcp"
);
let doc: ProtectedResourceMetadata = serde_json::from_slice(&resource.body).unwrap();
assert_eq!(doc.resource, "http://127.0.0.1:3000/mcp");
}
#[test]
fn it_canonicalizes_resource_override() {
let resource = OAuthResourceOptions::default()
.with_resource("HTTPS://API.Example.COM:443/mcp")
.resolve("http://127.0.0.1:3000/mcp")
.unwrap();
let doc: ProtectedResourceMetadata = serde_json::from_slice(&resource.body).unwrap();
assert_eq!(doc.resource, "https://api.example.com/mcp");
assert_eq!(
&*resource.metadata_url,
"https://api.example.com/.well-known/oauth-protected-resource/mcp"
);
}
#[test]
fn it_rejects_invalid_resource() {
let err = OAuthResourceOptions::default()
.with_resource("not a uri")
.resolve("http://127.0.0.1:3000/mcp")
.unwrap_err();
assert_eq!(err.code, ErrorCode::InternalError);
}
#[test]
fn it_serializes_authorization_servers_and_scopes() {
let resource = OAuthResourceOptions::default()
.with_authorization_servers(["https://auth.example.com"])
.with_scopes(["mcp:tools"])
.resolve("http://127.0.0.1:3000/mcp")
.unwrap();
let doc: ProtectedResourceMetadata = serde_json::from_slice(&resource.body).unwrap();
assert_eq!(doc.authorization_servers, ["https://auth.example.com"]);
assert_eq!(doc.scopes_supported, ["mcp:tools"]);
}
#[test]
fn it_passes_through_full_metadata_fields() {
let resource = OAuthResourceOptions::default()
.with_metadata(|md| md.with_resource_name("Weather MCP"))
.resolve("http://127.0.0.1:3000/mcp")
.unwrap();
let doc: ProtectedResourceMetadata = serde_json::from_slice(&resource.body).unwrap();
assert_eq!(doc.resource_name.as_deref(), Some("Weather MCP"));
}
#[test]
fn it_prerenders_a_parseable_challenge() {
let resource = OAuthResourceOptions::default()
.resolve("http://127.0.0.1:3000/mcp")
.unwrap();
let challenge = BearerChallenge::parse(&resource.challenge).unwrap();
assert_eq!(
challenge.resource_metadata(),
Some("http://127.0.0.1:3000/.well-known/oauth-protected-resource/mcp")
);
}
#[test]
fn it_mounts_at_well_known_root_for_root_endpoint() {
let resource = OAuthResourceOptions::default()
.resolve("http://127.0.0.1:3000/")
.unwrap();
assert_eq!(&*resource.metadata_path, WELL_KNOWN_PROTECTED_RESOURCE);
}
}