Skip to content

Commit

Permalink
[#108,#155,#235] Implement related OIDC features
Browse files Browse the repository at this point in the history
This commit implements Protected Resource mode for the HTTP API.
It relies on both Confidential Client mode and the alternate
User Mapping.
  • Loading branch information
MartinFlores751 authored and trel committed Mar 15, 2024
1 parent 99f07de commit 3b1bb67
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 61 deletions.
14 changes: 14 additions & 0 deletions core/include/irods/private/http_api/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>

#include <nlohmann/json.hpp>

#include <chrono>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -44,6 +46,7 @@ namespace irods::http
using request_handler_map_type = std::unordered_map<std::string_view, request_handler_type>;

using query_arguments_type = std::unordered_map<std::string, std::string>;
using body_arguments = std::unordered_map<std::string, std::string>;

using handler_type = void (*)(session_pointer_type, request_type&, query_arguments_type&);
// clang-format on
Expand Down Expand Up @@ -155,6 +158,17 @@ namespace irods::http

auto parse_url(const request_type& _req) -> url;

auto url_encode_body(const body_arguments& _args) -> std::string;

auto safe_base64_encode(std::string_view _view) -> std::string;

auto create_host_field(boost::urls::url_view _url, std::string_view _port) -> std::string;

auto create_oidc_request(boost::urls::url_view _url)
-> boost::beast::http::request<boost::beast::http::string_body>;

auto map_json_to_user(const nlohmann::json& _json) -> std::optional<std::string>;

auto resolve_client_identity(const request_type& _req) -> client_identity_resolution_result;

auto execute_operation(
Expand Down
187 changes: 187 additions & 0 deletions core/src/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
#include "irods/private/http_api/log.hpp"
#include "irods/private/http_api/process_stash.hpp"
#include "irods/private/http_api/session.hpp"
#include "irods/private/http_api/transport.hpp"
#include "irods/private/http_api/version.hpp"

#include <irods/base64.hpp>
#include <irods/client_connection.hpp>
#include <irods/irods_at_scope_exit.hpp>
#include <irods/irods_exception.hpp>
Expand All @@ -18,12 +20,21 @@
#include <irods/ticketAdmin.h>

#include <boost/any.hpp>
#include <boost/asio.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/beast.hpp>

#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>

#include <string>
#include <string_view>

// clang-format off
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
// clang-format on

namespace irods::http
{
Expand Down Expand Up @@ -207,6 +218,159 @@ namespace irods::http
return parse_url(fmt::format("http://ignored{}", _req.target()));
} // parse_url

auto url_encode_body(const body_arguments& _args) -> std::string
{
auto encode_pair{[](const body_arguments::value_type& i) {
return fmt::format("{}={}", encode(i.first), encode(i.second));
}};

return std::transform_reduce(
std::next(std::cbegin(_args)),
std::cend(_args),
encode_pair(*std::cbegin(_args)),
[](const auto& a, const auto& b) { return fmt::format("{}&{}", a, b); },
encode_pair);
}

auto safe_base64_encode(std::string_view _view) -> std::string
{
namespace logging = irods::http::log;

constexpr auto char_per_byte_set{4};
constexpr auto byte_set_size{3};

const auto max_size{char_per_byte_set * ((_view.size() + 2) / byte_set_size)};
auto max_size_plus_null_term{max_size + 1};

std::string encoded_data;
encoded_data.resize(max_size);

auto res{irods::base64_encode(
reinterpret_cast<const unsigned char*>(_view.data()),
_view.size(),
reinterpret_cast<unsigned char*>(encoded_data.data()),
&max_size_plus_null_term)};
if (res) {
logging::error("{}: Failed to encode the string [{}], output may be unusable.", __func__, _view);
}
return encoded_data;
}

auto create_host_field(boost::urls::url_view _url, std::string_view _port) -> std::string
{
if ((_port == "443" && _url.scheme_id() == boost::urls::scheme::https) ||
(_port == "80" && _url.scheme_id() == boost::urls::scheme::http))
{
return _url.host();
}
return fmt::format("{}:{}", _url.host(), _port);
}

auto create_oidc_request(boost::urls::url_view _url) -> beast::http::request<beast::http::string_body>
{
constexpr auto http_version_number{11};
beast::http::request<beast::http::string_body> req{beast::http::verb::post, _url.path(), http_version_number};

const auto port{get_port_from_url(_url)};

req.set(beast::http::field::host, create_host_field(_url, *port));
req.set(beast::http::field::user_agent, irods::http::version::server_name);
req.set(beast::http::field::content_type, "application/x-www-form-urlencoded");
req.set(beast::http::field::accept, "application/json");

if (const auto secret_key{irods::http::globals::oidc_configuration().find("client_secret")};
secret_key != std::end(irods::http::globals::oidc_configuration()))
{
const auto format_bearer_token{[](std::string_view _client_id, std::string_view _client_secret) {
auto encode_me{fmt::format("{}:{}", encode(_client_id), encode(_client_secret))};
return safe_base64_encode(encode_me);
}};

const auto& client_id{
irods::http::globals::oidc_configuration().at("client_id").get_ref<const std::string&>()};
const auto& client_secret{secret_key->get_ref<const std::string&>()};
const auto auth_string{fmt::format("Basic {}", format_bearer_token(client_id, client_secret))};

req.set(beast::http::field::authorization, auth_string);
}

return req;
}

auto hit_introspection_endpoint(std::string _encoded_body) -> nlohmann::json
{
namespace logging = irods::http::log;

const auto introspection_endpoint{irods::http::globals::oidc_endpoint_configuration()
.at("introspection_endpoint")
.get_ref<const std::string&>()};

const auto parsed_uri{boost::urls::parse_uri(introspection_endpoint)};

if (parsed_uri.has_error()) {
logging::error(
"{}: Error trying to parse introspection_endpoint [{}]. Please check configuration.",
__func__,
introspection_endpoint);
return {{"error", "bad endpoint"}};
}

const auto url{*parsed_uri};
const auto port{get_port_from_url(url)};

// Addr
net::io_context io_ctx;
auto tcp_stream{irods::http::transport_factory(url.scheme_id(), io_ctx)};
tcp_stream->connect(url.host(), *port);

// Build Request
auto req{create_oidc_request(url)};
req.body() = std::move(_encoded_body);
req.prepare_payload();

// Send request & receive response
auto res{tcp_stream->communicate(req)};

logging::debug("{}: Received the following response: [{}]", __func__, res.body());

// JSONize response
return nlohmann::json::parse(res.body());
}

auto map_json_to_user(const nlohmann::json& _json) -> std::optional<std::string>
{
const auto& oidc_config{irods::http::globals::oidc_configuration()};
const static auto user_claim{oidc_config.find("irods_user_claim")};
const static auto attribute_mapping{oidc_config.find("user_attribute_mapping")};

if (user_claim != std::end(oidc_config)) {
if (auto claim{_json.find(user_claim->get_ref<const std::string&>())}; claim != std::end(_json)) {
return {*claim};
}
}
else if (attribute_mapping != std::end(oidc_config)) {
const auto& mappings{*attribute_mapping};

for (auto& [key, value] : mappings.items()) {
const auto comparison_func{[&_json](const auto& _iter) -> bool {
const auto value_of_interest{_json.find(_iter.key())};
if (value_of_interest == std::end(_json)) {
return false;
}

return *value_of_interest == _iter.value();
}};

if (auto proxy_iter{value.items()};
std::all_of(std::begin(proxy_iter), std::end(proxy_iter), comparison_func)) {
return key;
}
}
}

return std::nullopt;
}

auto resolve_client_identity(const request_type& _req) -> client_identity_resolution_result
{
namespace logging = irods::http::log;
Expand Down Expand Up @@ -237,6 +401,29 @@ namespace irods::http
// Verify the bearer token is known to the server. If not, return an error.
auto mapped_value{irods::http::process_stash::find(bearer_token)};
if (!mapped_value.has_value()) {
// If we're running as a protected resource, assume we have a OIDC token
if (irods::http::globals::oidc_configuration().at("mode").get_ref<const std::string&>() ==
"protected_resource") {
body_arguments args{{"token", bearer_token}, {"token_type_hint", "access_token"}};

auto json_res{hit_introspection_endpoint(url_encode_body(args))};

// Validate access token
if (!json_res.at("active").get<bool>()) {
logging::warn("{}: Access token is invalid or expired.", __func__);
return {.response = fail(status_type::unauthorized)};
}

// Do mapping of user to irods user
auto user{map_json_to_user(json_res)};
if (user) {
return {.client_info = {.username = *std::move(user)}};
}

logging::warn("{}: Could not find a matching user.", __func__);
return {.response = fail(status_type::unauthorized)};
}

logging::error("{}: Could not find bearer token matching [{}].", __func__, bearer_token);
return {.response = fail(status_type::unauthorized)};
}
Expand Down
57 changes: 51 additions & 6 deletions core/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,29 +239,72 @@ constexpr auto default_jsonschema() -> std::string_view
"minimum": 1
}},
"provider_url": {{
"type": "string"
"type": "string",
"format": "uri"
}},
"mode": {{
"enum": ["client", "protected_resource"]
}},
"client_id": {{
"type": "string"
}},
"redirect_uri": {{
"client_secret": {{
"type": "string"
}},
"redirect_uri": {{
"type": "string",
"format": "uri"
}},
"irods_user_claim": {{
"type": "string"
}},
"tls_certificates_directory": {{
"type": "string"
}},
"user_attribute_mapping": {{
"type": "object",
"additionalProperties": {{
"type": "object"
}},
"minProperties": 1
}}
}},
"required": [
"timeout_in_seconds",
"state_timeout_in_seconds",
"provider_url",
"mode",
"client_id",
"redirect_uri",
"irods_user_claim",
"tls_certificates_directory"
],
"oneOf": [
{{
"required": [
"irods_user_claim"
]
}},
{{
"required": [
"user_attribute_mapping"
]
}}
],
"anyOf": [
{{
"not": {{
"properties": {{
"mode": {{
"const": "protected_resource"
}}
}}
}}
}},
{{
"required": [
"client_secret"
]
}}
]
}}
}},
Expand Down Expand Up @@ -485,6 +528,8 @@ auto print_configuration_template() -> void
"state_timeout_in_seconds": 3600,
"provider_url": "<string>",
"client_id": "<string>",
"client_secret": "<string>",
"mode": "client",
"redirect_uri": "<string>",
"irods_user_claim": "<string>",
"tls_certificates_directory": "<string>"
Expand Down Expand Up @@ -779,9 +824,9 @@ auto load_oidc_configuration(const json& _config, json& _oi_config, json& _endpo
tcp_stream->connect(url.host(), *port);

// Build Request
constexpr auto version_number{11};
beast::http::request<beast::http::string_body> req{beast::http::verb::get, path, version_number};
req.set(beast::http::field::host, fmt::format("{}:{}", url.host(), *port));
constexpr auto http_version_number{11};
beast::http::request<beast::http::string_body> req{beast::http::verb::get, path, http_version_number};
req.set(beast::http::field::host, irods::http::create_host_field(url, *port));
req.set(beast::http::field::user_agent, irods::http::version::server_name);

// Sends and recieves response
Expand Down
Loading

0 comments on commit 3b1bb67

Please sign in to comment.