Skip to content

Commit 4b445e0

Browse files
authored
[shared] Add C++ jwt library. Refactor vizier agent to use it (#2406)
Summary: [shared] Add C++ jwt library. Refactor vizier agent to use it This is part of a larger change to allow the PEM to be queried directly similar to the standalone PEM. The original change which I'm working to split into a smaller set of changes can be found in #2401 Relevant Issues: N/A Type of change: /kind feature Test Plan: `bazel test` --------- Signed-off-by: Dom Del Nano <ddelnano@gmail.com>
1 parent cad1388 commit 4b445e0

7 files changed

Lines changed: 473 additions & 15 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2018- The Pixie Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
load("//bazel:pl_build_system.bzl", "pl_cc_library", "pl_cc_test")
18+
19+
package(default_visibility = ["//src:__subpackages__"])
20+
21+
pl_cc_library(
22+
name = "cc_library",
23+
srcs = ["service_token.cc"],
24+
hdrs = ["service_token.h"],
25+
deps = [
26+
"@com_github_arun11299_cpp_jwt//:cpp_jwt",
27+
"@com_github_rlyeh_sole//:sole",
28+
],
29+
)
30+
31+
pl_cc_test(
32+
name = "service_token_test",
33+
srcs = ["service_token_test.cc"],
34+
deps = [
35+
":cc_library",
36+
"@com_github_arun11299_cpp_jwt//:cpp_jwt",
37+
],
38+
)
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright 2018- The Pixie Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
#include "src/shared/services/jwt/service_token.h"
20+
21+
#include <algorithm>
22+
#include <chrono>
23+
#include <string>
24+
#include <string_view>
25+
#include <vector>
26+
27+
#include <absl/strings/match.h>
28+
#include <absl/strings/str_split.h>
29+
#include <jwt/jwt.hpp>
30+
#include <sole.hpp>
31+
32+
namespace px {
33+
namespace services {
34+
35+
using ::jwt::decode;
36+
using ::jwt::jwt_object;
37+
using ::jwt::jwt_payload;
38+
using ::jwt::params::algorithm;
39+
using ::jwt::params::algorithms;
40+
using ::jwt::params::issuer;
41+
using ::jwt::params::leeway;
42+
using ::jwt::params::secret;
43+
using ::jwt::params::verify;
44+
45+
namespace {
46+
47+
constexpr std::string_view kBearerPrefix = "bearer ";
48+
constexpr std::chrono::seconds kNotBeforeSkew{60};
49+
50+
constexpr std::chrono::seconds kTokenValidity{60};
51+
52+
// cpp-jwt's built-in audience check reads `aud` as a string only and throws on
53+
// the array form, which RFC 7519 4.1.3 permits.
54+
bool AudienceMatches(const jwt_payload& payload, const std::string& want) {
55+
const auto& claims = payload.create_json_obj();
56+
const auto it = claims.find("aud");
57+
if (it == claims.end()) {
58+
return false;
59+
}
60+
if (it->is_string()) {
61+
return it->get_ref<const std::string&>() == want;
62+
}
63+
if (it->is_array()) {
64+
for (const auto& entry : *it) {
65+
if (entry.is_string() && entry.get_ref<const std::string&>() == want) {
66+
return true;
67+
}
68+
}
69+
}
70+
return false;
71+
}
72+
73+
Status CheckScopes(const jwt_payload& payload, const std::vector<std::string>& required_scopes) {
74+
if (required_scopes.empty()) {
75+
return Status::OK();
76+
}
77+
const auto& claims = payload.create_json_obj();
78+
const auto it = claims.find("Scopes");
79+
if (it == claims.end() || !it->is_string()) {
80+
return error::Unauthenticated("missing or non-string Scopes claim");
81+
}
82+
const std::vector<std::string_view> have = absl::StrSplit(it->get_ref<const std::string&>(), ',');
83+
for (const auto& want : required_scopes) {
84+
if (std::find(have.begin(), have.end(), want) == have.end()) {
85+
return error::Unauthenticated("token lacks the '$0' scope", want);
86+
}
87+
}
88+
return Status::OK();
89+
}
90+
91+
} // namespace
92+
93+
Status VerifyServiceJWT(std::string_view token, std::string_view signing_key,
94+
const ServiceJWTOptions& opts) {
95+
if (signing_key.empty()) {
96+
return error::InvalidArgument("signing key is empty");
97+
}
98+
if (token.empty()) {
99+
return error::Unauthenticated("empty token");
100+
}
101+
102+
std::error_code ec;
103+
jwt_object obj;
104+
try {
105+
obj = decode(std::string{token}, algorithms({"HS256"}), ec, secret(std::string{signing_key}),
106+
verify(true), leeway(static_cast<uint32_t>(opts.leeway.count())),
107+
issuer(opts.issuer));
108+
} catch (const std::exception& e) {
109+
return error::Unauthenticated("malformed token ($0)", e.what());
110+
}
111+
if (ec) {
112+
return error::Unauthenticated("$0", ec.message());
113+
}
114+
115+
// A clear `ec` means the signature verified, so the claims below are covered
116+
// by it.
117+
const auto& payload = obj.payload();
118+
119+
// cpp-jwt validates `exp` only when the claim is present, so a token minted
120+
// without one would otherwise verify forever.
121+
if (!payload.has_claim("exp")) {
122+
return error::Unauthenticated("missing exp claim");
123+
}
124+
if (!AudienceMatches(payload, opts.audience)) {
125+
return error::Unauthenticated("wrong audience (expected '$0')", opts.audience);
126+
}
127+
return CheckScopes(payload, opts.required_scopes);
128+
}
129+
130+
StatusOr<std::string> GenerateServiceToken(std::string_view signing_key,
131+
std::string_view service_id) {
132+
if (signing_key.empty()) {
133+
return error::InvalidArgument("cannot mint a service token with an empty signing key");
134+
}
135+
const auto now = std::chrono::system_clock::now();
136+
jwt_object obj{algorithm("HS256")};
137+
obj.add_claim("iss", std::string{kPixieIssuer});
138+
obj.add_claim("aud", std::string{kVizierAudience});
139+
obj.add_claim("jti", sole::uuid4().str());
140+
obj.add_claim("iat", now);
141+
obj.add_claim("nbf", now - kNotBeforeSkew);
142+
obj.add_claim("exp", now + kTokenValidity);
143+
obj.add_claim("sub", std::string{kServiceScope});
144+
obj.add_claim("Scopes", std::string{kServiceScope});
145+
obj.add_claim("ServiceID", std::string{service_id});
146+
obj.secret(std::string{signing_key});
147+
148+
std::error_code ec;
149+
auto token = obj.signature(ec);
150+
if (ec) {
151+
return error::Internal("failed to mint service token: $0", ec.message());
152+
}
153+
return token;
154+
}
155+
156+
std::string_view StripBearerPrefix(std::string_view authorization_value) {
157+
if (!absl::StartsWithIgnoreCase(authorization_value, kBearerPrefix)) {
158+
return {};
159+
}
160+
return authorization_value.substr(kBearerPrefix.size());
161+
}
162+
163+
} // namespace services
164+
} // namespace px
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2018- The Pixie Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
#pragma once
20+
21+
#include <chrono>
22+
#include <string>
23+
#include <string_view>
24+
#include <vector>
25+
26+
#include "src/common/base/base.h"
27+
28+
namespace px {
29+
namespace services {
30+
31+
inline constexpr std::string_view kVizierAudience = "vizier";
32+
inline constexpr std::string_view kPixieIssuer = "PL";
33+
inline constexpr std::string_view kServiceScope = "service";
34+
35+
struct ServiceJWTOptions {
36+
// `aud` may be a string or an array of strings (RFC 7519 4.1.3); this value
37+
// need only appear among them.
38+
std::string audience{kVizierAudience};
39+
40+
std::string issuer{kPixieIssuer};
41+
42+
// Must all appear in the comma-joined `Scopes` claim. Authorization comes
43+
// from here, not from `sub`, which varies per caller.
44+
std::vector<std::string> required_scopes{std::string{kServiceScope}};
45+
46+
// Applied to `exp` and `nbf`.
47+
std::chrono::seconds leeway{30};
48+
};
49+
50+
// Returns OK only for a well-formed HS256 JWT with a valid signature, an `exp`
51+
// that is present and unexpired, and claims satisfying `opts`.
52+
//
53+
// The Status message names the failing check, for operator logs. A caller
54+
// serving untrusted peers should return something generic to the peer instead.
55+
Status VerifyServiceJWT(std::string_view token, std::string_view signing_key,
56+
const ServiceJWTOptions& opts = {});
57+
58+
// Mints a token VerifyServiceJWT accepts, with `service_id` recorded in the
59+
// `ServiceID` claim.
60+
StatusOr<std::string> GenerateServiceToken(std::string_view signing_key,
61+
std::string_view service_id);
62+
63+
// Returns the token following a case-insensitive "bearer " prefix, or an empty
64+
// view if the prefix is absent. gRPC lowercases metadata keys but not values,
65+
// and Pixie mints a lowercase prefix while RFC 6750 spells it "Bearer".
66+
std::string_view StripBearerPrefix(std::string_view authorization_value);
67+
68+
} // namespace services
69+
} // namespace px

0 commit comments

Comments
 (0)