blob: aa17640f02500d3671c1fefcc826a81585b600fa [file] [log] [blame]
#include "oauth_utils.h"
#include <exception>
#include <fstream>
#include <iterator>
#include <optional>
#include <string>
#include <string_view>
#include "absl/log/log.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "redfish_v1.pb.h"
#include "zatar/x509_certificate.h"
#include "grpcpp/support/status.h"
#include "nlohmann/json.hpp"
#include "jwt-cpp/jwt.h"
#include "jwt-cpp/traits/nlohmann-json/defaults.h"
#include "config_parser.h"
namespace milotic::authz {
std::string GenerateOAuthSubject(const PeerSpiffeIdentity& peer_identity) {
std::string subject;
absl::StrAppend(&subject, "URI:", peer_identity.spiffe_id);
if (peer_identity.fqdn) {
absl::StrAppend(&subject, ", ", "DNS:", *peer_identity.fqdn);
}
return subject;
}
std::string GetSubject(const std::string& certificate_path) {
std::ifstream file(certificate_path.c_str());
if (!file.is_open()) {
LOG(WARNING) << "Certificate file at '" << certificate_path
<< "' is missing.";
return "";
}
std::string buffer = {std::istreambuf_iterator<char>(file),
std::istreambuf_iterator<char>()};
using ::ecclesia::GetSubjectAltName;
using ::ecclesia::SubjectAltName;
using ::milotic::authz::GenerateOAuthSubject;
using ::milotic::authz::PeerSpiffeIdentity;
auto san = GetSubjectAltName(buffer);
if (!san.ok() || !san->spiffe_id.has_value() || !san->fqdn.has_value()) {
LOG(WARNING) << "Certificate file at '" << certificate_path
<< "' is illegal.";
LOG(WARNING) << "SAN is '" << san->DebugInfo();
return "";
}
PeerSpiffeIdentity identity = {
.spiffe_id = *san->spiffe_id,
.fqdn = *san->fqdn,
};
return GenerateOAuthSubject(identity);
}
std::optional<std::string> GetOAuthTokenFromRequest(
const ::redfish::v1::Request& request) {
// Both field name of the Authorization header and the token type are case
// insensitive
// References
// https://www.rfc-editor.org/rfc/rfc7230#section-3.2
// https://www.rfc-editor.org/rfc/rfc6749#section-5.1
for (const auto& [key, value] : request.headers()) {
constexpr std::string_view kBearer = "Bearer ";
if (absl::EqualsIgnoreCase(key, "Authorization") &&
absl::StartsWithIgnoreCase(value, kBearer)) {
return value.substr(kBearer.size());
}
}
return std::nullopt;
}
grpc::Status VerifyAndExtractRoleFromToken(const std::string& token,
const std::string& public_key,
const std::string& expected_subject,
const std::string& expected_audience,
const std::string& expected_issuer,
std::string& redfish_role) {
try {
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::rs256(public_key))
.with_subject(expected_subject)
.with_audience(expected_audience)
.with_issuer(expected_issuer);
auto decoded = jwt::decode(token);
verifier.verify(decoded);
nlohmann::json scope = decoded.get_payload_claim("scope").to_json();
const std::string* scope_str = scope.get_ptr<const std::string*>();
if (scope_str == nullptr) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
"scope is not a string");
}
constexpr std::string_view kRedfishRole = "Redfish.Role.";
if (!absl::StartsWith(*scope_str, kRedfishRole)) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
"scope doesn't start with \"Redfish.Role.\"");
}
redfish_role = scope_str->substr(kRedfishRole.size());
return grpc::Status::OK;
} catch (const std::exception& e) {
// TODO(nanzhou) enumerate all the possible expceptions in jwt-cpp
LOG(WARNING) << "Token verification failed!";
LOG(WARNING) << "Token is " << token;
LOG(WARNING) << "Public key is " << public_key;
LOG(WARNING) << "Expected subject is " << expected_subject
<< "; Expected audience is " << expected_audience
<< "; Expected issuer is " << expected_issuer;
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
}
} // namespace milotic::authz