use axum::extract::Request;
use axum::response::Response;
use http::HeaderValue;
use http::header::WWW_AUTHENTICATE;
#[derive(Clone, Copy, Debug)]
pub enum BearerChallenge {
InvalidToken,
InsufficientScope,
NoCredentials,
}
impl BearerChallenge {
fn header_value(self) -> &'static str {
match self {
Self::InvalidToken => r#"Bearer error="invalid_token""#,
Self::InsufficientScope => r#"Bearer error="insufficient_scope""#,
Self::NoCredentials => r#"Bearer realm="api""#,
}
}
}
pub fn append_bearer_challenge(response: &mut Response, challenge: BearerChallenge) {
response.headers_mut().append(
WWW_AUTHENTICATE,
HeaderValue::from_static(challenge.header_value()),
);
}
pub fn resolve_path(req: &Request, matched_path: &str) -> String {
req.extensions()
.get::<axum::extract::NestedPath>()
.and_then(|np| strip_path_prefix(matched_path, np.as_str()))
.unwrap_or_else(|| matched_path.to_owned())
}
fn strip_path_prefix(path: &str, prefix: &str) -> Option<String> {
let rest = path.strip_prefix(prefix)?;
if rest.is_empty() {
Some("/".to_owned())
} else if rest.starts_with('/') {
Some(rest.to_owned())
} else {
None
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use axum::response::IntoResponse;
fn challenge_header(challenge: BearerChallenge) -> String {
let mut response = axum::http::StatusCode::UNAUTHORIZED.into_response();
append_bearer_challenge(&mut response, challenge);
response
.headers()
.get(WWW_AUTHENTICATE)
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_owned()
}
#[test]
fn error_challenges_carry_their_rfc_error_code() {
assert_eq!(
challenge_header(BearerChallenge::InvalidToken),
r#"Bearer error="invalid_token""#
);
assert_eq!(
challenge_header(BearerChallenge::InsufficientScope),
r#"Bearer error="insufficient_scope""#
);
}
#[test]
fn no_credentials_challenge_omits_error_code() {
let value = challenge_header(BearerChallenge::NoCredentials);
assert_eq!(value, r#"Bearer realm="api""#);
assert!(
!value.contains("error="),
"no-credentials challenge leaked an error code"
);
assert!(
value.starts_with("Bearer "),
"challenge must carry an auth-param"
);
}
#[test]
fn exact_match_returns_root() {
assert_eq!(strip_path_prefix("/cf", "/cf"), Some("/".to_owned()));
}
#[test]
fn segment_boundary_strips_correctly() {
assert_eq!(
strip_path_prefix("/cf/users", "/cf"),
Some("/users".to_owned())
);
}
#[test]
fn partial_segment_overlap_rejected() {
assert_eq!(strip_path_prefix("/cfish", "/cf"), None);
}
#[test]
fn no_prefix_match_returns_none() {
assert_eq!(strip_path_prefix("/other/path", "/cf"), None);
}
#[test]
fn nested_prefix_strips_correctly() {
assert_eq!(
strip_path_prefix("/api/v1/users", "/api/v1"),
Some("/users".to_owned())
);
}
#[test]
fn path_with_params_strips_correctly() {
assert_eq!(
strip_path_prefix("/cf/users/{id}", "/cf"),
Some("/users/{id}".to_owned())
);
}
#[test]
fn empty_prefix_returns_full_path() {
assert_eq!(strip_path_prefix("/users", ""), Some("/users".to_owned()));
}
}