Skip to content

Commit 1e96a67

Browse files
feat(http_server source): Add bearer authentication strategy
- feat — new feature (new auth strategy) - http_server source — scope matching the component - Description is concise
1 parent 55be10e commit 1e96a67

File tree

3 files changed

+225
-3
lines changed

3 files changed

+225
-3
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
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.

src/common/http/server_auth.rs

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ pub enum HttpServerAuthConfig {
5252
password: SensitiveString,
5353
},
5454

55+
/// Bearer authentication.
56+
///
57+
/// The token is matched against the `Authorization` header using the `Bearer` scheme.
58+
Bearer {
59+
/// The bearer token to match against incoming requests.
60+
#[configurable(metadata(docs::examples = "${TOKEN}"))]
61+
#[configurable(metadata(docs::examples = "my-secret-token"))]
62+
token: SensitiveString,
63+
},
64+
5565
/// Custom authentication using VRL code.
5666
///
5767
/// Takes in request and validates it using VRL code.
@@ -69,13 +79,13 @@ impl<'de> Deserialize<'de> for HttpServerAuthConfig {
6979
{
7080
struct HttpServerAuthConfigVisitor;
7181

72-
const FIELD_KEYS: [&str; 4] = ["strategy", "username", "password", "source"];
82+
const FIELD_KEYS: [&str; 5] = ["strategy", "username", "password", "token", "source"];
7383

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

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

8191
fn visit_map<A>(self, mut map: A) -> Result<HttpServerAuthConfig, A::Error>
@@ -114,13 +124,24 @@ impl<'de> Deserialize<'de> for HttpServerAuthConfig {
114124
password: SensitiveString::from(password),
115125
})
116126
}
127+
"bearer" => {
128+
let token = fields
129+
.remove("token")
130+
.ok_or_else(|| Error::missing_field("token"))?;
131+
Ok(HttpServerAuthConfig::Bearer {
132+
token: SensitiveString::from(token),
133+
})
134+
}
117135
"custom" => {
118136
let source = fields
119137
.remove("source")
120138
.ok_or_else(|| Error::missing_field("source"))?;
121139
Ok(HttpServerAuthConfig::Custom { source })
122140
}
123-
_ => Err(Error::unknown_variant(strategy, &["basic", "custom"])),
141+
_ => Err(Error::unknown_variant(
142+
strategy,
143+
&["basic", "bearer", "custom"],
144+
)),
124145
}
125146
}
126147
}
@@ -145,6 +166,15 @@ impl HttpServerAuthConfig {
145166
"Invalid username/password",
146167
))
147168
}
169+
HttpServerAuthConfig::Bearer { token } => {
170+
let auth = Authorization::bearer(token.inner()).map_err(|e| {
171+
format!("Invalid bearer token: {e}")
172+
})?;
173+
Ok(HttpServerAuthMatcher::AuthHeader(
174+
auth.0.encode(),
175+
"Invalid token",
176+
))
177+
}
148178
HttpServerAuthConfig::Custom { source } => {
149179
let state = TypeState::default();
150180

@@ -402,6 +432,112 @@ mod tests {
402432
);
403433
}
404434

435+
#[test]
436+
fn config_should_support_bearer_strategy() {
437+
let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
438+
strategy: bearer
439+
token: my-secret-token
440+
"#
441+
})
442+
.unwrap();
443+
444+
if let HttpServerAuthConfig::Bearer { token } = config {
445+
assert_eq!(token.inner(), "my-secret-token");
446+
} else {
447+
panic!("Expected HttpServerAuthConfig::Bearer");
448+
}
449+
}
450+
451+
#[test]
452+
fn build_bearer_auth_should_always_work() {
453+
let bearer_auth = HttpServerAuthConfig::Bearer {
454+
token: random_string(16).into(),
455+
};
456+
457+
let matcher = bearer_auth.build(&Default::default(), &Default::default());
458+
459+
assert!(matcher.is_ok());
460+
assert!(matches!(
461+
matcher.unwrap(),
462+
HttpServerAuthMatcher::AuthHeader { .. }
463+
));
464+
}
465+
466+
#[test]
467+
fn build_bearer_auth_should_use_token_related_message() {
468+
let bearer_auth = HttpServerAuthConfig::Bearer {
469+
token: random_string(16).into(),
470+
};
471+
472+
let (_, error_message) = bearer_auth
473+
.build(&Default::default(), &Default::default())
474+
.unwrap()
475+
.auth_header();
476+
assert_eq!("Invalid token", error_message);
477+
}
478+
479+
#[test]
480+
fn bearer_auth_matcher_should_return_401_when_missing_auth_header() {
481+
let bearer_auth = HttpServerAuthConfig::Bearer {
482+
token: "my-token".into(),
483+
};
484+
485+
let matcher = bearer_auth
486+
.build(&Default::default(), &Default::default())
487+
.unwrap();
488+
489+
let (_guard, addr) = next_addr();
490+
let result = matcher.handle_auth(Some(&addr), &HeaderMap::new(), "/");
491+
492+
assert!(result.is_err());
493+
let error = result.unwrap_err();
494+
assert_eq!(401, error.code());
495+
assert_eq!("No authorization header", error.message());
496+
}
497+
498+
#[test]
499+
fn bearer_auth_matcher_should_return_401_with_wrong_token() {
500+
let bearer_auth = HttpServerAuthConfig::Bearer {
501+
token: "my-token".into(),
502+
};
503+
504+
let matcher = bearer_auth
505+
.build(&Default::default(), &Default::default())
506+
.unwrap();
507+
508+
let mut headers = HeaderMap::new();
509+
headers.insert(AUTHORIZATION, HeaderValue::from_static("Bearer wrong-token"));
510+
let (_guard, addr) = next_addr();
511+
let result = matcher.handle_auth(Some(&addr), &headers, "/");
512+
513+
assert!(result.is_err());
514+
let error = result.unwrap_err();
515+
assert_eq!(401, error.code());
516+
assert_eq!("Invalid token", error.message());
517+
}
518+
519+
#[test]
520+
fn bearer_auth_matcher_should_return_ok_for_correct_token() {
521+
let token = "my-secret-token";
522+
let bearer_auth = HttpServerAuthConfig::Bearer {
523+
token: token.into(),
524+
};
525+
526+
let matcher = bearer_auth
527+
.build(&Default::default(), &Default::default())
528+
.unwrap();
529+
530+
let mut headers = HeaderMap::new();
531+
headers.insert(
532+
AUTHORIZATION,
533+
Authorization::bearer(token).unwrap().0.encode(),
534+
);
535+
let (_guard, addr) = next_addr();
536+
let result = matcher.handle_auth(Some(&addr), &headers, "/");
537+
538+
assert!(result.is_ok());
539+
}
540+
405541
#[test]
406542
fn build_custom_should_fail_on_invalid_source() {
407543
let custom_auth = HttpServerAuthConfig::Custom {

src/sources/http_server.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,91 @@ mod tests {
16121612
assert_eq!(200, send_with_headers(addr, "", headers).await);
16131613
}
16141614

1615+
#[tokio::test]
1616+
async fn returns_401_when_required_bearer_auth_is_missing() {
1617+
components::init_test();
1618+
let (_rx, addr) = source(
1619+
vec![],
1620+
vec![],
1621+
"http_path",
1622+
"remote_ip",
1623+
"/",
1624+
"GET",
1625+
StatusCode::OK,
1626+
Some(HttpServerAuthConfig::Bearer {
1627+
token: "my-token".into(),
1628+
}),
1629+
true,
1630+
EventStatus::Delivered,
1631+
true,
1632+
None,
1633+
None,
1634+
)
1635+
.await;
1636+
1637+
assert_eq!(401, send_request(addr, "GET", "", "/").await);
1638+
}
1639+
1640+
#[tokio::test]
1641+
async fn returns_401_when_required_bearer_auth_is_wrong() {
1642+
components::init_test();
1643+
let (_rx, addr) = source(
1644+
vec![],
1645+
vec![],
1646+
"http_path",
1647+
"remote_ip",
1648+
"/",
1649+
"POST",
1650+
StatusCode::OK,
1651+
Some(HttpServerAuthConfig::Bearer {
1652+
token: "my-token".into(),
1653+
}),
1654+
true,
1655+
EventStatus::Delivered,
1656+
true,
1657+
None,
1658+
None,
1659+
)
1660+
.await;
1661+
1662+
let mut headers = HeaderMap::new();
1663+
headers.insert(
1664+
AUTHORIZATION,
1665+
Authorization::bearer("wrong-token").unwrap().0.encode(),
1666+
);
1667+
assert_eq!(401, send_with_headers(addr, "", headers).await);
1668+
}
1669+
1670+
#[tokio::test]
1671+
async fn http_post_with_correct_bearer_auth() {
1672+
components::init_test();
1673+
let (_rx, addr) = source(
1674+
vec![],
1675+
vec![],
1676+
"http_path",
1677+
"remote_ip",
1678+
"/",
1679+
"POST",
1680+
StatusCode::OK,
1681+
Some(HttpServerAuthConfig::Bearer {
1682+
token: "my-token".into(),
1683+
}),
1684+
true,
1685+
EventStatus::Delivered,
1686+
true,
1687+
None,
1688+
None,
1689+
)
1690+
.await;
1691+
1692+
let mut headers = HeaderMap::new();
1693+
headers.insert(
1694+
AUTHORIZATION,
1695+
Authorization::bearer("my-token").unwrap().0.encode(),
1696+
);
1697+
assert_eq!(200, send_with_headers(addr, "", headers).await);
1698+
}
1699+
16151700
#[test]
16161701
fn output_schema_definition_vector_namespace() {
16171702
let config = SimpleHttpConfig {

0 commit comments

Comments
 (0)