Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/http_server_bearer_auth.enhancement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added bearer authentication strategy to HTTP server sources. The `http_server`, `heroku_logs`, and `websocket_server` components now support `strategy = "bearer"` in their `auth` configuration, allowing token-based authentication via the `Authorization: Bearer <token>` header.

authors: steveduan-IDME
142 changes: 139 additions & 3 deletions src/common/http/server_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ pub enum HttpServerAuthConfig {
password: SensitiveString,
},

/// Bearer authentication.
///
/// The token is matched against the `Authorization` header using the `Bearer` scheme.
Bearer {
/// The bearer token to match against incoming requests.
#[configurable(metadata(docs::examples = "${TOKEN}"))]
#[configurable(metadata(docs::examples = "my-secret-token"))]
token: SensitiveString,
},

/// Custom authentication using VRL code.
///
/// Takes in request and validates it using VRL code.
Expand All @@ -69,13 +79,13 @@ impl<'de> Deserialize<'de> for HttpServerAuthConfig {
{
struct HttpServerAuthConfigVisitor;

const FIELD_KEYS: [&str; 4] = ["strategy", "username", "password", "source"];
const FIELD_KEYS: [&str; 5] = ["strategy", "username", "password", "token", "source"];

impl<'de> Visitor<'de> for HttpServerAuthConfigVisitor {
type Value = HttpServerAuthConfig;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid authentication strategy (basic or custom)")
formatter.write_str("a valid authentication strategy (basic, bearer, or custom)")
}

fn visit_map<A>(self, mut map: A) -> Result<HttpServerAuthConfig, A::Error>
Expand Down Expand Up @@ -114,13 +124,24 @@ impl<'de> Deserialize<'de> for HttpServerAuthConfig {
password: SensitiveString::from(password),
})
}
"bearer" => {
let token = fields
.remove("token")
.ok_or_else(|| Error::missing_field("token"))?;
Ok(HttpServerAuthConfig::Bearer {
token: SensitiveString::from(token),
})
}
"custom" => {
let source = fields
.remove("source")
.ok_or_else(|| Error::missing_field("source"))?;
Ok(HttpServerAuthConfig::Custom { source })
}
_ => Err(Error::unknown_variant(strategy, &["basic", "custom"])),
_ => Err(Error::unknown_variant(
strategy,
&["basic", "bearer", "custom"],
)),
}
}
}
Expand All @@ -145,6 +166,15 @@ impl HttpServerAuthConfig {
"Invalid username/password",
))
}
HttpServerAuthConfig::Bearer { token } => {
let auth = Authorization::bearer(token.inner()).map_err(|e| {
format!("Invalid bearer token: {e}")
})?;
Ok(HttpServerAuthMatcher::AuthHeader(
auth.0.encode(),
"Invalid token",
))
}
HttpServerAuthConfig::Custom { source } => {
let state = TypeState::default();

Expand Down Expand Up @@ -402,6 +432,112 @@ mod tests {
);
}

#[test]
fn config_should_support_bearer_strategy() {
let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
strategy: bearer
token: my-secret-token
"#
})
.unwrap();

if let HttpServerAuthConfig::Bearer { token } = config {
assert_eq!(token.inner(), "my-secret-token");
} else {
panic!("Expected HttpServerAuthConfig::Bearer");
}
}

#[test]
fn build_bearer_auth_should_always_work() {
let bearer_auth = HttpServerAuthConfig::Bearer {
token: random_string(16).into(),
};

let matcher = bearer_auth.build(&Default::default(), &Default::default());

assert!(matcher.is_ok());
assert!(matches!(
matcher.unwrap(),
HttpServerAuthMatcher::AuthHeader { .. }
));
}

#[test]
fn build_bearer_auth_should_use_token_related_message() {
let bearer_auth = HttpServerAuthConfig::Bearer {
token: random_string(16).into(),
};

let (_, error_message) = bearer_auth
.build(&Default::default(), &Default::default())
.unwrap()
.auth_header();
assert_eq!("Invalid token", error_message);
}

#[test]
fn bearer_auth_matcher_should_return_401_when_missing_auth_header() {
let bearer_auth = HttpServerAuthConfig::Bearer {
token: "my-token".into(),
};

let matcher = bearer_auth
.build(&Default::default(), &Default::default())
.unwrap();

let (_guard, addr) = next_addr();
let result = matcher.handle_auth(Some(&addr), &HeaderMap::new(), "/");

assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(401, error.code());
assert_eq!("No authorization header", error.message());
}

#[test]
fn bearer_auth_matcher_should_return_401_with_wrong_token() {
let bearer_auth = HttpServerAuthConfig::Bearer {
token: "my-token".into(),
};

let matcher = bearer_auth
.build(&Default::default(), &Default::default())
.unwrap();

let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, HeaderValue::from_static("Bearer wrong-token"));
let (_guard, addr) = next_addr();
let result = matcher.handle_auth(Some(&addr), &headers, "/");

assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(401, error.code());
assert_eq!("Invalid token", error.message());
}

#[test]
fn bearer_auth_matcher_should_return_ok_for_correct_token() {
let token = "my-secret-token";
let bearer_auth = HttpServerAuthConfig::Bearer {
token: token.into(),
};

let matcher = bearer_auth
.build(&Default::default(), &Default::default())
.unwrap();

let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
Authorization::bearer(token).unwrap().0.encode(),
);
let (_guard, addr) = next_addr();
let result = matcher.handle_auth(Some(&addr), &headers, "/");

assert!(result.is_ok());
}

#[test]
fn build_custom_should_fail_on_invalid_source() {
let custom_auth = HttpServerAuthConfig::Custom {
Expand Down
85 changes: 85 additions & 0 deletions src/sources/http_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,91 @@ mod tests {
assert_eq!(200, send_with_headers(addr, "", headers).await);
}

#[tokio::test]
async fn returns_401_when_required_bearer_auth_is_missing() {
components::init_test();
let (_rx, addr) = source(
vec![],
vec![],
"http_path",
"remote_ip",
"/",
"GET",
StatusCode::OK,
Some(HttpServerAuthConfig::Bearer {
token: "my-token".into(),
}),
true,
EventStatus::Delivered,
true,
None,
None,
)
.await;

assert_eq!(401, send_request(addr, "GET", "", "/").await);
}

#[tokio::test]
async fn returns_401_when_required_bearer_auth_is_wrong() {
components::init_test();
let (_rx, addr) = source(
vec![],
vec![],
"http_path",
"remote_ip",
"/",
"POST",
StatusCode::OK,
Some(HttpServerAuthConfig::Bearer {
token: "my-token".into(),
}),
true,
EventStatus::Delivered,
true,
None,
None,
)
.await;

let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
Authorization::bearer("wrong-token").unwrap().0.encode(),
);
assert_eq!(401, send_with_headers(addr, "", headers).await);
}

#[tokio::test]
async fn http_post_with_correct_bearer_auth() {
components::init_test();
let (_rx, addr) = source(
vec![],
vec![],
"http_path",
"remote_ip",
"/",
"POST",
StatusCode::OK,
Some(HttpServerAuthConfig::Bearer {
token: "my-token".into(),
}),
true,
EventStatus::Delivered,
true,
None,
None,
)
.await;

let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
Authorization::bearer("my-token").unwrap().0.encode(),
);
assert_eq!(200, send_with_headers(addr, "", headers).await);
}

#[test]
fn output_schema_definition_vector_namespace() {
let config = SimpleHttpConfig {
Expand Down
Loading