Skip to content

Commit eff409c

Browse files
committed
MB-68594: [BP] Make gs2-authzid optional for OAUTHBEARER
Previously we required the gs2 header to specify no channel binding and mandatory gs2-authzid. This patch allows gs2-authzid to be optional and we'll pick out the username from the returned RBAC entry from ns_server. It is expected that *ns_server* honors the gs2-authzid as kv *WILL* use that as the username if that's present in the request. If no gs2-authzid is specified we'll pick the "one and only" username specified in the returned "rbac" entry in the returned token metadata: In the returned payload below we would use "myusername" as the user: { "token": { "exp": 1758705142, "rbac": { "myusername": { "buckets": { "default": { "privileges": [ "Read" ] } }, "domain": "external", "privileges": [] } } } } Change-Id: Ie3ca4efd1df04ac59f962f5718750c295a6ae1cc Reviewed-on: https://review.couchbase.org/c/kv_engine/+/238364 Well-Formed: Restriction Checker Reviewed-by: Jim Walker <jim@couchbase.com> Tested-by: Build Bot <build@couchbase.com>
1 parent 9ab3bed commit eff409c

11 files changed

Lines changed: 200 additions & 33 deletions

File tree

cbsasl/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ add_library(cbsasl STATIC
55
mechanism.cc
66
oauthbearer/oauthbearer.cc
77
oauthbearer/oauthbearer.h
8+
oauthbearer/parse_gs2_header.cc
9+
oauthbearer/parse_gs2_header.h
810
plain/check_password.cc
911
plain/check_password.h
1012
plain/plain.cc
@@ -46,6 +48,7 @@ cb_add_test_executable(cbsasl_unit_test
4648
client_server_test.cc
4749
domain_test.cc
4850
password_database_test.cc
51+
oauthbearer/parse_gs2_header_test.cc
4952
plain/plain_test.cc
5053
sasl_server_test.cc
5154
scram-sha/saslprep_test.cc

cbsasl/oauthbearer/oauthbearer.cc

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
#include "oauthbearer.h"
12+
#include "parse_gs2_header.h"
1213

1314
#include <cbsasl/username_util.h>
1415
#include <fmt/format.h>
@@ -24,33 +25,50 @@ std::pair<Error, std::string> ServerBackend::start(std::string_view input) {
2425
return {Error::BAD_PARAM, {}};
2526
}
2627

27-
auto gs2 = fields.front();
28-
if (!gs2.starts_with("n,a=")) {
28+
try {
29+
username = parse_gs2_header(fields.front());
30+
} catch (const std::invalid_argument&) {
2931
return {Error::BAD_PARAM, {}};
3032
}
31-
username = std::string(gs2.substr(4));
32-
if (username.empty()) {
33-
return {Error::BAD_PARAM, {}};
34-
}
35-
if (username.back() == ',') {
36-
username.pop_back();
33+
34+
std::string_view token;
35+
for (std::size_t index = 1; index < fields.size(); index++) {
36+
auto field = fields[index];
37+
if (!field.starts_with("auth=")) {
38+
continue;
39+
}
40+
field.remove_prefix(5);
41+
auto kv = cb::string::split(field, ' ');
42+
if (kv.size() != 2) {
43+
return {Error::BAD_PARAM, {}};
44+
}
45+
std::string auth;
46+
for (auto& c : kv.front()) {
47+
auth.push_back(std::tolower(c));
48+
}
49+
if (auth != "bearer") {
50+
return {Error::BAD_PARAM, {}};
51+
}
52+
token = kv.back();
3753
}
38-
username = username::decode(username);
39-
auto token = fields[1];
40-
if (!token.starts_with("auth=Bearer ")) {
54+
55+
if (token.empty()) {
4156
return {Error::BAD_PARAM, {}};
4257
}
43-
token.remove_prefix(12);
58+
4459
return {context.validateUserToken(username, token), {}};
4560
}
4661

4762
std::pair<Error, std::string> ClientBackend::start() {
48-
std::string header = fmt::format("n,a={},", usernameCallback());
63+
auto user = usernameCallback();
64+
auto header = fmt::format(
65+
"n,{},",
66+
user.empty() ? "" : fmt::format("a={}", username::encode(user)));
4967
header.push_back(0x01);
5068
header.append(fmt::format("auth=Bearer {}", passwordCallback()));
5169
header.push_back(0x01);
5270
header.push_back(0x01);
5371
return {Error::OK, std::move(header)};
5472
}
5573

56-
} // namespace cb::sasl::mechanism::oauthbearer
74+
} // namespace cb::sasl::mechanism::oauthbearer
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025-Present Couchbase, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License included
5+
* in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
6+
* in that file, in accordance with the Business Source License, use of this
7+
* software will be governed by the Apache License, Version 2.0, included in
8+
* the file licenses/APL2.txt.
9+
*/
10+
11+
#include "parse_gs2_header.h"
12+
13+
#include "cbsasl/username_util.h"
14+
#include <platform/split_string.h>
15+
#include <stdexcept>
16+
17+
namespace cb::sasl::mechanism::oauthbearer {
18+
std::string parse_gs2_header(std::string_view input) {
19+
const auto fields = cb::string::split(input, ',');
20+
if (fields.size() != 2) {
21+
throw std::invalid_argument("Invalid GS2 header");
22+
}
23+
24+
if (fields[0] != "n") {
25+
throw std::invalid_argument(
26+
"Only 'n' (no channel binding) is supported");
27+
}
28+
29+
std::string authzid;
30+
if (!fields[1].empty()) {
31+
if (!fields[1].starts_with("a=")) {
32+
throw std::invalid_argument("Invalid GS2 header");
33+
}
34+
authzid = username::decode(fields[1].substr(2));
35+
if (authzid.empty()) {
36+
throw std::invalid_argument("Invalid saslname");
37+
}
38+
}
39+
40+
return authzid;
41+
}
42+
} // namespace cb::sasl::mechanism::oauthbearer
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2025-Present Couchbase, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License included
5+
* in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
6+
* in that file, in accordance with the Business Source License, use of this
7+
* software will be governed by the Apache License, Version 2.0, included in
8+
* the file licenses/APL2.txt.
9+
*/
10+
#pragma once
11+
12+
#include <string>
13+
14+
namespace cb::sasl::mechanism::oauthbearer {
15+
16+
/**
17+
* Parse the GS2 header and return the authzid (if any)
18+
*
19+
* @param input The GS2 header to parse
20+
* @return authzid if present, empty string if not
21+
* @throws std::invalid_argument if the input is invalid
22+
*/
23+
std::string parse_gs2_header(std::string_view input);
24+
25+
} // namespace cb::sasl::mechanism::oauthbearer
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025-Present Couchbase, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License included
5+
* in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
6+
* in that file, in accordance with the Business Source License, use of this
7+
* software will be governed by the Apache License, Version 2.0, included in
8+
* the file licenses/APL2.txt.
9+
*/
10+
11+
#include "parse_gs2_header.h"
12+
#include <folly/portability/GTest.h>
13+
14+
using namespace cb::sasl::mechanism::oauthbearer;
15+
16+
class GS2ParserTest : public ::testing::Test {
17+
protected:
18+
void invalid(std::string_view input, std::string_view error) {
19+
try {
20+
parse_gs2_header(input);
21+
FAIL() << "Expected std::invalid_argument";
22+
} catch (const std::invalid_argument& e) {
23+
EXPECT_EQ(error, e.what());
24+
} catch (...) {
25+
FAIL() << "Expected exception of type std::invalid_argument";
26+
}
27+
}
28+
};
29+
30+
TEST_F(GS2ParserTest, ParseMinimalGS2Header) {
31+
auto result = parse_gs2_header("n,,");
32+
EXPECT_TRUE(result.empty());
33+
}
34+
35+
TEST_F(GS2ParserTest, ParseGS2HeaderWithAuthzid) {
36+
auto result = parse_gs2_header("n,a=foo,");
37+
EXPECT_EQ("foo", result);
38+
}
39+
40+
TEST_F(GS2ParserTest, ParseUnsupportedChannelBinding) {
41+
invalid("y,,", "Only 'n' (no channel binding) is supported");
42+
}
43+
44+
TEST_F(GS2ParserTest, ParseMissingTrailingComma) {
45+
invalid("n,", "Invalid GS2 header");
46+
}
47+
48+
TEST_F(GS2ParserTest, ParseInvalidAuthzid) {
49+
// authzid should be "a=saslname"
50+
invalid("n,b=foo,", "Invalid GS2 header");
51+
}
52+
53+
TEST_F(GS2ParserTest, ParseInvalidAuthzidNoUsername) {
54+
// saslname can't be empty
55+
invalid("n,a=,", "Invalid saslname");
56+
}

cluster_framework/auth_provider_service.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,13 +366,19 @@ static sasl::Error validateUserTokenFunction(
366366
return sasl::Error::BAD_PARAM;
367367
}
368368
const auto& claims = jwt->payload;
369-
if (claims.value("user", "") != user) {
369+
std::string user_str{user};
370+
if (user.empty()) {
371+
if (!claims.contains("user")) {
372+
return sasl::Error::NO_USER;
373+
}
374+
user_str = claims["user"];
375+
} else if (claims.value("user", "") != user) {
370376
return sasl::Error::NO_USER;
371377
}
372378

373379
nlohmann::json metadata;
374380
if (claims.contains("cb-rbac")) {
375-
metadata["rbac"][user] = nlohmann::json::parse(
381+
metadata["rbac"][user_str] = nlohmann::json::parse(
376382
base64url::decode(claims.value("cb-rbac", "")));
377383
} else {
378384
return sasl::Error::NO_RBAC_PROFILE;

daemon/sasl_auth_task.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,28 @@ void SaslAuthTask::successfull_external_auth(const nlohmann::json& json) {
9797

9898
if (json.contains("token")) {
9999
tokenMetadata = json["token"];
100+
if (getUsername().empty()) {
101+
if (!json["token"].contains("rbac")) {
102+
error = Error::BAD_PARAM;
103+
cookie.setErrorContext("Internal error. No rbac entry");
104+
return;
105+
}
106+
107+
auto rbac = json["token"]["rbac"];
108+
std::string username;
109+
int count = 0;
110+
for (auto it = rbac.begin(); it != rbac.end(); ++it) {
111+
username = it.key();
112+
++count;
113+
}
114+
if (count == 1 && !username.empty()) {
115+
serverContext.setUsername(std::move(username));
116+
} else {
117+
error = Error::BAD_PARAM;
118+
cookie.setErrorContext(
119+
"Internal error. Failed to locate username");
120+
}
121+
}
100122
}
101123

102124
externalAuthManager->login(serverContext.getUsername());

include/cbsasl/server.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ class ServerContext : public Context {
111111
return backend->getUsername();
112112
}
113113

114+
void setUsername(std::string user) {
115+
backend->setUsername(std::move(user));
116+
}
117+
114118
void setDomain(Domain domain) {
115119
backend->setDomain(domain);
116120
}

programs/mc_program_getopt.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ McProgramGetopt::createAuthenticatedConnection(
262262
ret->connect();
263263
if (token_auth) {
264264
ret->authenticateWithToken();
265-
} else if (!user.empty()) {
265+
} else if (!user.empty() && !password.empty()) {
266266
ret->authenticate(user,
267267
password,
268268
sasl_mechanism.empty() ? ret->getSaslMechanisms()
@@ -278,7 +278,7 @@ std::unique_ptr<MemcachedConnection> McProgramGetopt::getConnection() {
278278
ret->connect();
279279
if (token_auth) {
280280
ret->authenticateWithToken();
281-
} else if (!user.empty()) {
281+
} else if (!user.empty() && !password.empty()) {
282282
ret->authenticate(user,
283283
password,
284284
sasl_mechanism.empty() ? ret->getSaslMechanisms()

protocol/connection/client_connection.cc

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,16 +1274,7 @@ void MemcachedConnection::setTokenBuilder(
12741274
}
12751275

12761276
void MemcachedConnection::authenticateWithToken() {
1277-
auto token = tokenBuilder->build();
1278-
1279-
auto parsed = cb::jwt::Token::parse(tokenBuilder->build());
1280-
if (!parsed->payload.contains("sub")) {
1281-
throw std::logic_error(
1282-
"MemcachedConnection::authenticateWithToken: "
1283-
"The token must contain a sub field");
1284-
}
1285-
1286-
doSaslAuthenticate(parsed->payload["sub"], token, "OAUTHBEARER");
1277+
doSaslAuthenticate({}, tokenBuilder->build(), "OAUTHBEARER");
12871278
}
12881279

12891280
void MemcachedConnection::authenticate(

0 commit comments

Comments
 (0)