use std::sync::Arc;
use core::fmt;
use core::str::FromStr;
pub const WELL_KNOWN: &str = "/.well-known/oauth-protected-resource";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum BearerMethod {
#[default]
Header,
}
impl BearerMethod {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Header => "header",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ResourceUri(Arc<str>);
impl ResourceUri {
pub fn new(value: &str) -> Result<Self, ResourceUriError> {
value.parse()
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn from_parts(origin: &str, path: &str) -> Result<Self, ResourceUriError> {
let base = origin.trim_end_matches('/');
let trimmed = path.trim_matches('/');
let joined = if trimmed.is_empty() {
base.to_owned()
} else {
format!("{base}/{trimmed}")
};
Self::new(&joined)
}
#[must_use]
pub fn metadata_path(&self) -> String {
let Some(path) = self.path() else {
return WELL_KNOWN.to_owned();
};
format!("{WELL_KNOWN}{path}")
}
pub fn metadata_url(&self) -> Result<String, ResourceUriError> {
let origin = self.origin().ok_or(ResourceUriError::NoOrigin)?;
Ok(format!("{origin}{}", self.metadata_path()))
}
fn origin(&self) -> Option<&str> {
let after_scheme = self.0.find("://")? + 3;
let end = self.0[after_scheme..]
.find('/')
.map_or(self.0.len(), |offset| after_scheme + offset);
Some(&self.0[..end])
}
fn path(&self) -> Option<&str> {
let after_scheme = self.0.find("://")? + 3;
let slash = self.0[after_scheme..].find('/')? + after_scheme;
Some(&self.0[slash..])
}
}
impl FromStr for ResourceUri {
type Err = ResourceUriError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ResourceUriError::Empty);
}
let Some(scheme_end) = trimmed.find("://") else {
return Err(ResourceUriError::NotAbsolute);
};
let scheme = &trimmed[..scheme_end];
if scheme.is_empty()
|| !scheme
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
{
return Err(ResourceUriError::NotAbsolute);
}
let mut canonical = String::with_capacity(trimmed.len());
canonical.push_str(&scheme.to_ascii_lowercase());
canonical.push_str("://");
let rest = &trimmed[scheme_end + 3..];
let (authority, path) = rest
.find('/')
.map_or((rest, ""), |slash| (&rest[..slash], &rest[slash..]));
if authority.is_empty() {
return Err(ResourceUriError::NoAuthority);
}
canonical.push_str(&drop_default_port(authority, scheme));
let path = path.strip_suffix('/').unwrap_or(path);
canonical.push_str(path);
Ok(Self(Arc::from(canonical.as_str())))
}
}
fn drop_default_port(authority: &str, scheme: &str) -> String {
let default = match scheme.to_ascii_lowercase().as_str() {
"https" => Some("443"),
"http" => Some("80"),
_ => None,
};
let separator = if authority.starts_with('[') {
authority.find(']').and_then(|close| {
authority[close + 1..]
.find(':')
.map(|in_tail| close + 1 + in_tail)
})
} else {
authority.rfind(':')
};
let Some(separator) = separator else {
return authority.to_owned();
};
debug_assert!(authority.is_char_boundary(separator));
let (host, port) = authority.split_at(separator);
let port = &port[1..];
if default == Some(port) {
return host.to_owned();
}
authority.to_owned()
}
impl TryFrom<&str> for ResourceUri {
type Error = ResourceUriError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl fmt::Display for ResourceUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ResourceUriError {
#[error("the resource identifier is empty")]
Empty,
#[error("the resource identifier is not an absolute URI with an http(s) scheme")]
NotAbsolute,
#[error("the resource identifier names no host")]
NoAuthority,
#[error("the resource identifier has no origin to publish metadata under")]
NoOrigin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceMetadata {
resource: ResourceUri,
authorization_servers: Vec<Arc<str>>,
scopes_supported: Vec<Arc<str>>,
bearer_methods_supported: Vec<BearerMethod>,
resource_name: Option<Arc<str>>,
}
impl ResourceMetadata {
#[must_use]
pub fn new(resource: ResourceUri) -> Self {
Self {
resource,
authorization_servers: Vec::new(),
scopes_supported: Vec::new(),
bearer_methods_supported: vec![BearerMethod::Header],
resource_name: None,
}
}
#[must_use]
pub fn with_authorization_server(mut self, issuer: impl Into<Arc<str>>) -> Self {
self.authorization_servers.push(issuer.into());
self
}
#[must_use]
pub fn with_scope(mut self, scope: impl Into<Arc<str>>) -> Self {
self.scopes_supported.push(scope.into());
self
}
#[must_use]
pub fn with_name(mut self, name: impl Into<Arc<str>>) -> Self {
self.resource_name = Some(name.into());
self
}
#[must_use]
pub const fn resource(&self) -> &ResourceUri {
&self.resource
}
#[must_use]
pub fn authorization_servers(&self) -> &[Arc<str>] {
&self.authorization_servers
}
#[must_use]
pub fn scopes_supported(&self) -> &[Arc<str>] {
&self.scopes_supported
}
#[must_use]
pub fn bearer_methods_supported(&self) -> &[BearerMethod] {
&self.bearer_methods_supported
}
pub fn challenge(&self) -> Result<String, ResourceUriError> {
Ok(format!(
"Bearer resource_metadata=\"{}\"",
self.resource.metadata_url()?
))
}
}
#[cfg(test)]
#[path = "resource_metadata/tests.rs"]
mod tests;