#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BearerChallenge {
pub resource_metadata: Option<String>,
pub scope: Option<String>,
pub error: Option<String>,
}
impl BearerChallenge {
#[must_use]
pub fn scopes(&self) -> Vec<&str> {
self.scope
.as_deref()
.map(|scope| scope.split_ascii_whitespace().collect())
.unwrap_or_default()
}
#[must_use]
pub fn is_insufficient_scope(&self) -> bool {
self.error.as_deref() == Some("insufficient_scope")
}
}
#[must_use]
pub fn parse_bearer(header: &str) -> Option<BearerChallenge> {
let mut rest = header.trim();
loop {
let (scheme, after) = split_token(rest)?;
let (params, next) = read_params(after);
if scheme.eq_ignore_ascii_case("bearer") {
let mut challenge = BearerChallenge::default();
for (name, value) in params {
match name.to_ascii_lowercase().as_str() {
"resource_metadata" => challenge.resource_metadata = Some(value),
"scope" => challenge.scope = Some(value),
"error" => challenge.error = Some(value),
_ => {}
}
}
return Some(challenge);
}
rest = next?;
}
}
fn split_token(text: &str) -> Option<(&str, &str)> {
let text = text.trim_start_matches([' ', '\t', ',']);
let end = text.find([' ', '\t', ',']).unwrap_or(text.len());
let (token, rest) = text.split_at(end);
(!token.is_empty()).then_some((token, rest))
}
fn read_params(text: &str) -> (Vec<(String, String)>, Option<&str>) {
let mut params = Vec::new();
let mut rest = text;
loop {
let trimmed = rest.trim_start_matches([' ', '\t', ',']);
if trimmed.is_empty() {
return (params, None);
}
let name_end = trimmed.find(['=', ' ', '\t', ',']).unwrap_or(trimmed.len());
let (name, after_name) = trimmed.split_at(name_end);
let after_name_trimmed = after_name.trim_start();
let Some(after_equals) = after_name_trimmed.strip_prefix('=') else {
return (params, Some(trimmed));
};
let after_equals = after_equals.trim_start();
let (value, remaining) = after_equals.strip_prefix('"').map_or_else(
|| {
let end = after_equals
.find([',', ' ', '\t'])
.unwrap_or(after_equals.len());
let (value, remaining) = after_equals.split_at(end);
(value.to_owned(), remaining)
},
read_quoted,
);
params.push((name.to_owned(), value));
rest = remaining;
}
}
fn read_quoted(text: &str) -> (String, &str) {
let mut value = String::new();
let mut escaped = false;
for (index, c) in text.char_indices() {
if escaped {
value.push(c);
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
let after = text.get(index + 1..).unwrap_or_default();
return (value, after);
} else {
value.push(c);
}
}
(value, "")
}
#[cfg(test)]
mod tests {
use super::parse_bearer;
#[test]
fn reads_the_three_parameters_mcp_uses() {
let challenge = parse_bearer(
r#"Bearer error="insufficient_scope", scope="files:write", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", error_description="needs write""#,
);
let challenge = challenge.unwrap_or_default();
assert!(challenge.is_insufficient_scope());
assert_eq!(challenge.scopes(), ["files:write"]);
assert_eq!(
challenge.resource_metadata.as_deref(),
Some("https://mcp.example.com/.well-known/oauth-protected-resource")
);
}
#[test]
fn finds_bearer_among_other_schemes() {
let challenge = parse_bearer(r#"Basic realm="x", Bearer scope="a b""#).unwrap_or_default();
assert_eq!(challenge.scopes(), ["a", "b"]);
}
#[test]
fn the_scheme_is_case_insensitive_and_params_may_be_bare() {
let challenge = parse_bearer("bearer error=invalid_token").unwrap_or_default();
assert_eq!(challenge.error.as_deref(), Some("invalid_token"));
}
#[test]
fn a_bare_bearer_is_a_challenge_with_nothing_in_it() {
assert_eq!(
parse_bearer("Bearer"),
Some(super::BearerChallenge::default())
);
}
#[test]
fn no_bearer_is_none() {
assert_eq!(parse_bearer(r#"Basic realm="x""#), None);
assert_eq!(parse_bearer(""), None);
}
#[test]
fn quoted_values_unescape() {
let challenge = parse_bearer(r#"Bearer scope="a \"b\"""#).unwrap_or_default();
assert_eq!(challenge.scope.as_deref(), Some(r#"a "b""#));
}
}