|
| 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 |
0 commit comments