use std::collections::BTreeSet;
use std::fmt;
use ferrohealth_smart::metadata::{issuer_url, locations};
use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Issuer {
text: String,
url: Url,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct InvalidIssuer;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum EndpointError {
NotAUrl,
OtherOrigin,
Insecure,
}
impl Issuer {
pub(super) fn parse(text: &str) -> Result<Self, InvalidIssuer> {
let url = issuer_url(text).map_err(|_refused| InvalidIssuer)?;
if !url.username().is_empty() || url.password().is_some() || !canonical(text, &url) {
return Err(InvalidIssuer);
}
Ok(Self {
text: text.to_owned(),
url,
})
}
pub(super) fn as_str(&self) -> &str {
&self.text
}
pub(super) fn metadata_url(&self) -> Url {
let [oauth, _openid] = locations(&self.url);
oauth
}
pub(super) fn is_identical_to(&self, named: &str) -> bool {
named == self.text && Url::parse(named).ok().as_ref() == Some(&self.url)
}
pub(super) fn endpoint(&self, text: &str) -> Result<Url, EndpointError> {
let url = Url::parse(text).map_err(|_unparsable| EndpointError::NotAUrl)?;
if url.origin() != self.url.origin() {
return Err(EndpointError::OtherOrigin);
}
if !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() {
return Err(EndpointError::Insecure);
}
Ok(url)
}
}
fn canonical(text: &str, url: &Url) -> bool {
text == url.as_str() || (url.path() == "/" && url.as_str().strip_suffix('/') == Some(text))
}
pub(super) fn repeats_no_name(body: &[u8]) -> Result<(), serde_json::Error> {
serde_json::from_slice::<Unique>(body).map(|Unique| ())
}
struct Unique;
impl<'de> Deserialize<'de> for Unique {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(UniqueVisitor)
}
}
struct UniqueVisitor;
impl<'de> Visitor<'de> for UniqueVisitor {
type Value = Unique;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a JSON value whose objects repeat no name")
}
fn visit_bool<E: de::Error>(self, _value: bool) -> Result<Unique, E> {
Ok(Unique)
}
fn visit_i64<E: de::Error>(self, _value: i64) -> Result<Unique, E> {
Ok(Unique)
}
fn visit_u64<E: de::Error>(self, _value: u64) -> Result<Unique, E> {
Ok(Unique)
}
fn visit_f64<E: de::Error>(self, _value: f64) -> Result<Unique, E> {
Ok(Unique)
}
fn visit_str<E: de::Error>(self, _value: &str) -> Result<Unique, E> {
Ok(Unique)
}
fn visit_unit<E: de::Error>(self) -> Result<Unique, E> {
Ok(Unique)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Unique, A::Error> {
while seq.next_element::<Unique>()?.is_some() {}
Ok(Unique)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Unique, A::Error> {
let mut names = BTreeSet::new();
while let Some(name) = map.next_key::<String>()? {
if !names.insert(name) {
return Err(de::Error::custom("an object repeats a name"));
}
map.next_value::<Unique>()?;
}
Ok(Unique)
}
}
#[cfg(test)]
mod tests {
use super::*;
const ISSUER: &str = "https://as.example.org/oauth2/hospital";
fn issuer() -> Issuer {
Issuer::parse(ISSUER).expect("issuer")
}
#[test]
fn the_well_known_suffix_goes_between_host_and_path() {
assert_eq!(
issuer().metadata_url().as_str(),
"https://as.example.org/.well-known/oauth-authorization-server/oauth2/hospital"
);
let bare = Issuer::parse("https://as.example.org").expect("issuer");
assert_eq!(
bare.metadata_url().as_str(),
"https://as.example.org/.well-known/oauth-authorization-server"
);
let slash = Issuer::parse("https://as.example.org/tenant/").expect("issuer");
assert_eq!(
slash.metadata_url().as_str(),
"https://as.example.org/.well-known/oauth-authorization-server/tenant"
);
}
#[test]
fn an_issuer_outside_rfc_8414_or_its_canonical_form_is_refused() {
for text in [
"not a url",
"ftp://as.example.org",
"http://as.example.org",
"https://user@as.example.org",
"https://as.example.org/?q=1",
"https://as.example.org/#f",
"https://AS.example.org/tenant",
"https://as.example.org:443/tenant",
] {
assert_eq!(Err(InvalidIssuer), Issuer::parse(text), "{text}");
}
}
#[test]
fn a_plain_http_issuer_is_taken_on_a_loopback_host_only() {
assert!(Issuer::parse("http://127.0.0.1:8080/oauth2").is_ok());
assert!(Issuer::parse("http://localhost:8080").is_ok());
assert_eq!(
Err(InvalidIssuer),
Issuer::parse("http://as.example.org/oauth2")
);
}
#[test]
fn an_issuer_that_differs_in_text_or_url_is_not_identical() {
assert!(issuer().is_identical_to(ISSUER));
for named in [
"https://as.example.org/oauth2/other",
"https://AS.example.org/oauth2/hospital",
"https://as.example.org:443/oauth2/hospital",
"https://as.example.org/oauth2/hospital/",
] {
assert!(!issuer().is_identical_to(named), "{named}");
}
}
#[test]
fn an_endpoint_on_the_issuers_origin_is_taken() {
let url = issuer()
.endpoint(&format!("{ISSUER}/token"))
.expect("taken");
assert_eq!(url.as_str(), format!("{ISSUER}/token"));
}
#[test]
fn an_endpoint_off_the_issuers_origin_is_refused() {
for other in [
"http://as.example.org/oauth2/hospital/token",
"https://elsewhere.example.org/token",
"https://as.example.org:8443/token",
"https://as.example.org.evil.example/token",
] {
assert_eq!(
Err(EndpointError::OtherOrigin),
issuer().endpoint(other),
"{other}"
);
}
}
#[test]
fn an_endpoint_with_userinfo_or_a_fragment_is_refused() {
for other in [
"https://user@as.example.org/token",
"https://as.example.org/token#f",
] {
assert_eq!(
Err(EndpointError::Insecure),
issuer().endpoint(other),
"{other}"
);
}
assert_eq!(Err(EndpointError::NotAUrl), issuer().endpoint("token"));
}
#[test]
fn a_repeated_name_at_any_depth_is_refused() {
assert!(repeats_no_name(br#"{"a": 1, "b": {"c": [1, {"d": 2}]}}"#).is_ok());
assert!(repeats_no_name(br#"{"a": 1, "a": 2}"#).is_err());
assert!(repeats_no_name(br#"{"a": {"b": 1, "b": 1}}"#).is_err());
assert!(repeats_no_name(br#"[{"b": 1, "b": 1}]"#).is_err());
}
}