From 3b1bb676820c70cb96a4326cb463ba42c677b0f9 Mon Sep 17 00:00:00 2001 From: Martin Jaime Flores Jr Date: Thu, 14 Mar 2024 15:29:22 -0500 Subject: [PATCH] [#108,#155,#235] Implement related OIDC features This commit implements Protected Resource mode for the HTTP API. It relies on both Confidential Client mode and the alternate User Mapping. --- .../include/irods/private/http_api/common.hpp | 14 ++ core/src/common.cpp | 187 ++++++++++++++++++ core/src/main.cpp | 57 +++++- endpoints/authentication/src/main.cpp | 102 +++++----- 4 files changed, 299 insertions(+), 61 deletions(-) diff --git a/core/include/irods/private/http_api/common.hpp b/core/include/irods/private/http_api/common.hpp index 840f70f4..c792d1ad 100644 --- a/core/include/irods/private/http_api/common.hpp +++ b/core/include/irods/private/http_api/common.hpp @@ -16,6 +16,8 @@ #include #include +#include + #include #include #include @@ -44,6 +46,7 @@ namespace irods::http using request_handler_map_type = std::unordered_map; using query_arguments_type = std::unordered_map; + using body_arguments = std::unordered_map; using handler_type = void (*)(session_pointer_type, request_type&, query_arguments_type&); // clang-format on @@ -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; + + auto map_json_to_user(const nlohmann::json& _json) -> std::optional; + auto resolve_client_identity(const request_type& _req) -> client_identity_resolution_result; auto execute_operation( diff --git a/core/src/common.cpp b/core/src/common.cpp index 8eb433ea..e983dd1e 100644 --- a/core/src/common.cpp +++ b/core/src/common.cpp @@ -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 #include #include #include @@ -18,12 +20,21 @@ #include #include +#include #include +#include #include #include #include + #include +#include + +// clang-format off +namespace beast = boost::beast; // from +namespace net = boost::asio; // from +// clang-format on namespace irods::http { @@ -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(_view.data()), + _view.size(), + reinterpret_cast(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 + { + constexpr auto http_version_number{11}; + beast::http::request 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 auto& client_secret{secret_key->get_ref()}; + 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 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 + { + 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())}; 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; @@ -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() == + "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()) { + 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)}; } diff --git a/core/src/main.cpp b/core/src/main.cpp index 7f2fe6d2..f6f86ea5 100644 --- a/core/src/main.cpp +++ b/core/src/main.cpp @@ -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" + ] + }} ] }} }}, @@ -485,6 +528,8 @@ auto print_configuration_template() -> void "state_timeout_in_seconds": 3600, "provider_url": "", "client_id": "", + "client_secret": "", + "mode": "client", "redirect_uri": "", "irods_user_claim": "", "tls_certificates_directory": "" @@ -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 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 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 diff --git a/endpoints/authentication/src/main.cpp b/endpoints/authentication/src/main.cpp index ed430fc2..b3bc5a9a 100644 --- a/endpoints/authentication/src/main.cpp +++ b/endpoints/authentication/src/main.cpp @@ -46,10 +46,19 @@ namespace beast = boost::beast; // from namespace net = boost::asio; // from // clang-format on -using body_arguments = std::unordered_map; - namespace irods::http::handler { + auto remove_client_from_body_if_confidential_client(body_arguments _body) -> body_arguments + { + const static bool has_client_secret{irods::http::globals::oidc_configuration().contains("client_secret")}; + + if (has_client_secret) { + _body.erase("client_id"); + } + + return _body; + } + auto hit_token_endpoint(std::string _encoded_body) -> nlohmann::json { const auto token_endpoint{ @@ -68,23 +77,16 @@ namespace irods::http::handler const auto url{*parsed_uri}; const auto port{irods::http::get_port_from_url(url)}; + auto req{irods::http::create_oidc_request(url)}; - // TCP thing - auto tcp_stream{irods::http::transport_factory(url.scheme_id(), io_ctx)}; - tcp_stream->connect(url.host(), *port); - - // Build Request - constexpr auto version_number{11}; - beast::http::request req{beast::http::verb::post, url.path(), version_number}; - req.set(beast::http::field::host, url.host()); - req.set(beast::http::field::user_agent, irods::http::version::server_name); - req.set(beast::http::field::content_type, - "application/x-www-form-urlencoded"); // Possibly set a diff way? - - // Send + // Attach body to request req.body() = std::move(_encoded_body); req.prepare_payload(); + // Connect to remote + auto tcp_stream{irods::http::transport_factory(url.scheme_id(), io_ctx)}; + tcp_stream->connect(url.host(), *port); + // Send request and Read back response auto res{tcp_stream->communicate(req)}; log::debug("Got the following resp back: {}", res.body()); @@ -93,20 +95,6 @@ namespace irods::http::handler return nlohmann::json::parse(res.body()); } - auto encode_body(const body_arguments& _args) -> std::string - { - auto encode_pair{[](const body_arguments::value_type& i) { - return fmt::format("{}={}", irods::http::encode(i.first), irods::http::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 is_error_response(const nlohmann::json& _response_to_check) -> bool { if (const auto error{_response_to_check.find("error")}; error != std::cend(_response_to_check)) { @@ -163,13 +151,24 @@ namespace irods::http::handler return {std::move(username), std::move(password)}; } + auto is_oidc_running_as_client() -> bool + { + static const auto oidc_stanza_exists = irods::http::globals::configuration().contains( + nlohmann::json::json_pointer{"/http_server/authentication/openid_connect"}); + + if (oidc_stanza_exists) { + static const auto is_client{ + irods::http::globals::oidc_configuration().at("mode").get_ref() == "client"}; + return is_client; + } + + return false; + } + IRODS_HTTP_API_ENDPOINT_ENTRY_FUNCTION_SIGNATURE(authentication) { if (_req.method() == boost::beast::http::verb::get) { - static const auto oidc_stanza_exists = irods::http::globals::configuration().contains( - nlohmann::json::json_pointer{"/http_server/authentication/openid_connect"}); - - if (!oidc_stanza_exists) { + if (!is_oidc_running_as_client()) { log::error("{}: HTTP GET method cannot be used for Basic authentication.", __func__); return _sess_ptr->send(fail(status_type::method_not_allowed)); } @@ -202,7 +201,7 @@ namespace irods::http::handler const auto auth_endpoint{irods::http::globals::oidc_endpoint_configuration() .at("authorization_endpoint") .get_ref()}; - const auto encoded_url{fmt::format("{}?{}", auth_endpoint, encode_body(args))}; + const auto encoded_url{fmt::format("{}?{}", auth_endpoint, irods::http::url_encode_body(args))}; log::debug("{}: Proper redirect to [{}]", fn, encoded_url); @@ -314,7 +313,8 @@ namespace irods::http::handler irods::http::globals::oidc_configuration().at("redirect_uri").get_ref()}}; // Encode the string, hit endpoint, get res - nlohmann::json oidc_response{hit_token_endpoint(encode_body(args))}; + nlohmann::json oidc_response{hit_token_endpoint( + irods::http::url_encode_body(remove_client_from_body_if_confidential_client(args)))}; // Determine if we have an "error" json... if (is_error_response(oidc_response)) { @@ -329,11 +329,9 @@ namespace irods::http::handler // TODO: Handle case where we throw!!! auto decoded_token{jwt::decode(jwt_token).get_payload_json()}; - // Verify 'irods_username' exists - const auto& irods_claim_name{irods::http::globals::oidc_configuration() - .at("irods_user_claim") - .get_ref()}; - if (!decoded_token.contains(irods_claim_name)) { + auto irods_username{irods::http::map_json_to_user(decoded_token)}; + + if (!irods_username) { const auto user{ decoded_token.contains("preferred_username") ? decoded_token.at("preferred_username").get() @@ -343,9 +341,6 @@ namespace irods::http::handler return _sess_ptr->send(fail(status_type::bad_request)); } - // Get irods username - const std::string& irods_name{decoded_token.at(irods_claim_name).get_ref()}; - // Issue token? static const auto seconds = irods::http::globals::configuration() @@ -355,7 +350,7 @@ namespace irods::http::handler auto bearer_token = irods::http::process_stash::insert(authenticated_client_info{ .auth_scheme = authorization_scheme::openid_connect, - .username = std::move(irods_name), + .username = *std::move(irods_username), .expires_at = std::chrono::steady_clock::now() + std::chrono::seconds{seconds}}); response_type res_rep{status_type::ok, _req.version()}; @@ -523,7 +518,9 @@ namespace irods::http::handler return _sess_ptr->send(std::move(res)); } // OAuth 2.0 Resource Owner Password Credentials Grant - else if (const auto alt_method{iter->value().find("iRODS ")}; alt_method != std::string_view::npos) { + else if (const auto alt_method{iter->value().find("iRODS ")}; + is_oidc_running_as_client() && alt_method != std::string_view::npos) + { // Decode username and password here!!!!! constexpr auto basic_auth_scheme_prefix_size = 6; const auto [username, password]{ @@ -543,7 +540,8 @@ namespace irods::http::handler {"password", password}}; // Query endpoint - nlohmann::json oidc_response{hit_token_endpoint(encode_body(args))}; + nlohmann::json oidc_response{hit_token_endpoint( + irods::http::url_encode_body(remove_client_from_body_if_confidential_client(args)))}; // Determine if we have an "error" json... if (is_error_response(oidc_response)) { @@ -555,12 +553,9 @@ namespace irods::http::handler // Feed to JWT parser auto decoded_token{jwt::decode(jwt_token).get_payload_json()}; + auto irods_username{irods::http::map_json_to_user(decoded_token)}; - // Verify 'irods_username' exists - const auto& irods_claim_name{irods::http::globals::oidc_configuration() - .at("irods_user_claim") - .get_ref()}; - if (!decoded_token.contains(irods_claim_name)) { + if (!irods_username) { const auto user{ decoded_token.contains("preferred_username") ? decoded_token.at("preferred_username").get() @@ -570,9 +565,6 @@ namespace irods::http::handler return _sess_ptr->send(fail(status_type::bad_request)); } - // Get irods username - const std::string& irods_name{decoded_token.at(irods_claim_name).get_ref()}; - // Issue token? static const auto seconds = irods::http::globals::configuration() @@ -581,7 +573,7 @@ namespace irods::http::handler .get(); auto bearer_token = irods::http::process_stash::insert(authenticated_client_info{ .auth_scheme = authorization_scheme::openid_connect, - .username = std::move(irods_name), + .username = *std::move(irods_username), .expires_at = std::chrono::steady_clock::now() + std::chrono::seconds{seconds}}); response_type res_rep{status_type::ok, _req.version()};