use crate::errors::{AuthError, Result};
pub fn validate_resource_indicators(resources: &[String]) -> Result<()> {
for res in resources {
if res.is_empty() {
return Err(AuthError::validation(
"resource parameter must not be empty".to_string(),
));
}
let parsed = url::Url::parse(res).map_err(|e| {
AuthError::validation(format!(
"resource indicator is not a valid URI: {res} ({e})"
))
})?;
if parsed.fragment().is_some() {
return Err(AuthError::validation(format!(
"resource indicator must not contain a fragment: {res}"
)));
}
if parsed.scheme().is_empty() {
return Err(AuthError::validation(format!(
"resource indicator must be an absolute URI: {res}"
)));
}
}
Ok(())
}
pub fn validate_token_resource_subset(
token_resources: &[String],
authz_resources: &[String],
) -> Result<()> {
for res in token_resources {
if !authz_resources.contains(res) {
return Err(AuthError::validation(format!(
"resource '{res}' was not requested in the authorization request"
)));
}
}
Ok(())
}
pub fn audience_from_resources(resources: &[String]) -> Option<Vec<String>> {
if resources.is_empty() {
None
} else {
Some(resources.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_https_resource() {
let res = vec!["https://api.example.com/v1".to_string()];
assert!(validate_resource_indicators(&res).is_ok());
}
#[test]
fn valid_multiple_resources() {
let res = vec![
"https://api.example.com/v1".to_string(),
"https://data.example.com".to_string(),
];
assert!(validate_resource_indicators(&res).is_ok());
}
#[test]
fn rejects_empty_resource() {
let res = vec!["".to_string()];
assert!(validate_resource_indicators(&res).is_err());
}
#[test]
fn rejects_fragment() {
let res = vec!["https://api.example.com/v1#section".to_string()];
let err = validate_resource_indicators(&res).unwrap_err();
assert!(err.to_string().contains("fragment"));
}
#[test]
fn rejects_relative_uri() {
let res = vec!["/relative/path".to_string()];
assert!(validate_resource_indicators(&res).is_err());
}
#[test]
fn subset_check_passes() {
let authz = vec![
"https://a.example.com".to_string(),
"https://b.example.com".to_string(),
];
let token = vec!["https://a.example.com".to_string()];
assert!(validate_token_resource_subset(&token, &authz).is_ok());
}
#[test]
fn subset_check_fails_on_extra_resource() {
let authz = vec!["https://a.example.com".to_string()];
let token = vec![
"https://a.example.com".to_string(),
"https://c.example.com".to_string(),
];
assert!(validate_token_resource_subset(&token, &authz).is_err());
}
#[test]
fn audience_from_resources_none_when_empty() {
assert!(audience_from_resources(&[]).is_none());
}
#[test]
fn audience_from_resources_returns_list() {
let res = vec!["https://api.example.com".to_string()];
let aud = audience_from_resources(&res).unwrap();
assert_eq!(aud, res);
}
}