Skip to content
Merged
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ loosely while pre-1.0 (breaking changes can land on minor bumps).
## [Unreleased]

### Fixed
- **Bearer scheme matches RFC 6750 / RFC 9110.** `extract_bearer` now
treats the auth-scheme as case-insensitive and skips `1*SP` after it,
so `bearer` / `BEARER` and extra spaces no longer 401 a valid token.
- **Inbound HTTP body honors Content-Length.** `parse_http_request` now
takes at most `Content-Length` bytes from the header-read leftover, so
`Content-Length: 0` stays empty and extra pipelined or smuggled bytes
Expand Down
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,17 @@ if(INDEX_BUILD_TESTS)
target_link_libraries(unit_sse_mailbox PRIVATE Threads::Threads)
add_test(NAME unit_sse_mailbox COMMAND unit_sse_mailbox)

# --- Unit tests for RFC 6750 / RFC 9110 Bearer field parsing ---
add_executable(unit_auth
tests/test_auth.cpp
src/api/http_util.cpp
)
target_include_directories(unit_auth PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/third_party/doctest
)
add_test(NAME unit_auth COMMAND unit_auth)

# --- Unit tests for /v1/events agent routing ---
add_executable(unit_route_event
tests/test_route_event.cpp
Expand Down
5 changes: 5 additions & 0 deletions include/api/http_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ std::vector<std::string> split_path(const std::string& path);
std::string url_decode(const std::string& s);
std::map<std::string, std::string> parse_query(const std::string& path);

// RFC 6750 §2.1 / RFC 9110 §11.1: parse an Authorization field value.
// Auth-scheme is case-insensitive; one or more SP separate scheme and
// token. Returns empty when the field is not Bearer credentials.
std::string parse_bearer_authorization(const std::string& field_value);

} // namespace arbiter
12 changes: 4 additions & 8 deletions src/api/auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@

#include "api/auth.h"
#include "api/http_response.h"
#include "api/http_util.h"

#include "tenant_store.h"

namespace arbiter {

// Extract the bearer token from an Authorization header, or empty if missing.
// Header lookup only — scheme grammar lives in parse_bearer_authorization
// (RFC 6750 / RFC 9110: case-insensitive scheme, 1*SP).
std::string extract_bearer(const HttpRequest& req) {
auto it = req.headers.find("authorization");
if (it == req.headers.end()) return {};
static constexpr const char* kPrefix = "Bearer ";
static constexpr size_t kPrefixLen = 7;
const std::string& hdr = it->second;
if (hdr.size() <= kPrefixLen ||
hdr.compare(0, kPrefixLen, kPrefix) != 0)
return {};
return hdr.substr(kPrefixLen);
return parse_bearer_authorization(it->second);
}

// Single-threaded helper for request handlers: refresh a Tenant snapshot
Expand Down
27 changes: 27 additions & 0 deletions src/api/http_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "api/http_util.h"

#include <cctype>
#include <string>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -67,4 +68,30 @@ std::map<std::string, std::string> parse_query(const std::string& path) {
return out;
}

std::string parse_bearer_authorization(const std::string& field_value) {
// RFC 9110 §11.1: authentication schemes are case-insensitive.
// RFC 6750 §2.1: credentials = "Bearer" 1*SP token68
//
// A literal "Bearer " prefix 401s clients that send `bearer`,
// `BEARER`, or two spaces after the scheme. Trailing OWS on
// the field value is stripped so a proxy-appended space does not
// become part of the token hash.
static constexpr char kScheme[] = "bearer";
static constexpr size_t kSchemeLen = 6;
if (field_value.size() < kSchemeLen + 2) return {};
for (size_t i = 0; i < kSchemeLen; ++i) {
const unsigned char c = static_cast<unsigned char>(field_value[i]);
if (static_cast<char>(std::tolower(c)) != kScheme[i]) return {};
}
size_t i = kSchemeLen;
if (i >= field_value.size() || field_value[i] != ' ') return {};
while (i < field_value.size() && field_value[i] == ' ') ++i;
if (i >= field_value.size()) return {};
size_t end = field_value.size();
while (end > i && (field_value[end - 1] == ' ' || field_value[end - 1] == '\t'))
--end;
if (end <= i) return {};
return field_value.substr(i, end - i);
}

} // namespace arbiter
39 changes: 39 additions & 0 deletions tests/test_auth.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// tests/test_auth.cpp — RFC 6750 / RFC 9110 Bearer Authorization parsing.

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "api/http_util.h"

using namespace arbiter;

TEST_CASE("canonical Bearer scheme extracts the token") {
CHECK(parse_bearer_authorization("Bearer atr_secret") == "atr_secret");
}

TEST_CASE("auth-scheme is case-insensitive") {
CHECK(parse_bearer_authorization("bearer atr_secret") == "atr_secret");
CHECK(parse_bearer_authorization("BEARER atr_secret") == "atr_secret");
CHECK(parse_bearer_authorization("BeArEr atr_secret") == "atr_secret");
}

TEST_CASE("one or more spaces after the scheme are skipped") {
CHECK(parse_bearer_authorization("Bearer atr_secret") == "atr_secret");
CHECK(parse_bearer_authorization("bearer atr_secret") == "atr_secret");
}

TEST_CASE("trailing OWS is not part of the token") {
CHECK(parse_bearer_authorization("Bearer atr_secret ") == "atr_secret");
CHECK(parse_bearer_authorization("Bearer atr_secret\t") == "atr_secret");
CHECK(parse_bearer_authorization("Bearer atr_secret \t ") == "atr_secret");
}

TEST_CASE("non-Bearer schemes and malformed fields are rejected") {
CHECK(parse_bearer_authorization("").empty());
CHECK(parse_bearer_authorization("Bearer").empty());
CHECK(parse_bearer_authorization("Bearer ").empty());
CHECK(parse_bearer_authorization("Bearer ").empty());
CHECK(parse_bearer_authorization("Bearer\tatr_secret").empty());
CHECK(parse_bearer_authorization("Basic atr_secret").empty());
CHECK(parse_bearer_authorization("XBearer atr_secret").empty());
CHECK(parse_bearer_authorization("Bearerat_secret").empty());
}
Loading